From 5a855c02c2d24570064fe4088e1ab6a21dd0b891 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sun, 8 May 2016 20:56:54 +0200 Subject: [PATCH] Speed up and refactor Hints mode This makes most hints show up on screen up to twice as fast as before. The rest of the hints show up at the same speed as before. Hints generation has been optimized for the common case, by looking for markable elements in two passes (if needed and possible). First, we look for common and fast-to-find elements (such as links); then we look for everything else. Addresses #409. Code has been pulled out of marker.coffee, modes.coffee and hints.coffee, and put into the new marker-container.coffee. hints.coffee was then renamed to markable-elements.coffee (because all it does now is finding markable elements). The concept of tagging elements as "semantic" has been removed. The idea was to give "unsemantic" elements worse hints (so they wouldn't trump links, for example). Now, such elements are usually found in the second pass instead, which gives about the same effect. `` in Hints mode no longer replaces all hints with new ones for _all_ elements on screen. Instead, it replaces the hints with hints for all _unmarked_ elements on screen. Pressing `` again toggles back to the original hints. There is a breaking API change for `vimfx.setHintMatcher(hintMatcher)`. `hintMatcher` no longer receives and returns an object (of the shape `{type, semantic}`), but instead simply receives and returns the `type` of the element. --- documentation/api.md | 27 +- documentation/commands.md | 29 +- documentation/options.md | 42 ++- extension/lib/commands-frame.coffee | 91 +++--- extension/lib/commands.coffee | 139 +++++---- extension/lib/defaults.coffee | 4 +- extension/lib/help.coffee | 6 - extension/lib/main.coffee | 6 + ...{hints.coffee => markable-elements.coffee} | 147 ++------- extension/lib/marker-container.coffee | 282 ++++++++++++++++++ extension/lib/marker.coffee | 68 +---- extension/lib/modes.coffee | 67 ++--- extension/lib/parse-prefs.coffee | 14 +- extension/lib/viewport.coffee | 5 +- extension/locale/de/vimfx.properties | 2 +- extension/locale/en-US/vimfx.properties | 2 +- extension/locale/es/vimfx.properties | 2 +- extension/locale/fr/vimfx.properties | 2 +- extension/locale/id/vimfx.properties | 2 +- extension/locale/it/vimfx.properties | 2 +- extension/locale/ja/vimfx.properties | 2 +- extension/locale/nl/vimfx.properties | 2 +- extension/locale/pt-BR/vimfx.properties | 2 +- extension/locale/ru/vimfx.properties | 2 +- extension/locale/sv-SE/vimfx.properties | 2 +- extension/locale/zh-CN/vimfx.properties | 2 +- extension/locale/zh-TW/vimfx.properties | 2 +- extension/test/test-api.coffee | 12 +- 28 files changed, 563 insertions(+), 402 deletions(-) rename extension/lib/{hints.coffee => markable-elements.coffee} (68%) create mode 100644 extension/lib/marker-container.coffee diff --git a/documentation/api.md b/documentation/api.md index ba790bd..ab4af3d 100644 --- a/documentation/api.md +++ b/documentation/api.md @@ -721,9 +721,9 @@ If you call `vimfx.setHintMatcher(hintMatcher)` more than once, only the `hintMatcher` provided the last time will be used. ```js -vimfx.setHintMatcher((id, element, {type, semantic}) => { - // Inspect `element` and change `type` and `semantic` if needed. - return {type, semantic} +vimfx.setHintMatcher((id, element, type) => { + // Inspect `element` and return a different `type` if needed. + return type }) ``` @@ -735,19 +735,16 @@ The arguments passed to the `hintMatcher` function are: - `'tab'`: `F`, `gf` or `gF`. - `'copy'`: `yf`. - `'focus'`: `zf`. + - `'select'`: `v`, `zv` or `yv`. - element: `Element`. One out of all elements currently inside the viewport. -- info: `Object`. It has the following properties: +- type: `String` or `null`. If a string, it means that `element` should get a + hint. If `null`, it won’t. See the available strings below. When a marker + is matched, `type` decides what happens to `element`. - - type: `String` or `null`. If a string, it means that `element` should get a - hint. If `null`, it won’t. See the available strings below. When a marker - is matched, `type` decides what happens to `element`. - - semantic: `Boolean`. Indicates whether or not the element is “semantic.” - Semantic elements get better hints. - - This object contains information on how VimFx has matched `element`. You have - the opportunity to change this. + This parameter tells how VimFx has matched `element`. You have the opportunity + to change that. The available type strings depend on `id`: @@ -781,11 +778,7 @@ The available type strings depend on `id`: - selectable: An element with selectable text (but not text inputs). -The type string can also be `'other'`, regardless of what `id` is. That is the -case for elements for markers added by the `` Hints mode command. - -The function must return an object like the `info` parameter (with the `type` -and `semantic` properties). +The function must return `null` or a string like the `type` parameter. ## Stability diff --git a/documentation/commands.md b/documentation/commands.md index 38dd0f9..ae94828 100644 --- a/documentation/commands.md +++ b/documentation/commands.md @@ -216,28 +216,13 @@ shorter the hints. (Also, what should happen if you tried to `F` a button?) (You can also customize [which elements do and don’t get hints][hint-matcher].) Another way to make hints shorter is to assign the same hint to all links with -the same URL. So don’t get surprised if you see the same hint repeated several +the same URL. So don’t be surprised if you see the same hint repeated several times. VimFx also tries to give you shorter hints for elements that you are more likely to click. This is done by the surprisingly simple rule: The larger the element, -the shorter the hint. - -There are standardized elements which are always clickable—_semantically_ -clickable elements. Unfortunately, many sites use unclickable elements and then -make them clickable using JavaScript—unsemantically clickable elements. -Such elements are difficult to find. VimFx has a few techniques for doing so, -which works many times but not always, but unfortunately they sometimes produce -false positives. Many times those false positives are pretty large elements, -which according to the last paragraph would give them really short hints, making -other more important elements suffer by getting longer ones. Therefore VimFx -favors semantic elements over unsemantic ones and takes that into account when -deciding the hint length for elements. - -Some hint characters are easier to type than others. The ones on the home row -are of course the best. When customizing the [hint chars] option you should put -the best keys to the left and the worst ones to the right. VimFx favors keys to -the left, so that should give you the optimal hints. +the shorter the hint. To learn more about hint characters and hint length, read +about the [hint chars] option. Hints are added on top of the corresponding element. If they obscure the display too much you can hold shift to make them transparent. (See [Styling] if you’d @@ -283,9 +268,11 @@ like `F`. Finally, if the element you wanted to interact with didn’t get a hint marker you can try pressing `` while the hints are still shown. That will give -hint markers to _every_ element on screen. Warning: This can be very slow, and -result in an overwhelming amount of hint markers. See this as an escape hatch if -you _really_ want to avoid using the mouse at all costs. +hint markers to all _other_ elements. Warning: This can be very slow, and result +in an overwhelming amount of hint markers (making it difficult to know which +hint to activate sometimes). See this as an escape hatch if you _really_ want to +avoid using the mouse at all costs. (Press `` again to toggle back to +the previous hints.) [hint-matcher]: api.md#vimfxhintmatcher [hint chars]: options.md#hint-chars diff --git a/documentation/options.md b/documentation/options.md index 371efb7..4f6ef89 100644 --- a/documentation/options.md +++ b/documentation/options.md @@ -26,9 +26,47 @@ These options are available in VimFx’s settings page in the Add-ons Manager ### Hint chars The characters used for the hints in Hints mode, which can be entered using one -of the many `f` commands. See also [The `f` commands]. +of the many [`f` commands] \(and a few `v` commands). -[The `f` commands]: commands.md#the-f-commands--hints-mode +Quick suggestion: Put more easily reachable keys longer to the left. Put two +pretty good (but not the best) keys at the end, after the space. + +Some hint characters are easier to type than others. Many people think that the +ones on the home row are the best. VimFx favors keys to the left. That’s why you +should put better keys longer to the left. + +The hint characters always contain a single space. This splits them into two +groups: _primary_ hint characters (before the space), and _secondary_ hint +characters (after the space). Read on to find out why. + +Some markable elements are quicker to find than others. Therefore, VimFx looks +for markable elements in two passes for some commands, such as the `f` command. +(This is why all hints don’t always appear on screen at the same time). If two +passes are used, hints from the _first_ pass can only begin with _primary_ hint +characters. In all other cases hints may start with _any_ hint character. + +When choosing how many secondary hint characters you want (there are two by +default), think about this: Usually most markable elements are found in the +first pass, while fewer are found in the second pass. So it makes sense to have +more primary hint characters than secondary. It’s a tradeoff. If you think the +hints from the first pass are too long, you probably need more primary hint +characters. On the other hand, if you think the hints from the _second_ pass are +too long, you might need a few extra secondary hint characters, but remember +that it might be at the expense of longer hints in the first pass. + +All of this also help you understand why hints may be slow on some pages: + +- One reason could be that most hints come from a second pass, which are slower + to compute (and are worse than first pass hints). + + If a site gets an unusual amount of second pass hints, it might be because the + site is badly coded accessibility-wise. If so, consider contacting the site + and telling them so, which improves their accessibility for everyone! + +- Another reason could be that a page has a _huge_ amount of links. If that + bothers you regularly, feel free to send a pull request with faster code! + +[`f` commands]: commands.md#the-f-commands--hints-mode ### “Previous”/“Next” link patterns diff --git a/extension/lib/commands-frame.coffee b/extension/lib/commands-frame.coffee index 7707bab..cd17a6d 100644 --- a/extension/lib/commands-frame.coffee +++ b/extension/lib/commands-frame.coffee @@ -22,7 +22,7 @@ # same name as the command in commands.coffee that calls it. There are also a # few more generalized “commands” used in more than one place. -hints = require('./hints') +markableElements = require('./markable-elements') prefs = require('./prefs') SelectionManager = require('./selection') translate = require('./l10n') @@ -41,6 +41,20 @@ CLICKABLE_ARIA_ROLES = [ 'menuitem', 'menuitemcheckbox', 'menuitemradio' ] +createComplementarySelectors = (selectors) -> + return [ + selectors.join(',') + selectors.map((selector) -> ":not(#{selector})").join('') + ] + +FOLLOW_DEFAULT_SELECTORS = createComplementarySelectors([ + 'a', 'button', 'input', 'textarea', 'select' + '[role]', '[contenteditable]' +]) + +FOLLOW_SELECTABLE_SELECTORS = + createComplementarySelectors(['div', 'span']).reverse() + commands = {} commands.go_up_path = ({vim, count = 1}) -> @@ -90,22 +104,25 @@ commands.scroll_to_mark = (args) -> element = vim.state.scrollableElements.filterSuitableDefault() viewportUtils.scroll(element, args) -helper_follow = ({id, combine = true}, matcher, args) -> - {vim, markEverything = false} = args - hrefs = {} - vim.state.markerElements = [] +helper_follow = (options, matcher, {vim, pass}) -> + {id, combine = true, selectors = FOLLOW_DEFAULT_SELECTORS} = options + if vim.content.document instanceof XULDocument + selectors = ['*'] - filter = (element, getElementShape) -> - {type, semantic} = matcher({vim, element, getElementShape}) + if pass == 'auto' + pass = if selectors.length == 2 then 'first' else 'single' - if markEverything and not type - type = 'other' - semantic = false + vim.state.markerElements = [] if pass in ['single', 'first'] + hrefs = {} + filter = (element, getElementShape) -> + type = matcher({vim, element, getElementShape}) if vim.hintMatcher - {type, semantic} = vim.hintMatcher(id, element, {type, semantic}) - + type = vim.hintMatcher(id, element, type) + if pass == 'complementary' + type = if type then null else 'complementary' return unless type + shape = getElementShape(element) # CodeMirror editor uses a tiny hidden textarea positioned at the caret. @@ -130,7 +147,7 @@ helper_follow = ({id, combine = true}, matcher, args) -> originalRect = element.getBoundingClientRect() length = vim.state.markerElements.push({element, originalRect}) - wrapper = {type, semantic, shape, elementIndex: length - 1} + wrapper = {type, shape, elementIndex: length - 1} if wrapper.type == 'link' {href} = element @@ -157,17 +174,23 @@ helper_follow = ({id, combine = true}, matcher, args) -> return wrapper + selector = + if pass == 'complementary' + '*' + else + selectors[if pass == 'second' then 1 else 0] return { + wrappers: markableElements.find(vim.content, filter, selector) viewport: viewportUtils.getWindowViewport(vim.content) - wrappers: hints.getMarkableElements(vim.content, filter) + pass } -commands.follow = helper_follow.bind(null, {id: 'normal'}, +commands.follow = helper_follow.bind( + null, {id: 'normal'}, ({vim, element, getElementShape}) -> document = element.ownerDocument isXUL = (document instanceof XULDocument) type = null - semantic = true switch # Bootstrap. Match these before regular links, because especially slider # “buttons” often get the same hint otherwise. @@ -175,14 +198,13 @@ commands.follow = helper_follow.bind(null, {id: 'normal'}, element.hasAttribute?('data-dismiss') or element.hasAttribute?('data-slide') or element.hasAttribute?('data-slide-to') - # Some elements may not be semantic, but _should be_ and still deserve a - # good hint. type = 'clickable' when isProperLink(element) type = 'link' when isTypingElement(element) type = 'text' - when element.getAttribute?('role') in CLICKABLE_ARIA_ROLES or + when element.localName in ['a', 'button'] or + element.getAttribute?('role') in CLICKABLE_ARIA_ROLES or # element.hasAttribute?('aria-controls') or element.hasAttribute?('aria-pressed') or @@ -195,8 +217,6 @@ commands.follow = helper_follow.bind(null, {id: 'normal'}, # real hint that allows you to focus the document to start typing. element.id != 'docs-editor' type = 'clickable' - unless isXUL or element.localName in ['a', 'input', 'button'] - semantic = false when element != vim.state.scrollableElements.largest and vim.state.scrollableElements.has(element) type = 'scrollable' @@ -213,7 +233,6 @@ commands.follow = helper_follow.bind(null, {id: 'normal'}, # Google Drive Document. element.classList?.contains('kix-appview-editor') type = 'clickable' - semantic = false # Facebook comment fields. when element.parentElement?.classList?.contains('UFIInputContainer') type = 'clickable-special' @@ -245,7 +264,6 @@ commands.follow = helper_follow.bind(null, {id: 'normal'}, # a large element with a click-tracking event listener. unless element.querySelector?('a, button, input, [class*=button]') type = 'clickable' - semantic = false # When viewing an image it should get a marker to toggle zoom. This is the # most unlikely rule to match, so keep it last. when document.body?.childElementCount == 1 and @@ -254,16 +272,18 @@ commands.follow = helper_follow.bind(null, {id: 'normal'}, element.classList?.contains('shrinkToFit')) type = 'clickable' type = null if isXUL and element.classList?.contains('textbox-input') - return {type, semantic} + return type ) -commands.follow_in_tab = helper_follow.bind(null, {id: 'tab'}, +commands.follow_in_tab = helper_follow.bind( + null, {id: 'tab', selectors: ['a']}, ({element}) -> type = if isProperLink(element) then 'link' else null - return {type, semantic: true} + return type ) -commands.follow_copy = helper_follow.bind(null, {id: 'copy'}, +commands.follow_copy = helper_follow.bind( + null, {id: 'copy'}, ({element}) -> type = switch when isProperLink(element) @@ -274,10 +294,11 @@ commands.follow_copy = helper_follow.bind(null, {id: 'copy'}, 'text' else null - return {type, semantic: true} + return type ) -commands.follow_focus = helper_follow.bind(null, {id: 'focus', combine: false}, +commands.follow_focus = helper_follow.bind( + null, {id: 'focus', combine: false}, ({vim, element}) -> type = switch when element.tabIndex > -1 @@ -287,10 +308,11 @@ commands.follow_focus = helper_follow.bind(null, {id: 'focus', combine: false}, 'scrollable' else null - return {type, semantic: true} + return type ) -commands.follow_selectable = helper_follow.bind(null, {id: 'selectable'}, +commands.follow_selectable = helper_follow.bind( + null, {id: 'selectable', selectors: FOLLOW_SELECTABLE_SELECTORS}, ({element}) -> isRelevantTextNode = (node) -> # Ignore whitespace-only text nodes, and single-letter ones (which are @@ -301,7 +323,7 @@ commands.follow_selectable = helper_follow.bind(null, {id: 'selectable'}, 'selectable' else null - return {type, semantic: true} + return type ) commands.focus_marker_element = ({vim, elementIndex, options}) -> @@ -312,8 +334,9 @@ commands.focus_marker_element = ({vim, elementIndex, options}) -> vim.clearHover() vim.setHover(element) -commands.click_marker_element = (args) -> - {vim, elementIndex, type, preventTargetBlank} = args +commands.click_marker_element = ( + {vim, elementIndex, type, preventTargetBlank} +) -> {element} = vim.state.markerElements[elementIndex] if element.target == '_blank' and preventTargetBlank targetReset = element.target diff --git a/extension/lib/commands.coffee b/extension/lib/commands.coffee index 1de0a2c..2d5ed8a 100644 --- a/extension/lib/commands.coffee +++ b/extension/lib/commands.coffee @@ -28,7 +28,8 @@ config = require('./config') help = require('./help') -hints = require('./hints') +MarkerContainer = require('./marker-container') +markableElements = require('./markable-elements') prefs = require('./prefs') translate = require('./l10n') utils = require('./utils') @@ -428,41 +429,52 @@ commands.tab_close_other = ({vim}) -> helper_follow = (name, vim, callback, count = null) -> + {window} = vim vim.markPageInteraction() - help.removeHelp(vim.window) - - # Enter hints mode immediately, with an empty set of markers. The user might - # press keys before the `vim._run` callback is invoked. Those key presses - # should be handled in hints mode, not normal mode. - initialMarkers = [] - storage = vim.enterMode( - 'hints', initialMarkers, callback, count, vim.options.hints_sleep + help.removeHelp(window) + + markerContainer = new MarkerContainer({ + window + hintChars: vim.options.hint_chars + getComplementaryWrappers: (callback) -> + vim._run(name, {pass: 'complementary'}, ({wrappers, viewport}) -> + # `markerContainer.container` is `null`ed out when leaving Hints mode. + # If this callback is called after we’ve left Hints mode (and perhaps + # even entered it again), simply discard the result. + return unless markerContainer.container + if wrappers.length == 0 + vim.notify(translate('notification.follow.none')) + callback({wrappers, viewport}) + ) + }) + MarkerContainer.remove(window) # Better safe than sorry. + window.gBrowser.selectedBrowser.parentNode.appendChild( + markerContainer.container ) - setMarkers = ({wrappers, viewport}) -> - if wrappers.length > 0 - {markers, markerMap} = hints.injectHints( - vim.window, wrappers, viewport, vim.options - ) - storage.markers = markers - storage.markerMap = markerMap + # Enter Hints mode immediately, with an empty set of markers. The user might + # press keys before any hints have been generated. Those key presses should be + # handled in Hints mode, not Normal mode. + vim.enterMode('hints', { + markerContainer, callback, count + sleep: vim.options.hints_sleep + }) + + injectHints = ({wrappers, viewport, pass}) -> + # See `getComplementaryWrappers` above. + return unless markerContainer.container + + if wrappers.length == 0 + if pass in ['single', 'second'] and markerContainer.markers.length == 0 + vim.notify(translate('notification.follow.none')) + vim.enterMode('normal') else - vim.notify(translate('notification.follow.none')) - vim.enterMode('normal') - - vim._run(name, {}, (result) -> - # The user might have exited hints mode (and perhaps even entered it again) - # before this callback is invoked. If so, `storage.markers` has been - # cleared, or set to a new value. Only proceed if it is unchanged. - return unless storage.markers == initialMarkers - setMarkers(result) - storage.markEverything = -> - lastMarkers = storage.markers - vim._run(name, {markEverything: true}, (newResult) -> - return unless storage.markers == lastMarkers - setMarkers(newResult) - ) - ) + markerContainer.injectHints(wrappers, viewport, pass) + + if pass == 'first' + vim._run(name, {pass: 'second'}, injectHints) + + vim._run(name, {pass: 'auto'}, injectHints) helper_follow_clickable = (options, {vim, count = 1}) -> callback = (marker, timesLeft, keyStr) -> @@ -529,6 +541,7 @@ commands.follow_in_window = ({vim}) -> vim._focusMarkerElement(marker.wrapper.elementIndex) {href} = marker.wrapper vim.window.openLinkIn(href, 'window', {}) if href + return false helper_follow('follow_in_tab', vim, callback) commands.follow_multiple = (args) -> @@ -542,22 +555,23 @@ commands.follow_copy = ({vim}) -> 'href' when 'text' 'value' - when 'contenteditable', 'other' + when 'contenteditable', 'complementary' '_selection' helper_copy_marker_element(vim, marker.wrapper.elementIndex, property) + return false helper_follow('follow_copy', vim, callback) commands.follow_focus = ({vim}) -> callback = (marker) -> vim._focusMarkerElement(marker.wrapper.elementIndex, {select: true}) + return false helper_follow('follow_focus', vim, callback) commands.click_browser_element = ({vim}) -> + {window} = vim markerElements = [] - filter = ({markEverything}, element, getElementShape) -> - document = element.ownerDocument - semantic = true + filter = ({complementary}, element, getElementShape) -> type = switch when vim._state.scrollableElements.has(element) 'scrollable' @@ -565,21 +579,20 @@ commands.click_browser_element = ({vim}) -> (element.onclick and element.localName != 'statuspanel') 'clickable' - if markEverything and not type - type = 'other' - semantic = false + if complementary + type = if type then null else 'complementary' return unless type return unless shape = getElementShape(element) length = markerElements.push(element) - return {type, semantic, shape, elementIndex: length - 1} + return {type, shape, elementIndex: length - 1} callback = (marker) -> element = markerElements[marker.wrapper.elementIndex] switch marker.wrapper.type when 'scrollable' utils.focusElement(element, {flag: 'FLAG_BYKEY'}) - when 'clickable', 'other' + when 'clickable', 'complementary' sequence = if element.localName == 'tab' ['mousedown'] @@ -587,26 +600,34 @@ commands.click_browser_element = ({vim}) -> 'click-xul' utils.focusElement(element) utils.simulateMouseEvents(element, sequence) + return false - createMarkers = (wrappers) -> - viewport = viewportUtils.getWindowViewport(vim.window) - {markers} = hints.injectHints(vim.window, wrappers, viewport, { - hint_chars: vim.options.hint_chars - ui: true - }) - return markers - - wrappers = hints.getMarkableElements( - vim.window, filter.bind(null, {markEverything: false}) + wrappers = markableElements.find( + window, filter.bind(null, {complementary: false}) ) if wrappers.length > 0 - storage = vim.enterMode('hints', createMarkers(wrappers), callback) - storage.markEverything = -> - newWrappers = hints.getMarkableElements( - vim.window, filter.bind(null, {markEverything: true}) - ) - storage.markers = createMarkers(newWrappers) + viewport = viewportUtils.getWindowViewport(window) + + markerContainer = new MarkerContainer({ + window + hintChars: vim.options.hint_chars + adjustZoom: false + getComplementaryWrappers: (callback) -> + newWrappers = markableElements.find( + window, filter.bind(null, {complementary: true}) + ) + callback({wrappers: newWrappers, viewport}) + }) + MarkerContainer.remove(window) # Better safe than sorry. + markerContainer.container.classList.add('ui') + window.document.getElementById('browser-panel').appendChild( + markerContainer.container + ) + + markerContainer.injectHints(wrappers, viewport, 'single') + vim.enterMode('hints', {markerContainer, callback}) + else vim.notify(translate('notification.follow.none')) @@ -634,6 +655,7 @@ helper_follow_selectable = ({select}, {vim}) -> scroll: select }) vim.enterMode('caret', select) + return false helper_follow('follow_selectable', vim, callback) commands.element_text_caret = @@ -645,6 +667,7 @@ commands.element_text_select = commands.element_text_copy = ({vim}) -> callback = (marker) -> helper_copy_marker_element(vim, marker.wrapper.elementIndex, '_selection') + return false helper_follow('follow_selectable', vim, callback) helper_copy_marker_element = (vim, elementIndex, property) -> @@ -758,7 +781,7 @@ commands.esc = ({vim}) -> vim._run('esc') utils.blurActiveBrowserElement(vim) vim.window.gBrowser.getFindBar().close() - hints.removeHints(vim.window) # Better safe than sorry. + MarkerContainer.remove(vim.window) # Better safe than sorry. # Calling `.hide()` when the toolbar is not open can destroy it for the rest # of the Firefox session. The code here is taken from the `.toggle()` method. diff --git a/extension/lib/defaults.coffee b/extension/lib/defaults.coffee index 7eb496b..cd5c8df 100644 --- a/extension/lib/defaults.coffee +++ b/extension/lib/defaults.coffee @@ -136,7 +136,7 @@ shortcuts = '': 'rotate_markers_backward' '': 'delete_hint_char' '': 'increase_count' - '': 'mark_everything' + '': 'toggle_complementary' 'ignore': '': @@ -148,7 +148,7 @@ shortcuts = ' ': 'exit' options = - 'hint_chars': 'fjdkslaghrueiwovncm' + 'hint_chars': 'fjdkslaghrueiwonc mv' 'prev_patterns': 'prev previous ‹ « ◀ ← << < back newer' 'next_patterns': 'next › » ▶ → >> > more older' 'blacklist': '*example.com* http://example.org/editor/*' diff --git a/extension/lib/help.coffee b/extension/lib/help.coffee index c31396e..d59bc07 100644 --- a/extension/lib/help.coffee +++ b/extension/lib/help.coffee @@ -28,8 +28,6 @@ SEARCH_MATCH_CLASS = 'search-match' SEARCH_NON_MATCH_CLASS = 'search-non-match' SEARCH_HIGHLIGHT_CLASS = 'search-highlight' -shutdownHandlerAdded = false - injectHelp = (window, vimfx) -> removeHelp(window) @@ -70,10 +68,6 @@ injectHelp = (window, vimfx) -> # Uncomment this line if you want to use `gulp help.html`! # utils.writeToClipboard(container.outerHTML) - unless shutdownHandlerAdded - module.onShutdown(removeHelp.bind(null, window)) - shutdownHandlerAdded = true - removeHelp = (window) -> getHelp(window)?.remove() getHelp = (window) -> window.document.getElementById(CONTAINER_ID) diff --git a/extension/lib/main.coffee b/extension/lib/main.coffee index 5744c40..e778e06 100644 --- a/extension/lib/main.coffee +++ b/extension/lib/main.coffee @@ -25,7 +25,9 @@ button = require('./button') config = require('./config') defaults = require('./defaults') UIEventManager = require('./events') +help = require('./help') {applyMigrations} = require('./legacy') +MarkerContainer = require('./marker-container') messageManager = require('./message-manager') migrations = require('./migrations') modes = require('./modes') @@ -119,6 +121,10 @@ module.exports = (data, reason) -> eventManager.addListeners(vimfx, window) setWindowAttribute(window, 'mode', 'normal') setWindowAttribute(window, 'focus-type', 'none') + module.onShutdown(-> + MarkerContainer.remove(window) + help.removeHelp(window) + ) callback(true) ) diff --git a/extension/lib/hints.coffee b/extension/lib/markable-elements.coffee similarity index 68% rename from extension/lib/hints.coffee rename to extension/lib/markable-elements.coffee index e678538..80c84a1 100644 --- a/extension/lib/hints.coffee +++ b/extension/lib/markable-elements.coffee @@ -18,135 +18,20 @@ # along with VimFx. If not, see . ### -# This file contains functions for getting markable elements, and related data, -# as well as for creating and inserting markers for markable elements. +# This file contains functions for getting markable elements and related data. -huffman = require('n-ary-huffman') -{Marker} = require('./marker') utils = require('./utils') viewportUtils = require('./viewport') {devtools} = Cu.import('resource://devtools/shared/Loader.jsm', {}) -CONTAINER_ID = 'VimFxMarkersContainer' - Element = Ci.nsIDOMElement XULDocument = Ci.nsIDOMXULDocument -shutdownHandlerAdded = false - -# For some time we used to return the hints container from `injectHints`, and -# use that reference to remove the hints when needed. That’s fine in theory, but -# in case anything breaks we might loose that reference and end up with -# unremovable hints on the screen. Explicitly looking for an element with the -# container ID is more fail-safe. -removeHints = (window) -> - window.document.getElementById(CONTAINER_ID)?.remove() - -# Create `Marker`s for every element (represented by a regular object of data -# about the element—a “wrapper,” a stand-in for the real element, which is only -# accessible in frame scripts) in `wrappers`, and insert them into `window`. -injectHints = (window, wrappers, viewport, options) -> - semantic = [] - unsemantic = [] - combined = [] - markerMap = {} - - for wrapper in wrappers - marker = new Marker(wrapper, window.document) - group = switch - when wrapper.parentIndex? - combined - when wrapper.semantic - semantic - else - unsemantic - group.push(marker) - markerMap[wrapper.elementIndex] = marker - - markers = semantic.concat(unsemantic) - - # Each marker gets a unique `z-index`, so that it can be determined if a - # marker overlaps another. Put more important markers (higher weight) at the - # end, so that they get higher `z-index`, in order not to be overlapped. - zIndex = 0 - setZIndexes = (markers) -> - markers.sort((a, b) -> a.weight - b.weight) - for marker in markers when marker not instanceof huffman.BranchPoint - marker.markerElement.style.zIndex = zIndex - zIndex += 1 - # Add `z-index` space for all the children of the marker. - zIndex += marker.wrapper.numChildren if marker.wrapper.numChildren? - return - - # The `markers` passed to this function have been sorted by `setZIndexes` in - # advance, so we can skip sorting in the `huffman.createTree` function. - hintChars = options.hint_chars - createHuffmanTree = (markers) -> - return huffman.createTree(markers, hintChars.length, {sorted: true}) - - # Semantic elements should always get better hints and higher `z-index`:es - # than unsemantic ones, even if they are smaller. The former is achieved by - # putting the unsemantic elements in their own branch of the huffman tree. - if unsemantic.length > 0 - if markers.length > hintChars.length - setZIndexes(unsemantic) - subTree = createHuffmanTree(unsemantic) - semantic.push(subTree) - else - semantic.push(unsemantic...) - - setZIndexes(semantic) - - tree = createHuffmanTree(semantic) - tree.assignCodeWords(hintChars, (marker, hint) -> marker.setHint(hint)) - - # Markers for links with the same href can be combined to use the same hint. - # They should all have the same `z-index` (because they all have the same - # combined weight), but in case any of them cover another they still get a - # unique `z-index` (space for this was added in `setZIndexes`). - for marker in combined - parent = markerMap[marker.wrapper.parentIndex] - parentZIndex = Number(parent.markerElement.style.zIndex) - marker.markerElement.style.zIndex = parentZIndex - parent.markerElement.style.zIndex = parentZIndex + 1 - marker.setHint(parent.hint) - markers.push(combined...) - - removeHints(window) # Better safe than sorry. - container = window.document.createElement('box') - container.id = CONTAINER_ID - - zoom = 1 - - if options.ui - container.classList.add('ui') - window.document.getElementById('browser-panel').appendChild(container) - else - {ZoomManager, gBrowser: {selectedBrowser: browser}} = window - browser.parentNode.appendChild(container) - # If “full zoom” is not used, it means that “Zoom text only” is enabled. - # If so, that “zoom” does not need to be taken into account. - # `.getCurrentMode()` is added by the “Default FullZoom Level” extension. - if ZoomManager.getCurrentMode?(browser) ? ZoomManager.useFullZoom - zoom = ZoomManager.getZoomForBrowser(browser) - - for marker in markers - container.appendChild(marker.markerElement) - # Must be done after the hints have been inserted into the DOM (see - # marker.coffee). - marker.setPosition(viewport, zoom) - - unless shutdownHandlerAdded - module.onShutdown(removeHints.bind(null, window)) - shutdownHandlerAdded = true - - return {markers, markerMap} - -getMarkableElements = (window, filter) -> +find = (window, filter, selector = '*') -> viewport = viewportUtils.getWindowViewport(window) wrappers = [] - _getMarkableElements(window, viewport, wrappers, filter) + _getMarkableElements(window, viewport, wrappers, filter, selector) return wrappers # `filter` is a function that is given every element in every frame of the page. @@ -155,11 +40,16 @@ getMarkableElements = (window, filter) -> # is modified instead of using return values to avoid array concatenation for # each frame. It might sound expensive to go through _every_ element, but that’s # actually what other methods like using XPath or CSS selectors would need to do -# anyway behind the scenes. -_getMarkableElements = (window, viewport, wrappers, filter, parents = []) -> +# anyway behind the scenes. However, it is possible to pass in a CSS selector, +# which allows getting markable elements in several passes with different sets +# of candidates. +_getMarkableElements = ( + window, viewport, wrappers, filter, selector, parents = [] +) -> {document} = window - for element in getAllElements(document) when element instanceof Element + for element in getAllElements(document, selector) + continue unless element instanceof Element # `getRects` is fast and filters out most elements, so run it first of all. rects = getRects(element, viewport) continue unless rects.length > 0 @@ -178,21 +68,24 @@ _getMarkableElements = (window, viewport, wrappers, filter, parents = []) -> ) {viewport: frameViewport, offset} = result _getMarkableElements( - frame, frameViewport, wrappers, filter, parents.concat({window, offset}) + frame, frameViewport, wrappers, filter, selector, + parents.concat({window, offset}) ) return -getAllElements = (document) -> +getAllElements = (document, selector) -> unless document instanceof XULDocument - return document.getElementsByTagName('*') + return document.querySelectorAll(selector) # Use a `Set` since this algorithm may find the same element more than once. # Ideally we should find a way to find all elements without duplicates. elements = new Set() getAllRegular = (element) -> # The first time `zF` is run `.getElementsByTagName('*')` may oddly include - # `undefined` in its result! Filter those out. + # `undefined` in its result! Filter those out. (Also, `selector` is ignored + # here since it doesn’t make sense in XUL documents because of all the + # trickery around anonymous elements.) for child in element.getElementsByTagName('*') when child elements.add(child) getAllAnonymous(child) @@ -385,7 +278,5 @@ contains = (element, elementAtPoint) -> return container == elementAtPoint or container.contains(elementAtPoint) module.exports = { - removeHints - injectHints - getMarkableElements + find } diff --git a/extension/lib/marker-container.coffee b/extension/lib/marker-container.coffee new file mode 100644 index 0000000..3f0aac0 --- /dev/null +++ b/extension/lib/marker-container.coffee @@ -0,0 +1,282 @@ +### +# Copyright Simon Lydell 2013, 2014, 2015, 2016. +# +# This file is part of VimFx. +# +# VimFx is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# VimFx is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with VimFx. If not, see . +### + +# This file manages a collection of hint markers. This involves creating them, +# assigning hints to them and matching them against pressed keys. + +huffman = require('n-ary-huffman') +Marker = require('./marker') + +CONTAINER_ID = 'VimFxMarkersContainer' + +# `z-index` can be infinite in theory, but not in practice. This is the largest +# value Firefox handles. +MAX_Z_INDEX = 2147483647 + +class MarkerContainer + constructor: (options) -> + { + @window + @getComplementaryWrappers + hintChars + @adjustZoom = true + } = options + + [@primaryHintChars, @secondaryHintChars] = hintChars.split(' ') + @alphabet = @primaryHintChars + @secondaryHintChars + @numEnteredChars = 0 + + @isComplementary = false + @hasLookedForComplementaryWrappers = false + + @markers = [] + @markerMap = {} + + @container = @window.document.createElement('box') + @container.id = CONTAINER_ID + + # This static method looks for an element with the container ID and removes + # it. This is more fail-safe than `@container?.remove()`, because we might + # loose the reference to the container. Then we’d end up with unremovable + # hints on the screen (which has happened in the past). + @remove: (window) -> + window.document.getElementById(CONTAINER_ID)?.remove() + + remove: -> + MarkerContainer.remove(@window) + @container = null + + reset: -> + @numEnteredChars = 0 + marker.reset() for marker in @markers when marker.hintIndex > 0 + @refreshComplementaryVisiblity() + + refreshComplementaryVisiblity: -> + for marker in @markers + marker.setVisibility(marker.isComplementary == @isComplementary) + return + + # Create `Marker`s for every element (represented by a regular object of data + # about the element—a “wrapper,” a stand-in for the real element, which is + # only accessible in frame scripts) in `wrappers`, and insert them into + # `@window`. + injectHints: (wrappers, viewport, pass) -> + isComplementary = (pass == 'complementary') + combined = [] + markers = [] + markerMap = {} + + for wrapper in wrappers + marker = new Marker(wrapper, @window.document, {isComplementary}) + if wrapper.parentIndex? + combined.push(marker) + else + markers.push(marker) + markerMap[wrapper.elementIndex] = marker + + # Both the `z-index` assignment and the Huffman algorithm below require the + # markers to be sorted. + markers.sort((a, b) -> a.weight - b.weight) + + # Each marker gets a unique `z-index`, so that it can be determined if a + # marker overlaps another. More important markers (higher weight) should + # have higher `z-index`, in order not to start out overlapped. Existing + # markers should also have higher `z-index` than newer markers, which is why + # we start out large and not at zero. + zIndex = + MAX_Z_INDEX - markers.length - combined.length - @markers.length + 1 + for marker in markers + marker.markerElement.style.zIndex = zIndex + zIndex += 1 + # Add `z-index` space for all the children of the marker. + zIndex += marker.wrapper.numChildren if marker.wrapper.numChildren? + + prefixes = switch pass + when 'first' + @primaryHintChars + when 'second' + @primaryHintChars[@markers.length..] + @secondaryHintChars + else + @alphabet + diff = @alphabet.length - prefixes.length + paddedMarkers = + if diff > 0 + # Dummy nodes with infinite weight are be guaranteed to be first-level + # children of the Huffman tree. When there are less prefixes than + # characters in the alphabet, adding a few such dummy nodes makes sure + # that there is one child per prefix in the first level (discarding the + # dummy children). + markers.concat(Array(diff).fill({weight: Infinity})) + else + # Otherwise, nothing needs to be done. Simply use as many prefixes as + # needed (and ignore any remaining ones). + markers + + tree = huffman.createTree(paddedMarkers, @alphabet.length, {sorted: true}) + + setHint = (marker, hint) -> marker.setHint(hint) + index = 0 + for node in tree.children by -1 when node.weight != Infinity + prefix = prefixes[index] + if node instanceof huffman.BranchPoint + node.assignCodeWords(@alphabet, setHint, prefix) + else + setHint(node, prefix) + index += 1 + + # Markers for links with the same href can be combined to use the same hint. + # They should all have the same `z-index` (because they all have the same + # combined weight), but in case any of them cover another they still get a + # unique `z-index` (space for this was added above). + for marker in combined + parent = markerMap[marker.wrapper.parentIndex] + parentZIndex = Number(parent.markerElement.style.zIndex) + marker.markerElement.style.zIndex = parentZIndex + parent.markerElement.style.zIndex = parentZIndex + 1 + marker.setHint(parent.hint) + markers.push(combined...) + + zoom = 1 + if @adjustZoom + {ZoomManager, gBrowser: {selectedBrowser: browser}} = @window + # If “full zoom” is not used, it means that “Zoom text only” is enabled. + # If so, that “zoom” does not need to be taken into account. + # `.getCurrentMode()` is added by the “Default FullZoom Level” extension. + if ZoomManager.getCurrentMode?(browser) ? ZoomManager.useFullZoom + zoom = ZoomManager.getZoomForBrowser(browser) + + fragment = @window.document.createDocumentFragment() + fragment.appendChild(marker.markerElement) for marker in markers + @container.appendChild(fragment) + + # Must be done after the hints have been inserted into the DOM (see + # `Marker::setPosition`). + marker.setPosition(viewport, zoom) for marker in markers + + @markers.push(markers...) + Object.assign(@markerMap, markerMap) + + toggleComplementary: -> + if not @isComplementary and not @hasLookedForComplementaryWrappers + @isComplementary = true + @hasLookedForComplementaryWrappers = true + @getComplementaryWrappers(({wrappers, viewport}) => + if wrappers.length > 0 + @injectHints(wrappers, viewport, 'complementary') + if @isComplementary + @reset() + else + @refreshComplementaryVisiblity() + else + @isComplementary = false + @hasLookedForComplementaryWrappers = false + ) + else + @isComplementary = not @isComplementary + @reset() + + matchHintChar: (char) -> + matchedMarkers = [] + + for marker in @markers + if marker.isComplementary == @isComplementary and + marker.hintIndex == @numEnteredChars + matched = marker.matchHintChar(char) + marker.hide() unless matched + if marker.isMatched() + marker.markMatched(true) + matchedMarkers.push(marker) + + @numEnteredChars += 1 + return matchedMarkers + + deleteHintChar: -> + for marker in @markers + switch marker.hintIndex - @numEnteredChars + when 0 + marker.deleteHintChar() + when -1 + marker.show() + @numEnteredChars -= 1 unless @numEnteredChars == 0 + + + rotateOverlapping: (forward) -> + rotateOverlappingMarkers(@markers, forward) + +# Finds all stacks of markers that overlap each other (by using `getStackFor`) +# (#1), and rotates their `z-index`:es (#2), thus alternating which markers are +# visible. +rotateOverlappingMarkers = (originalMarkers, forward) -> + # Shallow working copy. This is necessary since `markers` will be mutated and + # eventually empty. + markers = originalMarkers[..] + + # (#1) + stacks = (getStackFor(markers.pop(), markers) while markers.length > 0) + + # (#2) + # Stacks of length 1 don't participate in any overlapping, and can therefore + # be skipped. + for stack in stacks when stack.length > 1 + # This sort is not required, but makes the rotation more predictable. + stack.sort((a, b) -> + return a.markerElement.style.zIndex - b.markerElement.style.zIndex + ) + + zIndices = (marker.markerElement.style.zIndex for marker in stack) + # Shift the `z-index`:es one item forward or back. The higher the `z-index`, + # the more important the element. `forward` should give the next-most + # important element the best `z-index` and so on. + if forward + zIndices.push(zIndices.shift()) + else + zIndices.unshift(zIndices.pop()) + + for marker, index in stack + marker.markerElement.style.zIndex = zIndices[index] + + return + +# Get an array containing `marker` and all markers that overlap `marker`, if +# any, which is called a "stack". All markers in the returned stack are spliced +# out from `markers`, thus mutating it. +getStackFor = (marker, markers) -> + stack = [marker] + + {top, bottom, left, right} = marker.position + + index = 0 + while index < markers.length + nextMarker = markers[index] + + next = nextMarker.position + overlapsVertically = (next.bottom >= top and next.top <= bottom) + overlapsHorizontally = (next.right >= left and next.left <= right) + + if overlapsVertically and overlapsHorizontally + # Also get all markers overlapping this one. + markers.splice(index, 1) + stack = stack.concat(getStackFor(nextMarker, markers)) + else + # Continue the search. + index += 1 + + return stack + +module.exports = MarkerContainer diff --git a/extension/lib/marker.coffee b/extension/lib/marker.coffee index 89cfbc4..6b51110 100644 --- a/extension/lib/marker.coffee +++ b/extension/lib/marker.coffee @@ -25,8 +25,8 @@ utils = require('./utils') class Marker # `@wrapper` is a stand-in for the element that the marker represents. See - # `injectHints` in hints.coffee for more information. - constructor: (@wrapper, @document) -> + # `MarkerContainer::injectHints` for more information. + constructor: (@wrapper, @document, {@isComplementary}) -> @elementShape = @wrapper.shape @markerElement = utils.createBox(@document, 'marker') @markerElement.setAttribute('data-type', @wrapper.type) @@ -123,66 +123,4 @@ class Marker markMatched: (matched) -> @markerElement.classList.toggle('marker--matched', matched) -# Finds all stacks of markers that overlap each other (by using `getStackFor`) -# (#1), and rotates their `z-index`:es (#2), thus alternating which markers are -# visible. -rotateOverlappingMarkers = (originalMarkers, forward) -> - # Shallow working copy. This is necessary since `markers` will be mutated and - # eventually empty. - markers = originalMarkers[..] - - # (#1) - stacks = (getStackFor(markers.pop(), markers) while markers.length > 0) - - # (#2) - # Stacks of length 1 don't participate in any overlapping, and can therefore - # be skipped. - for stack in stacks when stack.length > 1 - # This sort is not required, but makes the rotation more predictable. - stack.sort((a, b) -> - return a.markerElement.style.zIndex - b.markerElement.style.zIndex - ) - - # Array of z-indices. - indexStack = (marker.markerElement.style.zIndex for marker in stack) - # Shift the array of indices one item forward or back. - if forward - indexStack.unshift(indexStack.pop()) - else - indexStack.push(indexStack.shift()) - - for marker, index in stack - marker.markerElement.style.zIndex = indexStack[index] - - return - -# Get an array containing `marker` and all markers that overlap `marker`, if -# any, which is called a "stack". All markers in the returned stack are spliced -# out from `markers`, thus mutating it. -getStackFor = (marker, markers) -> - stack = [marker] - - {top, bottom, left, right} = marker.position - - index = 0 - while index < markers.length - nextMarker = markers[index] - - next = nextMarker.position - overlapsVertically = (next.bottom >= top and next.top <= bottom) - overlapsHorizontally = (next.right >= left and next.left <= right) - - if overlapsVertically and overlapsHorizontally - # Also get all markers overlapping this one. - markers.splice(index, 1) - stack = stack.concat(getStackFor(nextMarker, markers)) - else - # Continue the search. - index += 1 - - return stack - -module.exports = { - Marker - rotateOverlappingMarkers -} +module.exports = Marker diff --git a/extension/lib/modes.coffee b/extension/lib/modes.coffee index 5aa9b77..e4b9bb5 100644 --- a/extension/lib/modes.coffee +++ b/extension/lib/modes.coffee @@ -25,9 +25,7 @@ {commands, findStorage} = require('./commands') defaults = require('./defaults') help = require('./help') -hints = require('./hints') translate = require('./l10n') -{rotateOverlappingMarkers} = require('./marker') prefs = require('./prefs') SelectionManager = require('./selection') utils = require('./utils') @@ -195,35 +193,28 @@ mode('caret', { mode('hints', { - onEnter: ({vim, storage}, markers, callback, count = 1, sleep = -1) -> - storage.markers = markers - storage.markerMap = null + onEnter: ({vim, storage}, options) -> + {markerContainer, callback, count = 1, sleep = -1} = options + storage.markerContainer = markerContainer storage.callback = callback storage.count = count - storage.numEnteredChars = 0 - storage.markEverything = null if sleep >= 0 storage.clearInterval = utils.interval(vim.window, sleep, (next) -> - unless storage.markerMap + if markerContainer.markers.length == 0 next() return vim._send('getMarkableElementsMovements', null, (diffs) -> for {dx, dy}, index in diffs when not (dx == 0 and dy == 0) - storage.markerMap[index].updatePosition(dx, dy) + markerContainer.markerMap[index].updatePosition(dx, dy) next() ) ) - # Expose the storage so asynchronously computed markers can be set - # retroactively. - return storage - onLeave: ({vim, storage}) -> - # When clicking VimFx’s disable button in the Add-ons Manager, `hints` will - # have been `null`ed out when the timeout has passed. + {markerContainer} = storage vim.window.setTimeout( - (-> hints?.removeHints(vim.window)), + (-> markerContainer.remove()), vim.options.hints_timeout ) storage.clearInterval?() @@ -233,20 +224,12 @@ mode('hints', { onInput: (args, match) -> {vim, storage} = args - {markers, callback} = storage + {markerContainer, callback} = storage if match.type == 'full' match.command.run(args) - else if match.unmodifiedKey in vim.options.hint_chars and markers.length > 0 - matchedMarkers = [] - - for marker in markers when marker.hintIndex == storage.numEnteredChars - matched = marker.matchHintChar(match.unmodifiedKey) - marker.hide() unless matched - if marker.isMatched() - marker.markMatched(true) - matchedMarkers.push(marker) - + else if match.unmodifiedKey in vim.options.hint_chars + matchedMarkers = markerContainer.matchHintChar(match.unmodifiedKey) if matchedMarkers.length > 0 again = callback(matchedMarkers[0], storage.count, match.keyStr) storage.count -= 1 @@ -255,14 +238,11 @@ mode('hints', { marker.markMatched(false) for marker in matchedMarkers return ), vim.options.hints_timeout) - marker.reset() for marker in markers - storage.numEnteredChars = 0 + markerContainer.reset() else # The callback might have entered another mode. Only go back to Normal # mode if we’re still in Hints mode. vim.enterMode('normal') if vim.mode == 'hints' - else - storage.numEnteredChars += 1 return true @@ -270,28 +250,23 @@ mode('hints', { exit: ({vim, storage}) -> # The hints are removed automatically when leaving the mode, but after a # timeout. When aborting the mode we should remove the hints immediately. - hints.removeHints(vim.window) + storage.markerContainer.remove() vim.enterMode('normal') rotate_markers_forward: ({storage}) -> - rotateOverlappingMarkers(storage.markers, true) + storage.markerContainer.rotateOverlapping(true) rotate_markers_backward: ({storage}) -> - rotateOverlappingMarkers(storage.markers, false) + storage.markerContainer.rotateOverlapping(false) delete_hint_char: ({storage}) -> - for marker in storage.markers - switch marker.hintIndex - storage.numEnteredChars - when 0 - marker.deleteHintChar() - when -1 - marker.show() - storage.numEnteredChars -= 1 unless storage.numEnteredChars == 0 - - increase_count: ({storage}) -> storage.count += 1 - - mark_everything: ({storage}) -> - storage.markEverything?() + storage.markerContainer.deleteHintChar() + + increase_count: ({storage}) -> + storage.count += 1 + + toggle_complementary: ({storage}) -> + storage.markerContainer.toggleComplementary() }) diff --git a/extension/lib/parse-prefs.coffee b/extension/lib/parse-prefs.coffee index 513a967..a26fa69 100644 --- a/extension/lib/parse-prefs.coffee +++ b/extension/lib/parse-prefs.coffee @@ -1,5 +1,5 @@ ### -# Copyright Simon Lydell 2015. +# Copyright Simon Lydell 2015, 2016. # # This file is part of VimFx. # @@ -71,10 +71,20 @@ parsePatterns = (value) -> parsers = { hint_chars: (value, defaultValue) -> - parsed = utils.removeDuplicateCharacters(value).replace(/\s/g, '') + [leading..., end] = value.trim().split(/\s+/) + parsed = if leading.length > 0 then "#{leading.join('')} #{end}" else end + parsed = utils.removeDuplicateCharacters(parsed) + # Make sure that hint chars contain at least the required amount of chars. if parsed.length < MIN_NUM_HINT_CHARS parsed = defaultValue[...MIN_NUM_HINT_CHARS] + + unless parsed.includes(' ') + numDefaultSecondaryHintChars = + defaultValue.length - 1 - defaultValue.indexOf(' ') + index = Math.min(parsed.length // 2, numDefaultSecondaryHintChars) + parsed = "#{parsed[...-index]} #{parsed[-index..]}" + return {parsed, normalized: parsed} prev_patterns: parsePatterns diff --git a/extension/lib/viewport.coffee b/extension/lib/viewport.coffee index f2ff7ab..5591141 100644 --- a/extension/lib/viewport.coffee +++ b/extension/lib/viewport.coffee @@ -265,8 +265,9 @@ isInsideViewport = (rect, viewport) -> rect.right >= viewport.left + MINIMUM_EDGE_DISTANCE and rect.bottom >= viewport.top + MINIMUM_EDGE_DISTANCE -scroll = (element, args) -> - {method, type, directions, amounts, properties, adjustment, smooth} = args +scroll = ( + element, {method, type, directions, amounts, properties, adjustment, smooth} +) -> options = { behavior: if smooth then 'smooth' else 'instant' } diff --git a/extension/locale/de/vimfx.properties b/extension/locale/de/vimfx.properties index 0d53fcc..8f075b4 100644 --- a/extension/locale/de/vimfx.properties +++ b/extension/locale/de/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=Überlappende Markierungen vorwärts drehen mode.hints.rotate_markers_backward=Überlappende Markierungen rückwärts drehen mode.hints.delete_hint_char=Lösche zuletzt eingegebenes Hinweiszeichen mode.hints.increase_count=Anzahl erhöhen -mode.hints.mark_everything=Mark everything +mode.hints.toggle_complementary=Mark all other elements mode.ignore=Ignoriermodus mode.ignore.exit=Zum Normalmodus zurückkehren diff --git a/extension/locale/en-US/vimfx.properties b/extension/locale/en-US/vimfx.properties index 03eeb17..6f3fe20 100644 --- a/extension/locale/en-US/vimfx.properties +++ b/extension/locale/en-US/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=Rotate overlapping markers forward mode.hints.rotate_markers_backward=Rotate overlapping markers backward mode.hints.delete_hint_char=Delete last typed hint character mode.hints.increase_count=Increase count -mode.hints.mark_everything=Mark everything +mode.hints.toggle_complementary=Mark all other elements mode.ignore=Ignore mode mode.ignore.exit=Return to Normal mode diff --git a/extension/locale/es/vimfx.properties b/extension/locale/es/vimfx.properties index aa22fbd..a7257d6 100644 --- a/extension/locale/es/vimfx.properties +++ b/extension/locale/es/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=Rotar indicaciones superpuestas hacia delante mode.hints.rotate_markers_backward=Rotar indicaciones superpuestas hacia atrás mode.hints.delete_hint_char=Borrar último caracter de indicación tecleado mode.hints.increase_count=Incrementar contador -mode.hints.mark_everything=Mark everything +mode.hints.toggle_complementary=Mark all other elements mode.ignore=Modo Ignorar mode.ignore.exit=Volver a modo Normal diff --git a/extension/locale/fr/vimfx.properties b/extension/locale/fr/vimfx.properties index 9b60826..7325f34 100644 --- a/extension/locale/fr/vimfx.properties +++ b/extension/locale/fr/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=Faire tourner vers l'avant les marqueurs super mode.hints.rotate_markers_backward=Faire tourner en arrière mode.hints.delete_hint_char=Supprimer le dernier caractère frappé lors de la sélection d'un marqueur mode.hints.increase_count= -mode.hints.mark_everything=Mark everything +mode.hints.toggle_complementary=Mark all other elements mode.ignore=Mode ignorer mode.ignore.exit=Retourner au mode par défaut diff --git a/extension/locale/id/vimfx.properties b/extension/locale/id/vimfx.properties index f30c969..39ab01a 100644 --- a/extension/locale/id/vimfx.properties +++ b/extension/locale/id/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=Putar maju penanda bertumpukan mode.hints.rotate_markers_backward=Putar mundur penanda bertumpukan mode.hints.delete_hint_char=Hapus karakter petunjuk terakhir diketik mode.hints.increase_count=Naikkan hitungan -mode.hints.mark_everything=Mark everything +mode.hints.toggle_complementary=Mark all other elements mode.ignore=Mode Abai mode.ignore.exit=Kembali ke mode Normal diff --git a/extension/locale/it/vimfx.properties b/extension/locale/it/vimfx.properties index ebc022b..a5579b7 100644 --- a/extension/locale/it/vimfx.properties +++ b/extension/locale/it/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=Ruota i marcatori sovrappossti in avanti mode.hints.rotate_markers_backward=Ruota i marcatori sovrappossti all'indietro mode.hints.delete_hint_char=Cancella l'ultimo carattere di suggerimento digitato mode.hints.increase_count=Aumenta il conteggio -mode.hints.mark_everything=Mark everything +mode.hints.toggle_complementary=Mark all other elements mode.ignore=Modalità Ignore mode.ignore.exit=Ritorna al modo normale diff --git a/extension/locale/ja/vimfx.properties b/extension/locale/ja/vimfx.properties index 556903a..e085b2a 100644 --- a/extension/locale/ja/vimfx.properties +++ b/extension/locale/ja/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=重なったマーカーを前へ入れ替え mode.hints.rotate_markers_backward=重なったマーカーを後へ入れ替え mode.hints.delete_hint_char=最後に入力したヒント文字を削除 mode.hints.increase_count=カウントを増やす -mode.hints.mark_everything=Mark everything +mode.hints.toggle_complementary=Mark all other elements mode.ignore=無効モード mode.ignore.exit=ノーマルモードへ戻る diff --git a/extension/locale/nl/vimfx.properties b/extension/locale/nl/vimfx.properties index 3997dea..e6352b0 100644 --- a/extension/locale/nl/vimfx.properties +++ b/extension/locale/nl/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=Ga vooruit in markervolgorde mode.hints.rotate_markers_backward=Ga achteruit in markervolgorde mode.hints.delete_hint_char=Verwijder het laatst getypte markerkarakter mode.hints.increase_count=Verhoog aantal -mode.hints.mark_everything=Mark everything +mode.hints.toggle_complementary=Mark all other elements mode.ignore=Negeermodus mode.ignore.exit=Ga terug naar normale modus diff --git a/extension/locale/pt-BR/vimfx.properties b/extension/locale/pt-BR/vimfx.properties index 29e5c92..b8926fa 100644 --- a/extension/locale/pt-BR/vimfx.properties +++ b/extension/locale/pt-BR/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=Rotacionar marcadores sobrepostos para frente mode.hints.rotate_markers_backward=Rotacionar marcadores sobrepostos para trás mode.hints.delete_hint_char=Deletar o último caractere digitado da sugestão mode.hints.increase_count=Incrementar contador -mode.hints.mark_everything=Mark everything +mode.hints.toggle_complementary=Mark all other elements mode.ignore=Modo Ignore mode.ignore.exit=Voltar ao modo Normal diff --git a/extension/locale/ru/vimfx.properties b/extension/locale/ru/vimfx.properties index 3249eb3..967ef36 100644 --- a/extension/locale/ru/vimfx.properties +++ b/extension/locale/ru/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=Переставить перекрывающ mode.hints.rotate_markers_backward=Переставить перекрывающиеся маркеры в обратном порядке mode.hints.delete_hint_char=Удалить последний введённый символ mode.hints.increase_count=Увеличить количество -mode.hints.mark_everything=Mark everything +mode.hints.toggle_complementary=Mark all other elements mode.ignore=Режим игнорирования mode.ignore.exit=Вернуться в нормальный режим diff --git a/extension/locale/sv-SE/vimfx.properties b/extension/locale/sv-SE/vimfx.properties index c32169e..7b077dc 100644 --- a/extension/locale/sv-SE/vimfx.properties +++ b/extension/locale/sv-SE/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=Rotera överlappande etiketter framåt mode.hints.rotate_markers_backward=Rotera överlappande etiketter bakåt mode.hints.delete_hint_char=Radera senast inmatade tecken mode.hints.increase_count=Öka antal -mode.hints.mark_everything=Ge etiketter till allt +mode.hints.toggle_complementary=Ge etiketter till alla andra element mode.ignore=Ignoreringsläge mode.ignore.exit=Återvänd till Normalläge diff --git a/extension/locale/zh-CN/vimfx.properties b/extension/locale/zh-CN/vimfx.properties index 046dee1..65823b9 100644 --- a/extension/locale/zh-CN/vimfx.properties +++ b/extension/locale/zh-CN/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=向前旋转重叠的标记 mode.hints.rotate_markers_backward=向后旋转重叠的标记 mode.hints.delete_hint_char=删除最后输入的提示符 mode.hints.increase_count=递增计数 -mode.hints.mark_everything=标记所有元素 +mode.hints.toggle_complementary=Mark all other elements mode.ignore=忽略模式 mode.ignore.exit=返回普通模式 diff --git a/extension/locale/zh-TW/vimfx.properties b/extension/locale/zh-TW/vimfx.properties index a60b430..0775da1 100644 --- a/extension/locale/zh-TW/vimfx.properties +++ b/extension/locale/zh-TW/vimfx.properties @@ -116,7 +116,7 @@ mode.hints.rotate_markers_forward=向前旋轉重疊的標誌 mode.hints.rotate_markers_backward=向後旋轉重疊的標誌 mode.hints.delete_hint_char=刪除最後輸入的提示字元 mode.hints.increase_count=遞增計數 -mode.hints.mark_everything=Mark everything +mode.hints.toggle_complementary=Mark all other elements mode.ignore=忽略模式 mode.ignore.exit=回到正常模式 diff --git a/extension/test/test-api.coffee b/extension/test/test-api.coffee index 187aebc..5bdfd5b 100644 --- a/extension/test/test-api.coffee +++ b/extension/test/test-api.coffee @@ -43,7 +43,7 @@ exports['test exports'] = (assert, $vimfx) -> exports['test vimfx.get and vimfx.set'] = (assert, $vimfx, teardown) -> vimfx = createConfigAPI($vimfx) - resetHintChars = prefs.tmp('hint_chars', 'abcd') + resetHintChars = prefs.tmp('hint_chars', 'ab cd') resetBlacklist = prefs.tmp('blacklist', null) originalOptions = Object.assign({}, $vimfx.options) teardown(-> @@ -52,11 +52,11 @@ exports['test vimfx.get and vimfx.set'] = (assert, $vimfx, teardown) -> $vimfx.options = originalOptions ) - assert.equal(vimfx.get('hint_chars'), 'abcd') + assert.equal(vimfx.get('hint_chars'), 'ab cd') assert.ok(not prefs.has('blacklist')) - vimfx.set('hint_chars', 'xyz') - assert.equal(vimfx.get('hint_chars'), 'xyz') + vimfx.set('hint_chars', 'xy z') + assert.equal(vimfx.get('hint_chars'), 'xy z') vimfx.set('blacklist', 'test') assert.equal(vimfx.get('blacklist'), 'test') @@ -65,14 +65,14 @@ exports['test vimfx.get and vimfx.set'] = (assert, $vimfx, teardown) -> assert.deepEqual(vimfx.get('translations'), {KeyQ: ['ö', 'Ö']}) $vimfx.emit('shutdown') - assert.equal(vimfx.get('hint_chars'), 'abcd') + assert.equal(vimfx.get('hint_chars'), 'ab cd') assert.ok(not prefs.has('blacklist')) assert.deepEqual(vimfx.get('translations'), {}) exports['test vimfx.getDefault'] = (assert, $vimfx, teardown) -> vimfx = createConfigAPI($vimfx) - reset = prefs.tmp('hint_chars', 'abcd') + reset = prefs.tmp('hint_chars', 'ab cd') teardown(-> reset?() ) -- 2.39.3