From c4c417368af62e8b27f20121554f4ea03ef81c6e Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Wed, 10 Aug 2016 12:08:32 +0200 Subject: [PATCH] Make it possible to create custom hint commands My first idea was to use `vim.enterMode('hints', ...)`. However, that's too complicated to be useful for any user. Actually, I found that `vim.enterMode(...)` is too complicated for most (all?) modes, so I decided to make it private (removing it from the public API) while at it. (I also made the arguments of `vim.enterMode`/`mode.onEnter` more consistent with each other.) I also considered exposing the entire "API" that the `f` commands use internally. However, that's more complicated than the average user needs, and would increase the API surface _a lot._ Instead, the `f` commands now take a `callbackOverride` argument. This lets you choose the `f` command that matches the elements you're looking for and the override what happens when a hint marker is matched. Fixes #785. Fixes #654. --- documentation/api.md | 121 ++++++++++++++++++++++++--- extension/lib/api-frame.coffee | 4 + extension/lib/button.coffee | 2 +- extension/lib/commands.coffee | 74 ++++++++++------ extension/lib/events-frame.coffee | 4 +- extension/lib/events.coffee | 4 +- extension/lib/modes.coffee | 36 ++++---- extension/lib/vim-frame.coffee | 4 +- extension/lib/vim.coffee | 17 ++-- extension/test/test-api-frame.coffee | 17 ++++ 10 files changed, 212 insertions(+), 71 deletions(-) diff --git a/documentation/api.md b/documentation/api.md index 0e3e14e..8361987 100644 --- a/documentation/api.md +++ b/documentation/api.md @@ -113,7 +113,7 @@ one using `vimfx.set(...)` to able to use the new command. vimfx.addCommand({ name: 'hello', description: 'Log Hello World', -}, => { +}, () => { console.log('Hello World!') }) // Optional: @@ -246,7 +246,7 @@ can be used to replace the blacklist option). ```js vimfx.on('locationChange', ({vim, location}) => { if (location.hostname === 'example.com') { - vim.enterMode('ignore', {type: 'blacklist'}) + vimfx.modes.normal.commands.enter_mode_ignore.run({vim, blacklist: true}) } }) ``` @@ -458,6 +458,101 @@ categories.custom = { [categories.tabs.order, commands.focus_location_bar.order] ``` +### Custom hints commands + +Apart from the standard `f` commands, you can create your own. + +You may run any VimFx command by using the following pattern: + +```js +// config.js +vimfx.addCommand({ + name: 'run_other_command_example', + description: 'Run other command example', +}, (args) => { + // Replace 'follow' with any command name here: + vimfx.modes.normal.commands.follow.run(args) +}) +``` + +All `f` commands (except `zF`) also support `args.callbackOverride`: + +```js +// config.js +vimfx.addCommand({ + name: 'custom_hints_command_example', + description: 'Custom hints command example', +}, (args) => { + vimfx.modes.normal.commands.follow.run(Object.assign({}, args, { + callbackOverride({type, href, id, timesLeft}) { + console.log('Marker data:', {type, href, id, timesLeft}) + return (timesLeft > 1) + }, + )) +}) +``` + +This lets you piggy-back on one of the existing `f` commands by getting the same +hints on screen as that command, but then doing something different with the +matched hint marker. + +`callbackOverride` is called with an object with the following properties: + +- type: `String`. The type of the element of the matched hint marker. See + [`vimfx.setHintMatcher(...)`] for all possible values. + +- href: `String` or `null`. If `type` is `'link'`, then this is the `href` + attribute of the element of the matched hint marker. + +- id: An id that you can pass to [`vimfx.getMarkerElement(...)`] to get the + element of the matched hint marker. + +- timesLeft: `Number`. Calling an `f` command means that you want to run it + _count_ times in a row. This number tells how many times there are left to + run. If you don’t provide a count, the number is `1`. + +`callbackOverride` should return `true` if you want the hint markers to +re-appear on screen after you’ve matched one of them (as in the `af` command), +and `false` if you wish to exit Hints mode. If your command ignores counts, +simply always return `false`. Otherwise you most likely want to return +`timesLeft > 1`. + +Here’s an example which adds a command for opening a link in a new private +window using hint markers. It also highlights those links with a red background. + +```js +// config.js +let {commands} = vimfx.modes.normal + +vimfx.addCommand({ + name: 'follow_in_private_window', + category: 'browsing', + order: commands.follow_in_window.order + 1, + description: 'Follow link in a new private window', +}, (args) => { + let {vim} = args + commands.follow_in_window.run(Object.assign({}, args, { + callbackOverride({type, href, id, timesLeft}) { + if (href) { + vim.window.openLinkIn(href, 'window', {private: true}) + vimfx.send(vim, 'highlight_marker_element', {id}) + } + return false + }, + })) +}) +``` + +```js +// frame.js +vimfx.listen('highlight_marker_element', ({id}) => { + let element = vimfx.getMarkerElement(id) + if (element) { + element.style.backgroundColor = 'red' + } +}) +``` + ### Mode object A mode is an object with the following properties: @@ -488,11 +583,8 @@ properties: ##### onEnter This method is called with an object as mentioned above, and after that there -may be any number of arguments (`args` in `vim.enterMode(modeName, ...args)`) -that the mode is free to do whatever it wants with. - -Whatever is returned from `onEnter` will be returned from -`vim.enterMode(modeName, ...args)`. +may be any number of arguments that the mode is free to do whatever it wants +with. ##### onInput @@ -646,12 +738,6 @@ A `vim` object has the following properties: `match.likelyConflict` of [match object]s depend on `focusType`. -- enterMode(modeName, ...args): `Function`. Enter mode `modeName`, passing - `...args` to the mode. It is up to every mode to do whatever it wants to with - `...args`. If `modeName` was already the current mode, nothing is done and - `undefined` is returned. Otherwise it us up to the mode to return whatever it - wants to. - - isUIEvent(event): `Function`. Returns `true` if `event` occurred in the browser UI, and `false` otherwise (if it occurred in web page content). @@ -781,6 +867,12 @@ The available type strings depend on `id`: The function must return `null` or a string like the `type` parameter. +### `vimfx.getMarkerElement(id)` + +Takes an id that has been given to you when creating [custom hints commands] and +returns the DOM element associated with that id. If no element can be found, +`null` is returned. + ## Stability @@ -796,6 +888,7 @@ backwards compatibility will be a priority and won’t be broken until VimFx [`vimfx.send(...)`]: #vimfxsendvim-message-data--null-callback--null [`vimfx.listen(...)`]: #vimfxlistenmessage-listener [categories]: #vimfxgetcategories +[custom hints commands]: #custom-hints-commands [`vimfx.modes`]: #vimfxmodes [onInput]: #oninput [mode object]: #mode-object @@ -807,6 +900,8 @@ backwards compatibility will be a priority and won’t be broken until VimFx [location object]: #location-object [The `focusTypeChange` event]: #the-focustypechange-event [the `shutdown` event]: #the-shutdown-event +[`vimfx.setHintMatcher(...)`]: #vimfxsethintmatcherhintmatcher +[`vimfx.getMarkerElement(...)`]: #vimfxgetmarkerelementid [blacklisted]: options.md#blacklist [special options]: options.md#special-options diff --git a/extension/lib/api-frame.coffee b/extension/lib/api-frame.coffee index f14d02c..abd94a0 100644 --- a/extension/lib/api-frame.coffee +++ b/extension/lib/api-frame.coffee @@ -44,6 +44,10 @@ createConfigAPI = (vim, onShutdown = module.onShutdown) -> { ) vim.hintMatcher = hintMatcher onShutdown(-> vim.hintMatcher = null) + + getMarkerElement: (id) -> + data = vim.state.markerElements[id] + return if data then data.element else null } module.exports = createConfigAPI diff --git a/extension/lib/button.coffee b/extension/lib/button.coffee index 08c2453..b222a18 100644 --- a/extension/lib/button.coffee +++ b/extension/lib/button.coffee @@ -47,7 +47,7 @@ injectButton = (vimfx) -> if vim.mode == 'normal' and not helpVisible help.injectHelp(window, vimfx) else - vim.enterMode('normal') + vim._enterMode('normal') }) module.onShutdown(-> cui.destroyWidget(BUTTON_ID)) diff --git a/extension/lib/commands.coffee b/extension/lib/commands.coffee index 4d9618b..59369b1 100644 --- a/extension/lib/commands.coffee +++ b/extension/lib/commands.coffee @@ -241,11 +241,13 @@ helper_mark_last_scroll_position = (vim) -> vim._run('mark_scroll_position', {keyStr, notify: false}) commands.mark_scroll_position = ({vim}) -> - vim.enterMode('marks', (keyStr) -> vim._run('mark_scroll_position', {keyStr})) + vim._enterMode('marks', (keyStr) -> + vim._run('mark_scroll_position', {keyStr}) + ) vim.notify(translate('notification.mark_scroll_position.enter')) commands.scroll_to_mark = ({vim}) -> - vim.enterMode('marks', (keyStr) -> + vim._enterMode('marks', (keyStr) -> unless keyStr == vim.options['scroll.last_position_mark'] helper_mark_last_scroll_position(vim) helper_scroll( @@ -428,7 +430,7 @@ commands.tab_close_other = ({vim}) -> -helper_follow = (name, vim, callback, count = null) -> +helper_follow = ({name, callback}, {vim, count, callbackOverride = null}) -> {window} = vim vim.markPageInteraction() help.removeHelp(window) @@ -452,11 +454,19 @@ helper_follow = (name, vim, callback, count = null) -> markerContainer.container ) + chooseCallback = (marker, timesLeft, keyStr) -> + if callbackOverride + {type, href = null, elementIndex} = marker.wrapper + return callbackOverride({type, href, id: elementIndex, timesLeft}) + else + return callback(marker, timesLeft, keyStr) + # 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 + vim._enterMode('hints', { + markerContainer, count + callback: chooseCallback sleep: vim.options.hints_sleep }) @@ -467,7 +477,7 @@ helper_follow = (name, vim, callback, count = null) -> if wrappers.length == 0 if pass in ['single', 'second'] and markerContainer.markers.length == 0 vim.notify(translate('notification.follow.none')) - vim.enterMode('normal') + vim._enterMode('normal') else markerContainer.injectHints(wrappers, viewport, pass) @@ -476,7 +486,9 @@ helper_follow = (name, vim, callback, count = null) -> vim._run(name, {pass: 'auto'}, injectHints) -helper_follow_clickable = (options, {vim, count = 1}) -> +helper_follow_clickable = (options, args) -> + {vim} = args + callback = (marker, timesLeft, keyStr) -> {inTab, inBackground} = options {type, elementIndex} = marker.wrapper @@ -527,7 +539,7 @@ helper_follow_clickable = (options, {vim, count = 1}) -> return not isLast name = if options.inTab then 'follow_in_tab' else 'follow' - helper_follow(name, vim, callback, count) + helper_follow({name, callback}, args) commands.follow = helper_follow_clickable.bind(null, {inTab: false, inBackground: true}) @@ -538,19 +550,24 @@ commands.follow_in_tab = commands.follow_in_focused_tab = helper_follow_clickable.bind(null, {inTab: true, inBackground: false}) -commands.follow_in_window = ({vim}) -> +commands.follow_in_window = (args) -> + {vim} = args + callback = (marker) -> vim._focusMarkerElement(marker.wrapper.elementIndex) {href} = marker.wrapper vim.window.openLinkIn(href, 'window', {}) if href return false - helper_follow('follow_in_tab', vim, callback) + + helper_follow({name: 'follow_in_tab', callback}, args) commands.follow_multiple = (args) -> args.count = Infinity commands.follow(args) -commands.follow_copy = ({vim}) -> +commands.follow_copy = (args) -> + {vim} = args + callback = (marker) -> property = switch marker.wrapper.type when 'link' @@ -561,13 +578,17 @@ commands.follow_copy = ({vim}) -> '_selection' helper_copy_marker_element(vim, marker.wrapper.elementIndex, property) return false - helper_follow('follow_copy', vim, callback) -commands.follow_focus = ({vim}) -> + helper_follow({name: 'follow_copy', callback}, args) + +commands.follow_focus = (args) -> + {vim} = args + callback = (marker) -> vim._focusMarkerElement(marker.wrapper.elementIndex, {select: true}) return false - helper_follow('follow_focus', vim, callback) + + helper_follow({name: 'follow_focus', callback}, args) commands.click_browser_element = ({vim}) -> {window} = vim @@ -663,7 +684,7 @@ commands.click_browser_element = ({vim}) -> ) markerContainer.injectHints(wrappers, viewport, 'single') - vim.enterMode('hints', {markerContainer, callback}) + vim._enterMode('hints', {markerContainer, callback}) else vim.notify(translate('notification.follow.none')) @@ -684,16 +705,19 @@ commands.focus_text_input = ({vim, count}) -> vim.markPageInteraction() vim._run('focus_text_input', {count}) -helper_follow_selectable = ({select}, {vim}) -> +helper_follow_selectable = ({select}, args) -> + {vim} = args + callback = (marker) -> vim._run('element_text_select', { elementIndex: marker.wrapper.elementIndex full: select scroll: select }) - vim.enterMode('caret', select) + vim._enterMode('caret', {select}) return false - helper_follow('follow_selectable', vim, callback) + + helper_follow({name: 'follow_selectable', callback}, args) commands.element_text_caret = helper_follow_selectable.bind(null, {select: false}) @@ -701,11 +725,12 @@ commands.element_text_caret = commands.element_text_select = helper_follow_selectable.bind(null, {select: true}) -commands.element_text_copy = ({vim}) -> +commands.element_text_copy = (args) -> + {vim} = args callback = (marker) -> helper_copy_marker_element(vim, marker.wrapper.elementIndex, '_selection') return false - helper_follow('follow_selectable', vim, callback) + helper_follow(args, {name: 'follow_selectable', callback}) helper_copy_marker_element = (vim, elementIndex, property) -> if property == '_selection' @@ -747,7 +772,7 @@ helper_find = ({highlight, linksOnly = false}, {vim}) -> # In case `helper_find_from_top_of_viewport` is slow, make sure that keys # pressed before the find bar input is focsued doesn’t trigger commands. - vim.enterMode('find') + vim._enterMode('find') helper_mark_last_scroll_position(vim) helper_find_from_top_of_viewport(vim, FORWARD, -> @@ -806,12 +831,13 @@ commands.window_new = ({vim}) -> commands.window_new_private = ({vim}) -> vim.window.OpenBrowserWindow({private: true}) -commands.enter_mode_ignore = ({vim}) -> - vim.enterMode('ignore', {type: 'explicit'}) +commands.enter_mode_ignore = ({vim, blacklist = false}) -> + type = if blacklist then 'blacklist' else 'explicit' + vim._enterMode('ignore', {type}) # Quote next keypress (pass it through to the page). commands.quote = ({vim, count = 1}) -> - vim.enterMode('ignore', {type: 'explicit', count}) + vim._enterMode('ignore', {type: 'explicit', count}) commands.enter_reader_view = ({vim}) -> button = vim.window.document.getElementById('reader-mode-button') diff --git a/extension/lib/events-frame.coffee b/extension/lib/events-frame.coffee index c8b084b..50cff97 100644 --- a/extension/lib/events-frame.coffee +++ b/extension/lib/events-frame.coffee @@ -95,7 +95,7 @@ class FrameEventManager if target == @vim.content.document messageManager.send('frameCanReceiveEvents', false) - @vim.enterMode('normal') if @vim.mode == 'hints' + @vim._enterMode('normal') if @vim.mode == 'hints' # If the target isn’t the topmost document, it means that a frame has # changed: It could have been removed or its `src` attribute could have @@ -224,7 +224,7 @@ class FrameEventManager @vim.state.hasFocusedTextInput = true if @vim.mode == 'caret' and not utils.isContentEditable(target) - @vim.enterMode('normal') + @vim._enterMode('normal') # Blur the focus target, if autofocus prevention is enabled… if prefs.get('prevent_autofocus') and diff --git a/extension/lib/events.coffee b/extension/lib/events.coffee index 998bd3d..33b7261 100644 --- a/extension/lib/events.coffee +++ b/extension/lib/events.coffee @@ -130,7 +130,7 @@ class UIEventManager # mode. Otherwise we’d first return to normal mode and then the button # would open the help dialog. target != button.getButton(@window) - vim.enterMode('normal') + vim._enterMode('normal') vim._send('clearHover') unless isVimFxGeneratedEvent ) @@ -201,7 +201,7 @@ class UIEventManager vim._setFocusType(focusType) if focusType == 'editable' and vim.mode == 'caret' - vim.enterMode('normal') + vim._enterMode('normal') consumeKeyEvent: (vim, event) -> match = vim._consumeKeyEvent(event) diff --git a/extension/lib/modes.coffee b/extension/lib/modes.coffee index 4968b07..08810d3 100644 --- a/extension/lib/modes.coffee +++ b/extension/lib/modes.coffee @@ -52,11 +52,11 @@ mode = (modeName, obj, commands = null) -> mode('normal', { - onEnter: ({vim, storage}, options = {}) -> - if options.returnTo - storage.returnTo = options.returnTo + onEnter: ({vim, storage}, {returnTo = null} = {}) -> + if returnTo + storage.returnTo = returnTo else if storage.returnTo - vim.enterMode(storage.returnTo) + vim._enterMode(storage.returnTo) storage.returnTo = null onLeave: ({vim}) -> @@ -70,7 +70,7 @@ mode('normal', { (match.likelyConflict and not match.specialKeys['']) match.discard() if storage.returnTo - vim.enterMode(storage.returnTo) + vim._enterMode(storage.returnTo) storage.returnTo = null # If you press `aa` (and `a` is a prefix key, but there’s no `aa` # shortcut), don’t pass the second `a` to the page. @@ -82,7 +82,7 @@ mode('normal', { # If the command changed the mode, wait until coming back from that mode # before switching to `storage.returnTo` if any (see `onEnter` above). if storage.returnTo and vim.mode == 'normal' - vim.enterMode(storage.returnTo) + vim._enterMode(storage.returnTo) storage.returnTo = null # At this point the match is either full, partial or part of a count. Then @@ -122,7 +122,7 @@ helper_move_caret = (method, direction, {vim, storage, count = 1}) -> }) mode('caret', { - onEnter: ({vim, storage}, select = false) -> + onEnter: ({vim, storage}, {select = false} = {}) -> storage.select = select storage.caretBrowsingPref = prefs.root.get(CARET_BROWSING_PREF) prefs.root.set(CARET_BROWSING_PREF, true) @@ -182,11 +182,11 @@ mode('caret', { # clipboard, since `window.getSelection().toString()` sadly collapses # whitespace in `
` elements.
         vim.window.goDoCommand('cmd_copy')
-        vim.enterMode('normal')
+        vim._enterMode('normal')
     )
 
   exit: ({vim}) ->
-    vim.enterMode('normal')
+    vim._enterMode('normal')
 })
 
 
@@ -241,7 +241,7 @@ mode('hints', {
         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'
+          vim._enterMode('normal') if vim.mode == 'hints'
 
     return true
 
@@ -250,7 +250,7 @@ mode('hints', {
     # The hints are removed automatically when leaving the mode, but after a
     # timeout. When aborting the mode we should remove the hints immediately.
     storage.markerContainer.remove()
-    vim.enterMode('normal')
+    vim._enterMode('normal')
 
   rotate_markers_forward: ({storage}) ->
     storage.markerContainer.rotateOverlapping(true)
@@ -294,7 +294,7 @@ mode('ignore', {
           match.command.run(args)
           return true
       when 1
-        vim.enterMode('normal')
+        vim._enterMode('normal')
       else
         storage.count -= 1
     return false
@@ -302,9 +302,9 @@ mode('ignore', {
 }, {
   exit: ({vim, storage}) ->
     storage.type = null
-    vim.enterMode('normal')
+    vim._enterMode('normal')
   unquote: ({vim}) ->
-    vim.enterMode('normal', {returnTo: 'ignore'})
+    vim._enterMode('normal', {returnTo: 'ignore'})
 })
 
 
@@ -325,7 +325,7 @@ mode('find', {
 
 }, {
   exit: ({vim, findBar}) ->
-    vim.enterMode('normal')
+    vim._enterMode('normal')
     findBar.close()
 })
 
@@ -336,7 +336,7 @@ mode('marks', {
     storage.callback = callback
     storage.timeoutId = vim.window.setTimeout((->
       vim.hideNotification()
-      vim.enterMode('normal')
+      vim._enterMode('normal')
     ), vim.options.timeout)
 
   onLeave: ({vim, storage}) ->
@@ -350,9 +350,9 @@ mode('marks', {
       match.command.run(args)
     else
       storage.callback(match.keyStr)
-      vim.enterMode('normal')
+      vim._enterMode('normal')
     return true
 }, {
   exit: ({vim}) ->
-    vim.enterMode('normal')
+    vim._enterMode('normal')
 })
diff --git a/extension/lib/vim-frame.coffee b/extension/lib/vim-frame.coffee
index 5605018..e38705c 100644
--- a/extension/lib/vim-frame.coffee
+++ b/extension/lib/vim-frame.coffee
@@ -75,9 +75,9 @@ class VimFrame
       # `markerElements` and `inputs` could theoretically need to be filtered
       # too at this point. YAGNI until an issue arises from it.
 
-  enterMode: (@mode, args...) ->
+  _enterMode: (@mode, args...) ->
     messageManager.send('vimMethod', {
-      method: 'enterMode'
+      method: '_enterMode'
       args: [@mode, args...]
     })
 
diff --git a/extension/lib/vim.coffee b/extension/lib/vim.coffee
index 3c927ef..eb8e553 100644
--- a/extension/lib/vim.coffee
+++ b/extension/lib/vim.coffee
@@ -104,7 +104,7 @@ class Vim
            element != @window.gBrowser.selectedBrowser
 
   # `args...` is passed to the mode's `onEnter` method.
-  enterMode: (mode, args...) ->
+  _enterMode: (mode, args...) ->
     return if @mode == mode
 
     unless utils.has(@_parent.modes, mode)
@@ -115,10 +115,9 @@ class Vim
 
     @_call('onLeave') if @mode?
     @mode = mode
-    result = @_call('onEnter', null, args...)
+    @_call('onEnter', null, args...)
     @_parent.emit('modeChange', {vim: this})
     @_send('modeChange', {mode})
-    return result
 
   _consumeKeyEvent: (event) ->
     return @_parent.consumeKeyEvent(event, this)
@@ -131,10 +130,10 @@ class Vim
   _onLocationChange: (url) ->
     switch
       when @_isBlacklisted(url)
-        @enterMode('ignore', {type: 'blacklist'})
+        @_enterMode('ignore', {type: 'blacklist'})
       when (@mode == 'ignore' and @_storage.ignore.type == 'blacklist') or
            not @mode
-        @enterMode('normal')
+        @_enterMode('normal')
     @_parent.emit('locationChange', {vim: this, location: new @window.URL(url)})
 
   _call: (method, data = {}, extraArgs...) ->
@@ -183,13 +182,13 @@ class Vim
     @focusType = focusType
     switch
       when @focusType == 'ignore'
-        @enterMode('ignore', {type: 'focusType'})
+        @_enterMode('ignore', {type: 'focusType'})
       when @mode == 'ignore' and @_storage.ignore.type == 'focusType'
-        @enterMode('normal')
+        @_enterMode('normal')
       when @mode == 'normal' and @focusType == 'findbar'
-        @enterMode('find')
+        @_enterMode('find')
       when @mode == 'find' and @focusType != 'findbar'
-        @enterMode('normal')
+        @_enterMode('normal')
     @_parent.emit('focusTypeChange', {vim: this})
 
 module.exports = Vim
diff --git a/extension/test/test-api-frame.coffee b/extension/test/test-api-frame.coffee
index 817033f..c55e037 100644
--- a/extension/test/test-api-frame.coffee
+++ b/extension/test/test-api-frame.coffee
@@ -27,6 +27,7 @@ exports['test exports'] = (assert, $vim) ->
 
   assert.equal(typeof vimfx.listen, 'function', 'listen')
   assert.equal(typeof vimfx.setHintMatcher, 'function', 'setHintMatcher')
+  assert.equal(typeof vimfx.getMarkerElement, 'function', 'getMarkerElement')
 
 exports['test vimfx.listen'] = (assert, $vim, teardown) ->
   shutdownHandlers = []
@@ -71,6 +72,22 @@ exports['test vimfx.setHintMatcher'] = (assert, $vim) ->
   shutdownHandlers[0]()
   assert.ok(not $vim.hintMatcher)
 
+exports['test vimfx.getMarkerElement'] = (assert, $vim, teardown) ->
+  teardown(->
+    $vim.state.markerElements = []
+  )
+
+  vimfx = createConfigAPI($vim)
+  element = {}
+  $vim.state.markerElements = [{element}]
+
+  assert.equal(vimfx.getMarkerElement(0), element)
+  assert.equal(vimfx.getMarkerElement(1), null)
+  assert.equal(vimfx.getMarkerElement(null), null)
+
+  $vim.state.markerElements = []
+  assert.equal(vimfx.getMarkerElement(0), null)
+
 exports['test vimfx.listen errors'] = (assert, $vim) ->
   vimfx = createConfigAPI($vim)
 
-- 
2.39.3