# frozen_string_literal: true module Textbringer module Commands ISEARCH_MODE_MAP = Keymap.new (?\x20..?\x7e).each do |c| ISEARCH_MODE_MAP.define_key(c, :isearch_printing_char) end ISEARCH_MODE_MAP.define_key(?\t, :isearch_printing_char) ISEARCH_MODE_MAP.handle_undefined_key do |key| if key.is_a?(String) && /[\0-\x7f]/ !~ key :isearch_printing_char else nil end end ISEARCH_MODE_MAP.define_key(:backspace, :isearch_delete_char) ISEARCH_MODE_MAP.define_key(?\C-h, :isearch_delete_char) ISEARCH_MODE_MAP.define_key(?\C-s, :isearch_repeat_forward) ISEARCH_MODE_MAP.define_key(?\C-r, :isearch_repeat_backward) ISEARCH_MODE_MAP.define_key(?\C-w, :isearch_yank_word_or_char) ISEARCH_MODE_MAP.define_key(?\C-m, :isearch_exit) ISEARCH_MODE_MAP.define_key(?\C-g, :isearch_abort) ISEARCH_STATUS = { forward: true, string: "", last_string: "", start: 0, last_pos: 0, recursive_edit: false } define_command(:isearch_forward, doc: "Incrementally search forward.") do |**options| isearch_mode(true, **options) end define_command(:isearch_backward, doc: "Incrementally search backward.") do |**options| isearch_mode(false, **options) end def isearch_mode(forward, recursive_edit: false) ISEARCH_STATUS[:forward] = forward ISEARCH_STATUS[:string] = String.new ISEARCH_STATUS[:recursive_edit] = recursive_edit Controller.current.overriding_map = ISEARCH_MODE_MAP run_hooks(:isearch_mode_hook) add_hook(:pre_command_hook, :isearch_pre_command_hook) ISEARCH_STATUS[:start] = ISEARCH_STATUS[:last_pos] = Buffer.current.point if Buffer.current != Buffer.minibuffer message(isearch_prompt, log: false) end if recursive_edit recursive_edit() end end def isearch_prompt if ISEARCH_STATUS[:forward] "I-search: " else "I-search backward: " end end def isearch_pre_command_hook if /\Aisearch_/ !~ Controller.current.this_command isearch_done end end def isearch_done Buffer.current.delete_visible_mark Controller.current.overriding_map = nil remove_hook(:pre_command_hook, :isearch_pre_command_hook) ISEARCH_STATUS[:last_string] = ISEARCH_STATUS[:string] if ISEARCH_STATUS[:recursive_edit] exit_recursive_edit end end define_command(:isearch_exit, doc: "Exit incremental search.") do isearch_done end define_command(:isearch_abort, doc: "Abort incremental search.") do goto_char(Buffer.current[:isearch_start]) isearch_done raise Quit end define_command(:isearch_printing_char, doc: <<~EOD) do Add the typed character to the search string and search. EOD c = Controller.current.last_key ISEARCH_STATUS[:string].concat(c) isearch_search end define_command(:isearch_delete_char, doc: <<~EOD) do Delete the last character from the search string and search. EOD ISEARCH_STATUS[:string].chop! isearch_search end define_command(:isearch_yank_word_or_char, doc: <<~EOD) do Yank next word or character onto the end of the search string. EOD buffer = Buffer.current if buffer.looking_at?(/(\p{Letter}|\p{Number})+|\s+|./) ISEARCH_STATUS[:string].concat(buffer.match_string(0)) isearch_search end end def isearch_search forward = ISEARCH_STATUS[:forward] options = if /\A[A-Z]/.match?(ISEARCH_STATUS[:string]) nil else Regexp::IGNORECASE end re = Regexp.new(Regexp.quote(ISEARCH_STATUS[:string]), options) last_pos = ISEARCH_STATUS[:last_pos] offset = forward ? last_pos : last_pos - ISEARCH_STATUS[:string].bytesize if Buffer.current.byteindex(forward, re, offset) if Buffer.current != Buffer.minibuffer message(isearch_prompt + ISEARCH_STATUS[:string], log: false) end Buffer.current.set_visible_mark(forward ? match_beginning(0) : match_end(0)) goto_char(forward ? match_end(0) : match_beginning(0)) else if Buffer.current != Buffer.minibuffer message("Failing " + isearch_prompt + ISEARCH_STATUS[:string], log: false) end end end def isearch_repeat_forward isearch_repeat(true) end def isearch_repeat_backward isearch_repeat(false) end def isearch_repeat(forward) ISEARCH_STATUS[:forward] = forward ISEARCH_STATUS[:last_pos] = Buffer.current.point if ISEARCH_STATUS[:string].empty? ISEARCH_STATUS[:string] = ISEARCH_STATUS[:last_string] end isearch_search end end end