]> git.gir.st - VimFx.git/blob - extension/lib/modes.coffee
Make `<c-q>` in the options able to insert `<escape>`
[VimFx.git] / extension / lib / modes.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014.
4 # Copyright Wang Zhuochun 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 { commands
23 , findStorage } = require('./commands')
24 defaults = require('./defaults')
25 help = require('./help')
26 hints = require('./hints')
27 translate = require('./l10n')
28 { rotateOverlappingMarkers } = require('./marker')
29 utils = require('./utils')
30
31 { interfaces: Ci } = Components
32
33 XULDocument = Ci.nsIDOMXULDocument
34
35 # Helper to create modes in a DRY way.
36 mode = (modeName, obj, commands) ->
37 obj.name = translate.bind(null, "mode.#{ modeName }")
38 obj.order = defaults.mode_order[modeName]
39 obj.commands = {}
40 for commandName, fn of commands
41 pref = "mode.#{ modeName }.#{ commandName }"
42 obj.commands[commandName] =
43 pref: defaults.BRANCH + pref
44 run: fn
45 category: defaults.categoryMap[pref]
46 description: translate.bind(null, pref)
47 order: defaults.command_order[pref]
48 exports[modeName] = obj
49
50
51
52 mode('normal', {
53 onEnter: ({ vim, storage, args: [enterMode] }) ->
54 if enterMode
55 storage.enterMode = enterMode
56 else if storage.enterMode
57 vim.enterMode(storage.enterMode)
58 storage.enterMode = null
59
60 onLeave: ({ vim, storage }) ->
61 storage.inputs = null
62 help.removeHelp(vim.rootWindow)
63
64 onInput: (args, match) ->
65 { vim, storage, event } = args
66 { keyStr } = match
67
68 if storage.inputs
69 index = storage.inputs.indexOf(vim.window.document.activeElement)
70 if index >= 0
71 storage.inputIndex = index
72 else
73 storage.inputs = null
74
75 autoInsertMode = (match.focus != null)
76 if match.type == 'none' or (autoInsertMode and not match.force)
77 if storage.enterMode
78 vim.enterMode(storage.enterMode)
79 storage.enterMode = null
80 return false
81
82 if match.type == 'full'
83 { command } = match
84 # Rely on the default `<tab>` behavior, since it allows web pages to
85 # provide tab completion, for example, inside text inputs.
86 unless match.toplevel and not storage.inputs and
87 ((command.run == commands.focus_previous and keyStr == '<tab>') or
88 (command.run == commands.focus_next and keyStr == '<s-tab>'))
89 command.run(args)
90
91 # If the command changed the mode, wait until coming back from that mode
92 # before switching to `storage.enterMode` if any (see `onEnter` above).
93 if storage.enterMode and vim.mode == 'normal'
94 vim.enterMode(storage.enterMode)
95 storage.enterMode = null
96
97 # At this point the match is either full, partial or part of a count. Then
98 # we always want to suppress, except for one case: The Escape key.
99 #
100 # - It allows for stopping the loading of the page.
101 # - It allows for closing many custom dialogs (and perhaps other things
102 # -- Esc is a very commonly used key).
103 # - It is not passed if Esc is used for `command_Esc` and we’re blurring
104 # an element. That allows for blurring an input in a custom dialog
105 # without closing the dialog too.
106 # - There are two reasons we might suppress it in other modes. If some
107 # custom dialog of a website is open, we should be able to cancel hint
108 # markers on it without closing it. Secondly, otherwise cancelling hint
109 # markers on Google causes its search bar to be focused.
110 # - It may only be suppressed in web pages, not in browser chrome. That
111 # allows for reseting the location bar when blurring it, and closing
112 # dialogs such as the “bookmark this page” dialog (<c-d>).
113 document = event.originalTarget.ownerDocument
114 inBrowserChrome = (document instanceof XULDocument)
115 if keyStr == '<escape>' and (not autoInsertMode or inBrowserChrome)
116 return false
117
118 return true
119
120 }, commands)
121
122
123
124 mode('hints', {
125 onEnter: ({ vim, storage, args: [ filter, callback, count ] }) ->
126 [ markers, container ] = hints.injectHints(
127 vim.rootWindow, vim.window, filter, vim.parent.options
128 )
129 if markers.length > 0
130 storage.markers = markers
131 storage.container = container
132 storage.callback = callback
133 storage.count = count ? 1
134 storage.numEnteredChars = 0
135 else
136 vim.enterMode('normal')
137
138 onLeave: ({ vim, storage }) ->
139 { container } = storage
140 vim.rootWindow.setTimeout((->
141 container?.remove()
142 ), vim.parent.options.hints_timeout)
143 for key of storage
144 storage[key] = null
145
146 onInput: (args, match) ->
147 { vim, storage } = args
148 { markers, callback } = storage
149
150 if match.type == 'full'
151 match.command.run(args)
152 else if match.unmodifiedKey in vim.parent.options.hint_chars
153 matchedMarkers = []
154
155 for marker in markers when marker.hintIndex == storage.numEnteredChars
156 matched = marker.matchHintChar(match.unmodifiedKey)
157 marker.hide() unless matched
158 if marker.isMatched()
159 marker.markMatched(true)
160 matchedMarkers.push(marker)
161
162 if matchedMarkers.length > 0
163 again = callback(matchedMarkers[0], storage.count, match.keyStr)
164 storage.count--
165 if again
166 vim.rootWindow.setTimeout((->
167 marker.markMatched(false) for marker in matchedMarkers
168 ), vim.parent.options.hints_timeout)
169 marker.reset() for marker in markers
170 storage.numEnteredChars = 0
171 else
172 vim.enterMode('normal')
173 else
174 storage.numEnteredChars++
175
176 return true
177
178 }, {
179 exit: ({ vim, storage }) ->
180 # The hints are removed automatically when leaving the mode, but after a
181 # timeout. When aborting the mode we should remove the hints immediately.
182 storage.container?.remove()
183 vim.enterMode('normal')
184
185 rotate_markers_forward: ({ storage }) ->
186 rotateOverlappingMarkers(storage.markers, true)
187
188 rotate_markers_backward: ({ storage }) ->
189 rotateOverlappingMarkers(storage.markers, false)
190
191 delete_hint_char: ({ storage }) ->
192 for marker in storage.markers
193 switch marker.hintIndex - storage.numEnteredChars
194 when 0 then marker.deleteHintChar()
195 when -1 then marker.show()
196 storage.numEnteredChars-- unless storage.numEnteredChars == 0
197
198 increase_count: ({ storage }) -> storage.count++
199 })
200
201
202
203 mode('ignore', {
204 onEnter: ({ vim, storage, args: [ count ] }) ->
205 storage.count = count ? null
206
207 onLeave: ({ vim, storage }) ->
208 utils.blurActiveElement(vim.window) unless storage.count?
209
210 onInput: (args, match) ->
211 { vim, storage } = args
212 switch storage.count
213 when null
214 if match.type == 'full'
215 match.command.run(args)
216 return true
217 when 1
218 vim.enterMode('normal')
219 else
220 storage.count--
221 return false
222
223 }, {
224 exit: ({ vim }) -> vim.enterMode('normal')
225 unquote: ({ vim }) -> vim.enterMode('normal', 'ignore')
226 })
227
228
229
230 mode('find', {
231 onEnter: ->
232
233 onLeave: ({ vim }) ->
234 findBar = vim.rootWindow.gBrowser.getFindBar()
235 findStorage.lastSearchString = findBar._findField.value
236
237 onInput: (args, match) ->
238 args.findBar = args.vim.rootWindow.gBrowser.getFindBar()
239 if match.type == 'full'
240 match.command.run(args)
241 return true
242 return false
243
244 }, {
245 exit: ({ findBar }) -> findBar.close()
246 })
Imprint / Impressum