prompt-toolkitで検索ツールバーの実装と、インクリメンタル検索の実装した - Python
いろいろやって、できたから、まとめてみる
ソースはGistにあげた
https://gist.github.com/tamago324/806aac08455412d06b48b7022f5b660f
2018-11-16 00:21:49 編集:カレントバッファで検索結果の移動をするように変更
単純な検索機能を実装する
まずは、単純な検索を実装する
/
で検索の文字入力開始?
で検索の文字入力開始(逆順に検索)Enter
で検索実行c-c
で検索入力中止- 検索文字が0文字になったら、検索入力中止
n
で次の検索位置N
で逆順に次の検索位置
今は、ReadOnlyの想定で作ったから、nとかNとか入力できないのは気にしない
from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document from prompt_toolkit.filters import Condition, is_searching from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings import search from prompt_toolkit.layout.containers import HSplit, Window from prompt_toolkit.layout.controls import BufferControl from prompt_toolkit.layout.layout import Layout from prompt_toolkit.widgets.toolbars import SearchToolbar text = "\n".join([str(i) for i in range(100)]) # vi_mode=Trueとすると`/`と`?`で表示されるようになる search_toolbar = SearchToolbar(vi_mode=True) control = BufferControl( Buffer(document=Document(text), read_only=True), search_buffer_control=search_toolbar.control, ) # ウィンドウ body = HSplit([Window(control), search_toolbar]) # キーバインディング kb = KeyBindings() @kb.add("q") def _(event): event.app.exit() # 検索ツールバーの文字が空になったかどうか @Condition def search_buffer_is_empty(): " Returns True when the search buffer is empty. " return get_app().current_buffer.text == "" kb.add("/")(search.start_forward_incremental_search) kb.add("?")(search.start_reverse_incremental_search) kb.add("enter", filter=is_searching)(search.accept_search) kb.add("c-c")(search.abort_search) kb.add("backspace", filter=search_buffer_is_empty)(search.abort_search) @kb.add("n", filter=~is_searching) def _(event): search_state = get_app().current_search_state current_buffer = get_app().current_buffer cursor_position = current_buffer.get_search_position( search_state, include_current_position=False ) current_buffer.cursor_position = cursor_position @kb.add("N", filter=~is_searching) def _(event): search_state = get_app().current_search_state current_buffer = get_app().current_buffer cursor_position = current_buffer.get_search_position( ~search_state, include_current_position=False ) current_buffer.cursor_position = cursor_position app = Application(layout=Layout(body), key_bindings=kb, full_screen=True) app.run()
ptkのソースのprompt_toolkit.key_binging.key_bingings.vi.load_vi_search_bindings
とか、サンプルとかがすごい参考になった
実装手順
SearchToolBar
のインスタンスつくって、BufferControl
のsearch_buffer_control
に作ったインスタンスのcontrol
を渡す。(検索ツールバーの紐付け?)
search_toolbar = SearchToolbar(vi_mode=True) control = BufferControl( Buffer(document=Document(text), read_only=True), search_buffer_control=search_toolbar.control, )
/
で前方検索の開始
kb.add("/")(search.start_forward_incremental_search)
enter
で検索実行
kb.add("enter", filter=is_searching)(search.accept_search)
n
とN
で次の検索結果に移動
@kb.add("n", filter=~is_searching) def _(event): search_state = get_app().current_search_state current_buffer = get_app().current_buffer cursor_position = current_buffer.get_search_position( search_state, include_current_position=False ) current_buffer.cursor_position = cursor_position @kb.add("N", filter=~is_searching) def _(event): search_state = get_app().current_search_state current_buffer = get_app().current_buffer cursor_position = current_buffer.get_search_position( ~search_state, include_current_position=False ) current_buffer.cursor_position = cursor_position
2018-11-16 00:21:49 編集
current_buffer = get_app().current_buffer
にすることで、カレントバッファで検索結果の移動ができるようになる
これで一通りの検索ができるようになった!
実装するときにつまづいたこと
次の検索結果への移動の実装方法がわからなかった
これは、サンプルのtext_editor
を見たら書いてあったから、参考にした
def do_find_next(): search_state = get_app().current_search_state cursor_position = text_field.buffer.get_search_position( search_state, include_current_position=False) text_field.buffer.cursor_position = cursor_position
search_state
を反転させれば、逆に検索もできることをどこかで知って、それも使った。
filters.is_searching
がなんなのかよくわからなかった
いろいろ調べた結果、検索中で、検索対象のコントロールがカレントコントロールの場合、Trueになるってことがわかった
prompt_toolkit.filters.app.is_searching
に以下のように書いてあった
@Condition def is_searching(): " When we are searching. " app = get_app() return app.layout.is_searching
prompt_toolkit.layout.layout.Layout.is_searching
を見てみる
@property def is_searching(self): " True if we are searching right now. " return self.current_control in self.search_links
prompt_toolkit.layout.layout.Layout.search_links
とは
class Layout(object): def __init__(self, container, focused_element=None): ... # Map search BufferControl back to the original BufferControl. # This is used to keep track of when exactly we are searching, and for # applying the search. # When a link exists in this dictionary, that means the search is # currently active. self.search_links = {} # search_buffer_control -> original buffer control.
Map Search BufferControlを元のBufferControlに戻します。 これは、正確にいつ検索しているかを追跡し、検索を適用するために使用されます。 この辞書にリンクが存在する場合、検索が現在アクティブであることを意味します。
検索している時には、get_app().layout.search_links
に検索対象のBufferControlが格納されるから、is_searching
ではin
で調べる
search_links
を調べてみた
こんな感じのdictになっている
- Key:
SearchBufferControl
で、検索文字入力用のSearchBufferControl
- Value:
BufferControl
で、検索対象のBufferControl
検索文字列入力中は、current_control
はSearchBufferControl
になる。search_links[current_control]
とすると、それに紐づく検索対象のBufferControl
が返される
そのため、is_searching
は、検索文字列を入力しているかどうかということ。(検索ツールバーが表示されているかどうか)
n
とN
が検索文字列入力中に打てなくなってしまった
@kb.add('n')
みたいに書いちゃうと、検索するときに、n
が打てなくなってしまった。。。そのため、以下のようにする
@kb.add('n', filter=~is_searching)
@kb.add('N', filter=~is_searching)
こうすることで、検索文字を入力していないときのみ、n
とN
のKeyBindingが有効になる
インクリメンタル検索を実装する
検索文字入力中にもハイライト(インクリメンタル検索, incremental search)したかったから、やってみた
from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document from prompt_toolkit.filters import Condition, is_searching from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings import search from prompt_toolkit.layout.containers import HSplit, Window from prompt_toolkit.layout.controls import BufferControl from prompt_toolkit.layout.layout import Layout from prompt_toolkit.styles import Style from prompt_toolkit.widgets.toolbars import SearchToolbar from prompt_toolkit.layout.processors import ( ConditionalProcessor, DisplayMultipleCursors, HighlightIncrementalSearchProcessor, HighlightSearchProcessor, HighlightSelectionProcessor, ) text = "\n".join([str(i) for i in range(100)]) all_input_processors = [ # 検索モードではないときだけハイライト ConditionalProcessor(HighlightSearchProcessor(), ~is_searching), HighlightIncrementalSearchProcessor(), HighlightSelectionProcessor(), DisplayMultipleCursors(), ] # vi_mode=Trueとすると`/`と`?`で表示される様になる search_toolbar = SearchToolbar(vi_mode=True) control = BufferControl( Buffer(document=Document(text), read_only=True), search_buffer_control=search_toolbar.control, preview_search=True, include_default_input_processors=False, input_processors=all_input_processors, ) # ウィンドウ body = HSplit([Window(control), search_toolbar]) # キーバインディング kb = KeyBindings() @kb.add("q") def _(event): event.app.exit() @Condition def search_buffer_is_empty(): " Returns True when the search buffer is empty. " return get_app().current_buffer.text == "" kb.add("/")(search.start_forward_incremental_search) kb.add("?")(search.start_reverse_incremental_search) kb.add("enter", filter=is_searching)(search.accept_search) kb.add("c-c")(search.abort_search) kb.add("backspace", filter=search_buffer_is_empty)(search.abort_search) @kb.add("n", filter=~is_searching) def _(event): search_state = get_app().current_search_state current_buffer = get_app().current_buffer cursor_position = current_buffer.get_search_position( search_state, include_current_position=False ) current_buffer.cursor_position = cursor_position @kb.add("N", filter=~is_searching) def _(event): search_state = get_app().current_search_state current_buffer = get_app().current_buffer cursor_position = current_buffer.get_search_position( ~search_state, include_current_position=False ) current_buffer.cursor_position = cursor_position style = Style([("incsearch", "fg:ansibrightyellow reverse")]) app = Application(layout=Layout(body), key_bindings=kb, full_screen=True, style=style) app.run()
シンプルな検索との違いは
Processor
のリストを作っているBufferControl
のコンストラクタへの引数を追加- IncSearch用のスタイルを用意
Processor
のリストを作る
all_input_processors = [
# 検索モードではないときだけハイライト
ConditionalProcessor(HighlightSearchProcessor(), ~is_searching),
HighlightIncrementalSearchProcessor(),
HighlightSelectionProcessor(),
DisplayMultipleCursors(),
]
何も指定しなくても、デフォルトでこの4つは生成されるが、HighlightSearchProcessor
が、デフォルトのままだと検索文字列入力中にもハイライトされてしまうため、(1回目の検索結果が2回目のIncSearchのハイライトの邪魔になる)ConditionalProcessor
で検索中はハイライトしないようにした
BufferControl
のコンストラクタへの引数を追加
以下の3つを追加した
preview_search=True
- IncSearchの有効化
include_default_input_processors=False
input_proecssors
で渡したPrcessor
のみを有効にしたいため、False
input_processors=all_input_processors
- 適用する
Processor
のリストを渡す
- 適用する
IncSearch用のスタイルを用意
style = Style([("incsearch", "fg:ansibrightyellow reverse")])
IncSearch
中には、style
のincsearch
というclass
がつくため、それ用のスタイルを定義して、Application
に渡しておく。
ドキュメントに書いてあった
増分検索を強調表示するために使用される検索条件を強調表示します。 スタイルクラス 'incsearch'がコンテンツに適用されます。
重要:これは、BufferControlにpreview_search = Trueフラグを設定する必要があります。 そうしないと、検索中にカーソル位置が検索一致に設定されず、何も起こりません。
実装してみて
ドキュメントとソースをたくさん読んでて、少しずつわかっていくのがなんか楽しかった。あと、動いたときの嬉しさ半端ない