]> git.gir.st - VimFx.git/blob - extension/lib/commands.coffee
Lay the foundation for using multi-process Firefox
[VimFx.git] / extension / lib / commands.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014.
4 # Copyright Wang Zhuochun 2013, 2014.
5 #
6 # This file is part of VimFx.
7 #
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.
12 #
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.
17 #
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/>.
20 ###
21
22 help = require('./help')
23 { Marker } = require('./marker')
24 utils = require('./utils')
25
26 { isProperLink, isTextInputElement, isContentEditable } = utils
27
28 XULDocument = Ci.nsIDOMXULDocument
29
30 commands = {}
31
32
33
34 commands.focus_location_bar = ({ vim }) ->
35 # This function works even if the Address Bar has been removed.
36 vim.rootWindow.focusAndSelectUrlBar()
37
38 commands.focus_search_bar = ({ vim }) ->
39 # The `.webSearch()` method opens a search engine in a tab if the Search Bar
40 # has been removed. Therefore we first check if it exists.
41 if vim.rootWindow.BrowserSearch.searchBar
42 vim.rootWindow.BrowserSearch.webSearch()
43
44 helper_paste_and_go = (event, { vim }) ->
45 { gURLBar, goDoCommand } = vim.rootWindow
46 gURLBar.value = vim.rootWindow.readFromClipboard()
47 gURLBar.handleCommand(event)
48
49 commands.paste_and_go = helper_paste_and_go.bind(null, null)
50
51 commands.paste_and_go_in_tab = helper_paste_and_go.bind(null, {altKey: true})
52
53 commands.copy_current_url = ({ vim }) ->
54 utils.writeToClipboard(vim.rootWindow.gBrowser.currentURI.spec)
55
56 # Go up one level in the URL hierarchy.
57 commands.go_up_path = ({ vim, count = 1 }) ->
58 { pathname } = vim.window.location
59 vim.window.location.pathname = pathname.replace(
60 /// (?: /[^/]+ ){1,#{ count }} /?$ ///, ''
61 )
62
63 # Go up to root of the URL hierarchy.
64 commands.go_to_root = ({ vim }) ->
65 vim.window.location.href = vim.window.location.origin
66
67 commands.go_home = ({ vim }) ->
68 vim.rootWindow.BrowserHome()
69
70 helper_go_history = (num, { vim, count = 1 }) ->
71 { gBrowser } = vim.rootWindow
72 { index, count: length } = gBrowser.sessionHistory
73 newIndex = index + num * count
74 newIndex = Math.max(newIndex, 0)
75 newIndex = Math.min(newIndex, length - 1)
76 gBrowser.gotoIndex(newIndex) unless newIndex == index
77
78 commands.history_back = helper_go_history.bind(null, -1)
79
80 commands.history_forward = helper_go_history.bind(null, +1)
81
82 commands.reload = ({ vim }) ->
83 vim.rootWindow.BrowserReload()
84
85 commands.reload_force = ({ vim }) ->
86 vim.rootWindow.BrowserReloadSkipCache()
87
88 commands.reload_all = ({ vim }) ->
89 vim.rootWindow.gBrowser.reloadAllTabs()
90
91 commands.reload_all_force = ({ vim }) ->
92 for tab in vim.rootWindow.gBrowser.visibleTabs
93 gBrowser = tab.linkedBrowser
94 consts = gBrowser.webNavigation
95 flags = consts.LOAD_FLAGS_BYPASS_PROXY | consts.LOAD_FLAGS_BYPASS_CACHE
96 gBrowser.reload(flags)
97 return
98
99 commands.stop = ({ vim }) ->
100 vim.rootWindow.BrowserStop()
101
102 commands.stop_all = ({ vim }) ->
103 for tab in vim.rootWindow.gBrowser.visibleTabs
104 tab.linkedBrowser.stop()
105 return
106
107
108
109 axisMap =
110 x: ['left', 'scrollLeftMax', 'clientWidth', 'horizontalScrollDistance', 5]
111 y: ['top', 'scrollTopMax', 'clientHeight', 'verticalScrollDistance', 20]
112
113 helper_scroll = (method, type, axis, amount, { vim, event, count = 1 }) ->
114 frameDocument = event.target.ownerDocument
115 element =
116 if vim.state.scrollableElements.has(event.target)
117 event.target
118 else
119 frameDocument.documentElement
120
121 [ direction, max, dimension, distance, lineAmount ] = axisMap[axis]
122
123 if method == 'scrollTo'
124 amount = Math.min(amount, element[max])
125 else
126 unit = switch type
127 when 'lines'
128 prefs.root.get("toolkit.scrollbox.#{ distance }") * lineAmount
129 when 'pages'
130 element[dimension]
131 amount *= unit * count
132
133 options = {}
134 options[direction] = amount
135 if prefs.root.get('general.smoothScroll') and
136 prefs.root.get("general.smoothScroll.#{ type }")
137 options.behavior = 'smooth'
138
139 prefs.root.tmp(
140 'layout.css.scroll-behavior.spring-constant',
141 vim.parent.options["smoothScroll.#{ type }.spring-constant"],
142 ->
143 element[method](options)
144 # When scrolling the whole page, the body sometimes needs to be scrolled
145 # too.
146 if element == frameDocument.documentElement
147 frameDocument.body?[method](options)
148 )
149
150 scroll = Function::bind.bind(helper_scroll, null)
151
152 commands.scroll_left = scroll('scrollBy', 'lines', 'x', -1)
153 commands.scroll_right = scroll('scrollBy', 'lines', 'x', +1)
154 commands.scroll_down = scroll('scrollBy', 'lines', 'y', +1)
155 commands.scroll_up = scroll('scrollBy', 'lines', 'y', -1)
156 commands.scroll_page_down = scroll('scrollBy', 'pages', 'y', +1)
157 commands.scroll_page_up = scroll('scrollBy', 'pages', 'y', -1)
158 commands.scroll_half_page_down = scroll('scrollBy', 'pages', 'y', +0.5)
159 commands.scroll_half_page_up = scroll('scrollBy', 'pages', 'y', -0.5)
160 commands.scroll_to_top = scroll('scrollTo', 'other', 'y', 0)
161 commands.scroll_to_bottom = scroll('scrollTo', 'other', 'y', Infinity)
162 commands.scroll_to_left = scroll('scrollTo', 'other', 'x', 0)
163 commands.scroll_to_right = scroll('scrollTo', 'other', 'x', Infinity)
164
165
166
167 commands.tab_new = ({ vim }) ->
168 vim.rootWindow.BrowserOpenTab()
169
170 commands.tab_duplicate = ({ vim }) ->
171 { gBrowser } = vim.rootWindow
172 gBrowser.duplicateTab(gBrowser.selectedTab)
173
174 absoluteTabIndex = (relativeIndex, gBrowser) ->
175 tabs = gBrowser.visibleTabs
176 { selectedTab } = gBrowser
177
178 currentIndex = tabs.indexOf(selectedTab)
179 absoluteIndex = currentIndex + relativeIndex
180 numTabs = tabs.length
181
182 wrap = (Math.abs(relativeIndex) == 1)
183 if wrap
184 absoluteIndex %%= numTabs
185 else
186 absoluteIndex = Math.max(0, absoluteIndex)
187 absoluteIndex = Math.min(absoluteIndex, numTabs - 1)
188
189 return absoluteIndex
190
191 helper_switch_tab = (direction, { vim, count = 1 }) ->
192 { gBrowser } = vim.rootWindow
193 gBrowser.selectTabAtIndex(absoluteTabIndex(direction * count, gBrowser))
194
195 commands.tab_select_previous = helper_switch_tab.bind(null, -1)
196
197 commands.tab_select_next = helper_switch_tab.bind(null, +1)
198
199 helper_move_tab = (direction, { vim, count = 1 }) ->
200 { gBrowser } = vim.rootWindow
201 { selectedTab } = gBrowser
202 { pinned } = selectedTab
203
204 index = absoluteTabIndex(direction * count, gBrowser)
205
206 if index < gBrowser._numPinnedTabs
207 gBrowser.pinTab(selectedTab) unless pinned
208 else
209 gBrowser.unpinTab(selectedTab) if pinned
210
211 gBrowser.moveTabTo(selectedTab, index)
212
213 commands.tab_move_backward = helper_move_tab.bind(null, -1)
214
215 commands.tab_move_forward = helper_move_tab.bind(null, +1)
216
217 commands.tab_select_first = ({ vim }) ->
218 vim.rootWindow.gBrowser.selectTabAtIndex(0)
219
220 commands.tab_select_first_non_pinned = ({ vim }) ->
221 firstNonPinned = vim.rootWindow.gBrowser._numPinnedTabs
222 vim.rootWindow.gBrowser.selectTabAtIndex(firstNonPinned)
223
224 commands.tab_select_last = ({ vim }) ->
225 vim.rootWindow.gBrowser.selectTabAtIndex(-1)
226
227 commands.tab_toggle_pinned = ({ vim }) ->
228 currentTab = vim.rootWindow.gBrowser.selectedTab
229 if currentTab.pinned
230 vim.rootWindow.gBrowser.unpinTab(currentTab)
231 else
232 vim.rootWindow.gBrowser.pinTab(currentTab)
233
234 commands.tab_close = ({ vim, count = 1 }) ->
235 { gBrowser } = vim.rootWindow
236 return if gBrowser.selectedTab.pinned
237 currentIndex = gBrowser.visibleTabs.indexOf(gBrowser.selectedTab)
238 for tab in gBrowser.visibleTabs[currentIndex...(currentIndex + count)]
239 gBrowser.removeTab(tab)
240 return
241
242 commands.tab_restore = ({ vim, count = 1 }) ->
243 vim.rootWindow.undoCloseTab() for [1..count] by 1
244
245 commands.tab_close_to_end = ({ vim }) ->
246 { gBrowser } = vim.rootWindow
247 gBrowser.removeTabsToTheEndFrom(gBrowser.selectedTab)
248
249 commands.tab_close_other = ({ vim }) ->
250 { gBrowser } = vim.rootWindow
251 gBrowser.removeAllTabsBut(gBrowser.selectedTab)
252
253
254
255 # Combine links with the same href.
256 combine = (hrefs, marker) ->
257 if marker.type == 'link'
258 { href } = marker.element
259 if href of hrefs
260 parent = hrefs[href]
261 marker.parent = parent
262 parent.weight += marker.weight
263 parent.numChildren++
264 else
265 hrefs[href] = marker
266 return marker
267
268 follow_callback = (vim, { inTab, inBackground }, marker, count, keyStr) ->
269 isLast = (count == 1)
270 isLink = (marker.type == 'link')
271
272 switch
273 when keyStr.startsWith(vim.parent.options.hints_toggle_in_tab)
274 inTab = not inTab
275 when keyStr.startsWith(vim.parent.options.hints_toggle_in_background)
276 inTab = true
277 inBackground = not inBackground
278 else
279 unless isLast
280 inTab = true
281 inBackground = true
282
283 inTab = false unless isLink
284
285 if marker.type == 'text' or (isLink and not (inTab and inBackground))
286 isLast = true
287
288 { element } = marker
289 utils.focusElement(element)
290
291 if inTab
292 utils.openTab(vim.rootWindow, element.href, {
293 inBackground
294 relatedToCurrent: true
295 })
296 else
297 if element.target == '_blank' and vim.parent.options.prevent_target_blank
298 targetReset = element.target
299 element.target = ''
300 utils.simulateClick(element)
301 element.target = targetReset if targetReset
302
303 return not isLast
304
305 # Follow links, focus text inputs and click buttons with hint markers.
306 commands.follow = ({ vim, count = 1 }) ->
307 hrefs = {}
308 filter = (element, getElementShape) ->
309 document = element.ownerDocument
310 isXUL = (document instanceof XULDocument)
311 semantic = true
312 switch
313 when isProperLink(element)
314 type = 'link'
315 when isTextInputElement(element) or isContentEditable(element)
316 type = 'text'
317 when element.tabIndex > -1 and
318 not (isXUL and element.nodeName.endsWith('box'))
319 type = 'clickable'
320 unless isXUL or element.nodeName in ['A', 'INPUT', 'BUTTON']
321 semantic = false
322 when element != document.documentElement and
323 vim.state.scrollableElements.has(element)
324 type = 'scrollable'
325 when element.hasAttribute('onclick') or
326 element.hasAttribute('onmousedown') or
327 element.hasAttribute('onmouseup') or
328 element.hasAttribute('oncommand') or
329 element.getAttribute('role') in ['link', 'button'] or
330 # Twitter special-case.
331 element.classList.contains('js-new-tweets-bar') or
332 # Feedly special-case.
333 element.hasAttribute('data-app-action') or
334 element.hasAttribute('data-uri') or
335 element.hasAttribute('data-page-action')
336 type = 'clickable'
337 semantic = false
338 # Putting markers on `<label>` elements is generally redundant, because
339 # its `<input>` gets one. However, some sites hide the actual `<input>`
340 # but keeps the `<label>` to click, either for styling purposes or to keep
341 # the `<input>` hidden until it is used. In those cases we should add a
342 # marker for the `<label>`.
343 when element.nodeName == 'LABEL'
344 if element.htmlFor
345 input = document.getElementById(element.htmlFor)
346 if input and not getElementShape(input)
347 type = 'clickable'
348 # Elements that have “button” somewhere in the class might be clickable,
349 # unless they contain a real link or button or yet an element with
350 # “button” somewhere in the class, in which case they likely are
351 # “button-wrapper”s. (`<SVG element>.className` is not a string!)
352 when not isXUL and typeof element.className == 'string' and
353 element.className.toLowerCase().contains('button')
354 unless element.querySelector('a, button, [class*=button]')
355 type = 'clickable'
356 semantic = false
357 # When viewing an image it should get a marker to toggle zoom.
358 when document.body?.childElementCount == 1 and
359 element.nodeName == 'IMG' and
360 (element.classList.contains('overflowing') or
361 element.classList.contains('shrinkToFit'))
362 type = 'clickable'
363 return unless type
364 return unless shape = getElementShape(element)
365 return combine(hrefs, new Marker(element, shape, {semantic, type}))
366
367 callback = follow_callback.bind(null, vim, {inTab: false, inBackground: true})
368
369 vim.enterMode('hints', filter, callback, count)
370
371 # Follow links in a new background tab with hint markers.
372 commands.follow_in_tab = ({ vim, count = 1 }, inBackground = true) ->
373 hrefs = {}
374 filter = (element, getElementShape) ->
375 return unless isProperLink(element)
376 return unless shape = getElementShape(element)
377 return combine(hrefs, new Marker(element, shape,
378 {semantic: true, type: 'link'}))
379
380 callback = follow_callback.bind(null, vim, {inTab: true, inBackground})
381
382 vim.enterMode('hints', filter, callback, count)
383
384 # Follow links in a new foreground tab with hint markers.
385 commands.follow_in_focused_tab = (args) ->
386 commands.follow_in_tab(args, false)
387
388 # Like command_follow but multiple times.
389 commands.follow_multiple = (args) ->
390 args.count = Infinity
391 commands.follow(args)
392
393 # Copy the URL or text of a markable element to the system clipboard.
394 commands.follow_copy = ({ vim }) ->
395 hrefs = {}
396 filter = (element, getElementShape) ->
397 type = switch
398 when isProperLink(element) then 'link'
399 when isTextInputElement(element) then 'textInput'
400 when isContentEditable(element) then 'contenteditable'
401 return unless type
402 return unless shape = getElementShape(element)
403 return combine(hrefs, new Marker(element, shape, {semantic: true, type}))
404
405 callback = (marker) ->
406 { element } = marker
407 text = switch marker.type
408 when 'link' then element.href
409 when 'textInput' then element.value
410 when 'contenteditable' then element.textContent
411 utils.writeToClipboard(text)
412
413 vim.enterMode('hints', filter, callback)
414
415 # Focus element with hint markers.
416 commands.follow_focus = ({ vim }) ->
417 filter = (element, getElementShape) ->
418 type = switch
419 when element.tabIndex > -1
420 'focusable'
421 when element != element.ownerDocument.documentElement and
422 vim.state.scrollableElements.has(element)
423 'scrollable'
424 return unless type
425 return unless shape = getElementShape(element)
426 return new Marker(element, shape, {semantic: true, type})
427
428 callback = (marker) ->
429 { element } = marker
430 utils.focusElement(element, {select: true})
431
432 vim.enterMode('hints', filter, callback)
433
434 helper_follow_pattern = (type, { vim }) ->
435 { document } = vim.window
436
437 # If there’s a `<link rel=prev/next>` element we use that.
438 for link in document.head?.getElementsByTagName('link')
439 # Also support `rel=previous`, just like Google.
440 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
441 vim.rootWindow.gBrowser.loadURI(link.href)
442 return
443
444 # Otherwise we look for a link or button on the page that seems to go to the
445 # previous or next page.
446 candidates = document.querySelectorAll(vim.parent.options.pattern_selector)
447
448 # Note: Earlier patterns should be favored.
449 patterns = vim.parent.options["#{ type }_patterns"]
450
451 # Search for the prev/next patterns in the following attributes of the
452 # element. `rel` should be kept as the first attribute, since the standard way
453 # of marking up prev/next links (`rel="prev"` and `rel="next"`) should be
454 # favored. Even though some of these attributes only allow a fixed set of
455 # keywords, we pattern-match them anyways since lots of sites don’t follow the
456 # spec and use the attributes arbitrarily.
457 attrs = vim.parent.options.pattern_attrs
458
459 matchingLink = do ->
460 # Helper function that matches a string against all the patterns.
461 matches = (text) -> patterns.some((regex) -> regex.test(text))
462
463 # First search in attributes (favoring earlier attributes) as it's likely
464 # that they are more specific than text contexts.
465 for attr in attrs
466 for element in candidates
467 return element if matches(element.getAttribute(attr))
468
469 # Then search in element contents.
470 for element in candidates
471 return element if matches(element.textContent)
472
473 return null
474
475 utils.simulateClick(matchingLink) if matchingLink
476
477 commands.follow_previous = helper_follow_pattern.bind(null, 'prev')
478
479 commands.follow_next = helper_follow_pattern.bind(null, 'next')
480
481 # Focus last focused or first text input.
482 commands.focus_text_input = ({ vim, storage, count = null }) ->
483 { lastFocusedTextInput } = vim.state
484 inputs = Array.filter(
485 vim.window.document.querySelectorAll('input, textarea'), (element) ->
486 return utils.isTextInputElement(element) and utils.area(element) > 0
487 )
488 if lastFocusedTextInput and lastFocusedTextInput not in inputs
489 inputs.push(lastFocusedTextInput)
490 return unless inputs.length > 0
491 inputs.sort((a, b) -> a.tabIndex - b.tabIndex)
492 unless count?
493 count =
494 if lastFocusedTextInput
495 inputs.indexOf(lastFocusedTextInput) + 1
496 else
497 1
498 index = Math.min(count, inputs.length) - 1
499 utils.focusElement(inputs[index], {select: true})
500 storage.inputs = inputs
501
502 # Switch between text inputs or simulate `<tab>`.
503 helper_move_focus = (direction, { vim, storage }) ->
504 if storage.inputs
505 { inputs } = storage
506 nextInput = inputs[(storage.inputIndex + direction) %% inputs.length]
507 utils.focusElement(nextInput, {select: true})
508 else
509 focusManager = Cc['@mozilla.org/focus-manager;1']
510 .getService(Ci.nsIFocusManager)
511 direction =
512 if direction == -1
513 focusManager.MOVEFOCUS_BACKWARD
514 else
515 focusManager.MOVEFOCUS_FORWARD
516 focusManager.moveFocus(
517 null, # Use current window.
518 null, # Move relative to the currently focused element.
519 direction,
520 focusManager.FLAG_BYKEY
521 )
522
523 commands.focus_next = helper_move_focus.bind(null, +1)
524 commands.focus_previous = helper_move_focus.bind(null, -1)
525
526
527
528 findStorage = {lastSearchString: ''}
529
530 helper_find = (highlight, { vim }) ->
531 findBar = vim.rootWindow.gBrowser.getFindBar()
532
533 findBar.onFindCommand()
534 utils.focusElement(findBar._findField, {select: true})
535
536 return unless highlightButton = findBar.getElement('highlight')
537 if highlightButton.checked != highlight
538 highlightButton.click()
539
540 # Open the find bar, making sure that hightlighting is off.
541 commands.find = helper_find.bind(null, false)
542
543 # Open the find bar, making sure that hightlighting is on.
544 commands.find_highlight_all = helper_find.bind(null, true)
545
546 helper_find_again = (direction, { vim }) ->
547 findBar = vim.rootWindow.gBrowser.getFindBar()
548 if findStorage.lastSearchString.length > 0
549 findBar._findField.value = findStorage.lastSearchString
550 findBar.onFindAgainCommand(direction)
551 message = findBar._findStatusDesc.textContent
552 vim.notify(message) if message
553
554 commands.find_next = helper_find_again.bind(null, false)
555
556 commands.find_previous = helper_find_again.bind(null, true)
557
558
559
560 commands.enter_mode_ignore = ({ vim }) ->
561 vim.enterMode('ignore')
562
563 # Quote next keypress (pass it through to the page).
564 commands.quote = ({ vim, count = 1 }) ->
565 vim.enterMode('ignore', count)
566
567 # Display the Help Dialog.
568 commands.help = ({ vim }) ->
569 help.injectHelp(vim.rootWindow, vim.parent)
570
571 # Open and focus the Developer Toolbar.
572 commands.dev = ({ vim }) ->
573 vim.rootWindow.DeveloperToolbar.show(true) # `true` to focus.
574
575 commands.esc = ({ vim, event }) ->
576 utils.blurActiveElement(vim.window)
577
578 # Some browser UI elements, such as the web console, are not marked as
579 # focusable, so we need to force the blur.
580 utils.blurActiveElement(vim.rootWindow, {force: true})
581
582 help.removeHelp(vim.rootWindow)
583
584 vim.rootWindow.DeveloperToolbar.hide()
585
586 vim.rootWindow.gBrowser.getFindBar().close()
587
588 vim.rootWindow.TabView.hide()
589
590 { document } = vim.window
591 if document.exitFullscreen
592 document.exitFullscreen()
593 else
594 document.mozCancelFullScreen()
595
596
597
598 module.exports = {
599 commands
600 findStorage
601 }
Imprint / Impressum