はじめに
バックエンドエンジニアのやまぴーです。
pryというツールをマネしながら対話型デバッグツールを作成していた 前回の記事の続きになります。
シンタックスハイライト
どうも味気ない印象なので、見栄えを良くしていきたいです。
エスケープシーケンスで色やスタイルを付与したいと思います。
試行錯誤の末、テストが最終的にこんな感じになりました。
{ '42' => "\e[1;34m42", '6.7' => "\e[1;35m6.7", '"string"' => "\e[31m\"\e[31mstring\e[31m\"", 'class' => "\e[32mclass", 'Object' => "\e[1;34;4mObject", }.each do |input, expected| assert(expected, Pry::SyntaxHighlighted.new(input).to_s) end
前回と同様、左が入力値、右が期待値になります。
"string"
に対するエスケープシーケンスが冗長なことに気付かれるかもしれません。
これは今回、字句解析器を利用して抽出されたトークンひとつひとつに対して付与した為です。
具体的にはRipper::Filterを使用しました。実装です。
class Ripper::Filter (Ruby 3.1 リファレンスマニュアル)
module Pry class SyntaxHighlighted def initialize(source) @source = source.to_s end def to_s Class.new(Ripper::Filter) do def on_default(event, tok, f) escape_seq = case event when :on_int "\e[1;34m" when :on_float "\e[1;35m" when :on_tstring_beg, :on_tstring_content, :on_tstring_end "\e[31m" when :on_kw "\e[32m" when :on_const "\e[1;34;4m" else "\e[m" end f << escape_seq << tok end end. new(@source). parse('') end end end
Class.new(Ripper::Filter)
でRipper::Filter
を親とするクラスを作成できます。
そして、on_default
メソッドを定義します。
字句解析でトークンが抽出される度にデフォルトで呼ばれるメソッドです。
メソッドの引数には
が渡されます。
インスタンス化した後に空文字とともにparseを呼びますが、これが「毎回の処理の結果」の初期値になります。
どんな感じになるか出力して確認していたのがこちらです。
これを既に実装した分に反映していきます。
まずは入力の評価結果の文字列を装飾してみます。
def pry loop do Pry::Completion.enable(self) puts Pry::SourceNavigation.new(self).then { |s| "\n%s\n" % s } input = Readline.readline('> ') Pry::Completion.disable - puts Pry::Output.new(self, input) + puts Pry::SyntaxHighlighted.new(Pry::Output.new(self, input)) end end
ついでにユーザが入力した文字も装飾してみます。 入力中にリアルタイムで反映させるのは難しそうですが 読み込んだ後なら簡単な方法があります。
def pry
loop do
Pry::Completion.enable(self)
puts Pry::SourceNavigation.new(self).then { |s| "\n%s\n" % s }
input = Readline.readline('> ')
Pry::Completion.disable
+ puts "\e[1A\e[2C#{Pry::SyntaxHighlighted.new(input)}"
puts Pry::SyntaxHighlighted.new(Pry::Output.new(self, input))
end
end
REPLのReadした直後にエスケープシーケンス \e[1A\e[2C
(1つ上、2つ右へ) でカーソルをプロンプトの直後まで移動させます。
そして装飾した文字列で元々ユーザが入力していた文字列を上書くことにしています。
pryを使う時と同じ様にメソッド定義の途中で改行したらsyntax errorになってしまい、何度か撮り直しました ^^;
何らかの条件で続きの入力を促す様にするべきですが、どうするべきか。。興味深いですね。
さておき、同じ要領でブレークポイント周辺のソース表示も装飾してみました。
かなり、いいんじゃないでしょうか?!
このキャプチャだけなら本家と遜色ない様な気がします!!
おわりに
今回は以上になります。
少しのコードで見た目が大分華やかになったのが嬉しかったです!
(もちろん、Ripperのおかげではあるのですが)
機能的にはまだまだなので、また弄るかもしれません。
最後に、ここまでのコードを貼っておきます。※テストコードは抜きました
require 'pp' require 'readline' require 'ripper' module Pry class SyntaxHighlighted def initialize(source) @source = source.to_s end def to_s Class.new(Ripper::Filter) do def on_default(event, tok, f) escape_seq = case event when :on_int "\e[1;34m" when :on_float "\e[1;35m" when :on_tstring_beg, :on_tstring_content, :on_tstring_end "\e[31m" when :on_kw "\e[32m" when :on_const "\e[1;34;4m" else "\e[m" end f << escape_seq << tok end end. new(@source). parse('') end end end module Pry module Completion class Candidates def initialize(binding, input) @binding = binding @input = input end def to_a @binding. local_variables. concat(@binding.receiver.methods). concat(@binding.receiver.class.constants). map(&:to_s). select { |s| s.start_with?(@input) } end end end end module Pry module Completion def self.enable(binding) @@old_completion_proc = Readline.completion_proc Readline.completion_proc = ->(i) { Candidates.new(binding, i).to_a } end def self.disable Readline.completion_proc = @@old_completion_proc end end end module Pry class SourceNavigation DISTANCE = 5 def initialize(binding) @binding = binding end def to_s filepath, lineno = @binding.source_location lines = Pry::SyntaxHighlighted.new(File.read(filepath)).to_s.lines offset = lineno - 1 from = [offset - DISTANCE, 0].max to = [offset + DISTANCE, lines.size].min prependings = (1..lines.count).map do |n| arrow = n == lineno ? '=>' : ' ' no = format("%#{to.digits.size}d", n) "#{arrow} #{Pry::SyntaxHighlighted.new("#{no}:")} " end prependings.zip(lines).slice(from..to).join end end end module Pry class Output def initialize(binding, input) @binding = binding @input = input end def to_s @binding.eval(@input).pretty_inspect end end end class Binding def pry loop do Pry::Completion.enable(self) puts Pry::SourceNavigation.new(self).then { |s| "\n%s\n" % s } input = Readline.readline('> ') Pry::Completion.disable puts "\e[1A\e[2C#{Pry::SyntaxHighlighted.new(input)}" puts Pry::SyntaxHighlighted.new(Pry::Output.new(self, input)) end end end a = 1 b = 2 binding.pry