# NOTE(smblott). Ultimately, all of the FindMode-related code should be moved here.
# This prevents unmapped printable characters from being passed through to underlying page; see #1415. Only
# used by PostFindMode, below.
class SuppressPrintable extends Mode
constructor: (options) ->
super options
handler = (event) => if KeyboardUtils.isPrintable event then @suppressEvent else @continueBubbling
type = document.getSelection().type
# We use unshift here, so we see events after normal mode, so we only see unmapped keys.
@unshift
_name: "mode-#{@id}/suppress-printable"
keydown: handler
keypress: handler
keyup: (event) =>
# If the selection type has changed (usually, no longer "Range"), then the user is interacting with
# the input element, so we get out of the way. See discussion of option 5c from #1415.
if document.getSelection().type != type then @exit() else handler event
# When we use find, the selection/focus can land in a focusable/editable element. In this situation, special
# considerations apply. We implement three special cases:
# 1. Disable insert mode, because the user hasn't asked to enter insert mode. We do this by using
# InsertMode.suppressEvent.
# 2. Prevent unmapped printable keyboard events from propagating to the page; see #1415. We do this by
# inheriting from SuppressPrintable.
# 3. If the very-next keystroke is Escape, then drop immediately into insert mode.
#
class PostFindMode extends SuppressPrintable
constructor: ->
return unless document.activeElement and DomUtils.isEditable document.activeElement
element = document.activeElement
super
name: "post-find"
# PostFindMode shares a singleton with focusInput; each displaces the other.
singleton: "post-find-mode/focus-input"
exitOnBlur: element
exitOnClick: true
keydown: (event) -> InsertMode.suppressEvent event # Always truthy, so always continues bubbling.
keypress: (event) -> InsertMode.suppressEvent event
keyup: (event) -> InsertMode.suppressEvent event
# If the very-next keydown is Escape, then exit immediately, thereby passing subsequent keys to the
# underlying insert-mode instance.
@push
_name: "mode-#{@id}/handle-escape"
keydown: (event) =>
if KeyboardUtils.isEscape event
DomUtils.suppressKeyupAfterEscape handlerStack
@exit()
@suppressEvent
else
handlerStack.remove()
@continueBubbling
class FindMode extends Mode
@query:
rawQuery: ""
matchCount: 0
hasResults: false
constructor: (options = {}) ->
# Save the selection, so findInPlace can restore it.
@initialRange = getCurrentRange()
FindMode.query = rawQuery: ""
if options.returnToViewport
@scrollX = window.scrollX
@scrollY = window.scrollY
super extend options,
name: "find"
indicator: false
exitOnClick: true
exitOnEscape: true
# This prevents further Vimium commands launching before the find-mode HUD receives the focus.
# E.g. "/" followed quickly by "i" should not leave us in insert mode.
suppressAllKeyboardEvents: true
HUD.showFindMode this
exit: (event) ->
super()
handleEscapeForFindMode() if event
restoreSelection: ->
range = @initialRange
selection = getSelection()
selection.removeAllRanges()
selection.addRange range
findInPlace: (query) ->
# If requested, restore the scroll position (so that failed searches leave the scroll position unchanged).
@checkReturnToViewPort()
FindMode.updateQuery query
# Restore the selection. That way, we're always searching forward from the same place, so we find the right
# match as the user adds matching characters, or removes previously-matched characters. See #1434.
@restoreSelection()
query = if FindMode.query.isRegex then FindMode.getNextQueryFromRegexMatches(0) else FindMode.query.parsedQuery
FindMode.query.hasResults = FindMode.execute query
@updateQuery: (query) ->
@query.rawQuery = query
# the query can be treated differently (e.g. as a plain string versus regex depending on the presence of
# escape sequences. '\' is the escape character and needs to be escaped itself to be used as a normal
# character. here we grep for the relevant escape sequences.
@query.isRegex = Settings.get 'regexFindMode'
hasNoIgnoreCaseFlag = false
@query.parsedQuery = @query.rawQuery.replace /(\\{1,2})([rRI]?)/g, (match, slashes, flag) =>
return match if flag == "" or slashes.length != 1
switch (flag)
when "r"
@query.isRegex = true
when "R"
@query.isRegex = false
when "I"
hasNoIgnoreCaseFlag = true
""
# default to 'smartcase' mode, unless noIgnoreCase is explicitly specified
@query.ignoreCase = !hasNoIgnoreCaseFlag && !Utils.hasUpperCase(@query.parsedQuery)
regexPattern = if @query.isRegex
@query.parsedQuery
else
Utils.escapeRegexSpecialCharacters @query.parsedQuery
# If we are dealing with a regex, grep for all matches in the text, and then call window.find() on them
# sequentially so the browser handles the scrolling / text selection.
# If we are doing a basic plain string match, we still want to grep for matches of the string, so we can
# show a the number of results.
try
pattern = new RegExp regexPattern, "g#{if @query.ignoreCase then "i" else ""}"
catch error
return # If we catch a SyntaxError, assume the user is not done typing yet and return quietly.
# innerText will not return the text of hidden elements, and strip out tags while preserving newlines.
# NOTE(mrmr1993): innerText doesn't include the text contents of s and