Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show a quick preview of inspect result before pager launch #1040

Merged
merged 5 commits into from
Jan 21, 2025

Conversation

tompng
Copy link
Member

@tompng tompng commented Nov 29, 2024

IRB takes about 2 seconds to show the result of 200000.times.to_a.

irb(main):001> 200000.times.to_a

In this case, creating the full pretty_print content takes time. Fortunately, there is a way to get the first WINDOW_HEIGHT lines of the content quickly.
This pull request will change IRB to show the first page of the pretty_print result quickly even when pretty_print is slow.

preview_before_pager
First page is displayed 0.1sec after pressing ENTER key.
After two seconds, the whole pretty_print content is ready and pager launches (: on the bottom of the line indicates that).

newline_before_multiline_output

If the inspect takes more than 0.1 sec and result exceeds screen height, IRB will print the first page of the content immediately.
If the inspect of assignment expression result exceeds 1 line, generating rest of the content will be skipped.
In this case, output will be treated as multiline even if the un-generated whole inspect result does not contain newline.

Considering pretty_print prints SQL log in Rails console

In rails console, pretty printing ActiveRecord::Relation will print a SQL log.

Here is an example of the terminal buffer with terminal height = 5

irb(main):001> [User.where(name: 'foo'), User.where(name: 'bar')]
User Load(9.9ms) SELECT * from users where name="foo"
(and other SQL log while retrieving the first page)
=>
[#<User id: 1>,
 #<User id: 2>,
 #<User id: 3>
User Load(9.9ms) SELECT * from users where name="bar"
(and other SQL log while retrieving the rest of the pretty_print content)
=>
[#<User id: 1>,
 #<User id: 2>,
 #<User id: 3>
:

There is a duplicated output in the terminal buffer if pretty_print takes more than 0.1sec. This is the downside of this pull request.

It is possible to erase it by switching to alternate screen with escape sequence "\e[?1049h", "\e[?1049l".
This pull request does not do that because it will also erase the SQL log from the terminal buffer.

Another simple implementation

This works pretty well in most cases, but I didn't use it in this pull request.

IRB::Pager.page(retain_content: true) do |out|
  out.puts '=>'
  @context.inspect_last_value(out)
end

When evaluating a code like [100.times.to_a, User.where(condition)] or ClassThatStoresRelationAsInstanceVariable.new(User.where(condition)), SQL log messes up the pager output.

@tompng tompng force-pushed the fast_preview_before_pager branch 7 times, most recently from c09c633 to c4e9a3e Compare January 4, 2025 14:45
@tompng tompng marked this pull request as ready for review January 4, 2025 14:51
@tompng tompng changed the title [PoC] Show a quick preview of inspect result before pager launch Show a quick preview of inspect result before pager launch Jan 4, 2025
lib/irb/pager.rb Show resolved Hide resolved
lib/irb/pager.rb Outdated
def should_page?
IRB.conf[:USE_PAGER] && STDIN.tty? && (ENV.key?("TERM") && ENV["TERM"] != "dumb")
end

def page_with_preview(width, height, modifier_proc)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The block passing around modifier_proc, yield, and overflow_callback is quite confusing imo.
I think if we keep this method's logic inside Irb class, the implementation could be simpler as we don't need to package the context as block as much. WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if we move this part to Irb, the code is still complicated. We still need modifier_proc because it is used in two place.

+ modifier_proc = ->(content, multipage) do
+   content = content.chomp
+   content = "\n#{content}" if @context.newline_before_multiline_output? && (multipage || content.include?("\n"))
+   format(@context.return_format, content)
+ end
  ...
- yield out
+ @context.inspect_last_value(out)
  ...

Irb#output_value will be long, we will want to extract it into a method. This implementation is doing it.
I don't have a good idea to make it more simple.

The usage of this method is similar to Pager.page do |io| end and IMO it is not so bad.

Pager.page_with_preview(winwidth, winheight, modifier_proc) do |out|
  @context.inspect_last_value(out)
end

@st0012 st0012 added the enhancement New feature or request label Jan 8, 2025
@tompng tompng force-pushed the fast_preview_before_pager branch from c4e9a3e to 1054cba Compare January 10, 2025 16:33
lib/irb/pager.rb Show resolved Hide resolved
lib/irb/pager.rb Outdated
end
out = PageOverflowIO.new(width, height, overflow_callback)
yield out
content = modifier_proc.call(out.string, out.multipage?)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit lost on this part. Why do we need to call modifier proc in the overflow callback, and why here too?

Copy link
Member Author

@tompng tompng Jan 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modifier_proc formats the content. (Maybe we should rename it to formatter_proc)

The format is typically "(prefix)%s(postfix)".
We can do this instead of specifying modifier_proc if prefix and postfix is a constant, and the format is using %s.

Pager.page_with_preview(winwidth, winheight) do |out|
  out.write(prefix)
  @context.inspect_last_value(out)
  out.write(postfix)
end

But there are two difficulties:

  • Prefix will change depending on content.lines.size when newline_before_multiline_output is set to true
  • Format can contain %p or any other % format instead of %s

So this method will

  1. Calculates overflow without applying format
  2. Prints the formatted preview just after IRB knows that the content overflows screen
  3. Continue preparing the whole content
  4. Prints the full formatted content with pager

Step 2 calls modifier_proc with partial content.
Step 4 calls modifier_proc with full content.

lib/irb/pager.rb Show resolved Hide resolved
@st0012
Copy link
Member

st0012 commented Jan 11, 2025

I'm used to using j to move down the pager whenever it opens. Currently, I'd wait for the pager to open before scrolling down. It's not super convenient but easy to understand.

With this feature, I can get a preview fast, which is great 👍

But to users:

  1. It's unclear that they're seeing a preview, not the pager with the full result
  2. It's unclear that when IRB switches to full content pager

So if I keep my old habit to press j when a pager is rendered, it would feel like the pager renders very fast, then stuck in the middle, and then resume, like demonstrated below:

Screen.Recording.2025-01-11.at.14.36.07.mov

Is there a way we can make this a smoother experience or easier to understand through UI hints....etc.?

@tompng tompng force-pushed the fast_preview_before_pager branch from 1054cba to e614719 Compare January 12, 2025 12:48
@tompng
Copy link
Member Author

tompng commented Jan 12, 2025

Is there a way we can make this a smoother experience or easier to understand through UI hints....etc.?

That's a good point. I updated to show a message like this.

Before pager

=>
[0,
 1,
 2,
 3,
 4,
Inspecting...
█

After pager launch

=>
[0,
 1,
 2,
 3,
 4,
 5,
:█

Considering pretty_print prints something to stdout(ex: SQL log in rails console), cursor should be at the last line and the line should be blank.

lib/irb/pager.rb Outdated Show resolved Hide resolved
@tompng
Copy link
Member Author

tompng commented Jan 19, 2025

The last commit adds 0.1s delay before calling overflow_callback.
When pretty_print completes within 0.1s, preview is not shown. This will eliminate the downside of duplicated output (preview and pager output) in terminal buffer when pretty_print is not slow.

@tompng tompng force-pushed the fast_preview_before_pager branch from 05625fb to 1a7781a Compare January 19, 2025 18:37
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'reline'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: do you think pager should be part of Reline?
If we position Reline as Ruby's default terminal application utility library, it kinda makes sense for it to have a pager class too 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the basic pager should be in Reline, because it's mainly about command process names(less, more) and options(-R, -X).
The added preview part is IRB specific that we can't assume pretty_print not printing a log.
If we can assume no log while pritty-printing, there is an easy way: just stream the output to pager io.

Making a pure-ruby pager library (key handling, rendering, scrolling, etc): I think it is very interesting.

Copy link
Member

@st0012 st0012 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

lib/irb/pager.rb Outdated Show resolved Hide resolved
@tompng tompng force-pushed the fast_preview_before_pager branch from 1a7781a to 4cb7730 Compare January 21, 2025 13:38
@tompng tompng merged commit 03c3658 into ruby:master Jan 21, 2025
34 checks passed
@tompng tompng deleted the fast_preview_before_pager branch January 21, 2025 13:58
tompng added a commit to tompng/ruby that referenced this pull request Jan 22, 2025
launch
(ruby/irb#1040)

* Quickly show inspect preview even if pretty_print takes too much time

* Show a message "Inspecting..." while generating pretty_print content

* Update inspecting message

Co-authored-by: Stan Lo <[email protected]>

* Update rendering test for preparing inspect message

* Don't show preview if pretty_print does not take time

---------

ruby/irb@03c36586e6

Co-authored-by: Stan Lo <[email protected]>
tompng added a commit to tompng/ruby that referenced this pull request Jan 22, 2025
launch
(ruby/irb#1040)

* Quickly show inspect preview even if pretty_print takes too much time

* Show a message "Inspecting..." while generating pretty_print content

* Update inspecting message

Co-authored-by: Stan Lo <[email protected]>

* Update rendering test for preparing inspect message

* Don't show preview if pretty_print does not take time

---------

ruby/irb@03c36586e6

Co-authored-by: Stan Lo <[email protected]>
tompng added a commit to ruby/ruby that referenced this pull request Jan 22, 2025
launch
(ruby/irb#1040)

* Quickly show inspect preview even if pretty_print takes too much time

* Show a message "Inspecting..." while generating pretty_print content

* Update inspecting message

Co-authored-by: Stan Lo <[email protected]>

* Update rendering test for preparing inspect message

* Don't show preview if pretty_print does not take time

---------

ruby/irb@03c36586e6

Co-authored-by: Stan Lo <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Development

Successfully merging this pull request may close these issues.

2 participants