]> git.gir.st - VimFx.git/blob - extension/lib/modes.coffee
Major refactor: Rework all UI and related improvements
[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: ->
54
55 onLeave: ({ vim }) ->
56 help.removeHelp(vim.rootWindow)
57
58 onInput: (args, match) ->
59 { vim, storage, event } = args
60 target = event.originalTarget
61 document = target.ownerDocument
62
63 { activatable_element_keys, adjustable_element_keys } = vim.parent.options
64 autoInsertMode = \
65 utils.isTextInputElement(target) or
66 utils.isContentEditable(target) or
67 (utils.isActivatable(target) and
68 match.keyStr in activatable_element_keys) or
69 (utils.isAdjustable(target) and
70 match.keyStr in adjustable_element_keys) or
71 vim.rootWindow.TabView.isVisible() or
72 document.fullscreenElement or document.mozFullScreenElement
73
74 return false if match.type == 'none' or (autoInsertMode and not match.force)
75
76 match.command.run(args) if match.type == 'full'
77
78 # At this point the match is either full, partial or part of a count. Then
79 # we always want to suppress, except for one case: The Escape key.
80 #
81 # - It allows for stopping the loading of the page.
82 # - It allows for closing many custom dialogs (and perhaps other things
83 # -- Esc is a very commonly used key).
84 # - It is not passed if Esc is used for `command_Esc` and we’re blurring
85 # an element. That allows for blurring an input in a custom dialog
86 # without closing the dialog too.
87 # - There are two reasons we might suppress it in other modes. If some
88 # custom dialog of a website is open, we should be able to cancel hint
89 # markers on it without closing it. Secondly, otherwise cancelling hint
90 # markers on Google causes its search bar to be focused.
91 # - It may only be suppressed in web pages, not in browser chrome. That
92 # allows for reseting the location bar when blurring it, and closing
93 # dialogs such as the “bookmark this page” dialog (<c-d>).
94 inBrowserChrome = (document instanceof XULDocument)
95 if match.keyStr == '<escape>' and (not autoInsertMode or inBrowserChrome)
96 return false
97
98 return true
99
100 }, commands)
101
102
103
104 mode('hints', {
105 onEnter: ({ vim, storage, args: [ filter, callback ] }) ->
106 [ markers, container ] = hints.injectHints(
107 vim.rootWindow, vim.window, filter, vim.parent.options
108 )
109 if markers.length > 0
110 storage.markers = markers
111 storage.container = container
112 storage.callback = callback
113 storage.numEnteredChars = 0
114 else
115 vim.enterMode('normal')
116
117 onLeave: ({ vim, storage }) ->
118 { container } = storage
119 vim.rootWindow.setTimeout((->
120 container?.remove()
121 ), vim.parent.options.hints_timeout)
122 for key of storage
123 storage[key] = null
124
125 onInput: (args, match) ->
126 { vim, storage } = args
127 { markers, callback } = storage
128
129 if match.type == 'full'
130 match.command.run(args)
131 else if match.keyStr in vim.parent.options.hint_chars
132 matchedMarkers = []
133
134 for marker in markers when marker.hintIndex == storage.numEnteredChars
135 matched = marker.matchHintChar(match.keyStr)
136 marker.hide() unless matched
137 if marker.isMatched()
138 marker.markMatched(true)
139 matchedMarkers.push(marker)
140
141 if matchedMarkers.length > 0
142 again = callback(matchedMarkers[0])
143 if again
144 vim.rootWindow.setTimeout((->
145 marker.markMatched(false) for marker in matchedMarkers
146 ), vim.parent.options.hints_timeout)
147 marker.reset() for marker in markers
148 storage.numEnteredChars = 0
149 else
150 vim.enterMode('normal')
151 else
152 storage.numEnteredChars++
153
154 return true
155
156 }, {
157 exit: ({ vim, storage }) ->
158 # The hints are removed automatically when leaving the mode, but after a
159 # timeout. When aborting the mode we should remove the hints immediately.
160 storage.container?.remove()
161 vim.enterMode('normal')
162
163 rotate_markers_forward: ({ storage }) ->
164 rotateOverlappingMarkers(storage.markers, true)
165
166 rotate_markers_backward: ({ storage }) ->
167 rotateOverlappingMarkers(storage.markers, false)
168
169 delete_hint_char: ({ storage }) ->
170 for marker in storage.markers
171 switch marker.hintIndex - storage.numEnteredChars
172 when 0 then marker.deleteHintChar()
173 when -1 then marker.show()
174 storage.numEnteredChars-- unless storage.numEnteredChars == 0
175 })
176
177
178
179 mode('insert', {
180 onEnter: ({ vim, storage, args: [ count ] }) ->
181 storage.count = count ? null
182
183 onLeave: ({ vim }) ->
184 utils.blurActiveElement(vim.window)
185
186 onInput: (args, match) ->
187 { vim, storage } = args
188 switch storage.count
189 when null
190 if match.type == 'full'
191 match.command.run(args)
192 return true
193 when 1
194 vim.enterMode('normal')
195 else
196 storage.count--
197 return false
198
199 }, {
200 exit: ({ vim }) -> vim.enterMode('normal')
201 })
202
203
204
205 mode('text_input', {
206 onEnter: ({ vim, storage, args: [ inputs ] }) ->
207 storage.inputs = inputs
208
209 onLeave: ({ vim, storage }) ->
210 storage.inputs = null
211
212 onInput: (args, match) ->
213 { vim, storage: { inputs } } = args
214 index = inputs.indexOf(vim.window.document.activeElement)
215 if index == -1
216 vim.enterMode('normal')
217 return false
218 return false unless match.type == 'full'
219 diff = match.command.run(args)
220 inputs[(index + diff) %% inputs.length].select() unless diff == 0
221 return true
222
223 }, {
224 exit: ({ vim }) ->
225 utils.blurActiveElement(vim.window)
226 vim.enterMode('normal')
227 return 0
228 input_previous: -> -1
229 input_next: -> +1
230 })
231
232
233
234 mode('find', {
235 onEnter: ->
236
237 onLeave: ({ vim }) ->
238 findBar = vim.rootWindow.gBrowser.getFindBar()
239 findStorage.lastSearchString = findBar._findField.value
240
241 onInput: (args, match) ->
242 args.findBar = args.vim.rootWindow.gBrowser.getFindBar()
243 if match.type == 'full'
244 match.command.run(args)
245 return true
246 return false
247
248 }, {
249 exit: ({ findBar }) -> findBar.close()
250 })
Imprint / Impressum