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

Feature request: Toggle line numbering for multiline cells in iPython shell #13965

Closed
cohml opened this issue Mar 7, 2023 · 7 comments
Closed
Milestone

Comments

@cohml
Copy link

cohml commented Mar 7, 2023

With Vi mode enabled, ngg will move your cursor to the nth line in the cell.

This could be very useful for longer cells, but without line numbers visible, it's difficult to move precisely.

So the ability to toggle dynamic line numbering would be super helpful. For example, like this:

1 | In [5]: """
2 |    ...: This
3 |    ...: is
4 |    ...: a
5 |    ...: multiline
6 |    ...: string
7 |    ...: """

Bonus points for a relative numbering as well, but one thing at a time ;)

@cohml
Copy link
Author

cohml commented Aug 1, 2023

@Carreau @krassowski @shaperilio Pinging you guys as the most active contributors to this project over the past year.

I'd like to start working on this. Or, if basically not feasible, I'd like to be told so by a core dev so I can stop dreaming about it. Got any pointers?

As a starting point, I did fool around on my own a while back by trying to add automatic numbering to the prompt tokens. Note that this would only cover the piece about displaying the numbers, not about toggling them on and off. Anyway, here's what happened:

For Line 1, it was simple enough: Just prepend a hard-coded 1 to the in token string of in_prompt_tokens. That worked.

For Lines 2+, this strategy proved much harder to implement. This is because continuation_prompt_tokens (which determine the string for Lines 2+) seems to get set only once, rather than updated anew for each new line. Hence, the result ended up something like this, with "2" just being fixed:

1 | In [5]: """
2 |    ...: This
2 |    ...: is
2 |    ...: a
2 |    ...: multiline
2 |    ...: string
2 |    ...: """

Can you think of an altogether more promising approach that either

  1. allows continuation_prompt_tokens to update, or
  2. doesn't involve modifying the prompt tokens at all?

Or does the challenge described above with the prompt tokens being static indicate that dynamic line numbering is not possible in iPython?

@Carreau
Copy link
Member

Carreau commented Aug 7, 2023

Thanks for your patience, I don't have much time on IPython and tend to miss requests.

We will need to modify tokens, but it's ok as you can define you own class for prompts, so for now we can focus on modifying the right place in IPython so that the prompt continuatin get access to their line number.

Here is a crud attempt, the only necessary thing is to pass lineno to continuation_prompt_tokens: continuation_prompt_tokens(width, lno=lineno), which is technically backward incompatible as people may have custom Prompts, but I think it's ok to do it. We could inspect the signature to be safe.

$ git diff -U0
diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py
index 75cf25ea6..e60f46e95 100644
--- a/IPython/terminal/interactiveshell.py
+++ b/IPython/terminal/interactiveshell.py
@@ -756 +756 @@ def get_message():
-                    self.prompts.continuation_prompt_tokens(width)
+                    self.prompts.continuation_prompt_tokens(width, lno=lineno)
diff --git a/IPython/terminal/prompts.py b/IPython/terminal/prompts.py
index 3f5c07b98..38d531ebf 100644
--- a/IPython/terminal/prompts.py
+++ b/IPython/terminal/prompts.py
@@ -31,2 +31,3 @@ def in_prompt_tokens(self):
-            (Token.Prompt, self.vi_mode() ),
-            (Token.Prompt, 'In ['),
+            (Token.Prompt, "1|"),
+            (Token.Prompt, self.vi_mode()),
+            (Token.Prompt, "In ["),
@@ -40 +41,8 @@ def _width(self):
-    def continuation_prompt_tokens(self, width=None):
+    def continuation_prompt_tokens(self, width=None, *, lno=None):
+        """
+        lno is 0-indexed
+        """
+
+        current_line = (
+            self.shell.pt_app.default_buffer.document.cursor_position_row or 0
+        )
@@ -44 +52,4 @@ def continuation_prompt_tokens(self, width=None):
-            (Token.Prompt, (' ' * (width - 5)) + '...: '),
+            (
+                Token.Prompt,
+                str(lno - current_line) + "|" + (" " * (width - 2)) + "...: ",
+            ),

And with a bit of efforts I think relative number works, except for in_prompt_tokens that might not be redrawn...

Untitled.mov

I'll let you get inspiration from the above and send a PR :-)

@cohml
Copy link
Author

cohml commented Aug 7, 2023

Amazing! I'll get cracking on this shortly. Thanks!

I'll start by working on absolute numbering and submit a PR for that.

Once absolute numbering is implemented, I'll step back to think about how to get relative numbering working. This will likely be a completely separate prompt class and either some magic or config option to toggle between absolute and relative. Unless there is some way to configure a generic Numbered prompt type using options. Let me know.

@cohml
Copy link
Author

cohml commented Aug 7, 2023

Hold up. Upon closer inspection, I see that your "crude attempt" actually modifies the Prompt base class. Now I understand your comment about backwards compatibility.

Is that what you'd recommend, adding this capability to the base class rather than creating a whole new child class for line numbering? If so, I will pursue it.

I would of course love for this to become a core feature of iPython rather than bolted on, ideally as something toggleable via ipython_config.py. But I also don't want to rock the boat too much on the ground floor and risk breaking everything without your go-ahead.

@Carreau
Copy link
Member

Carreau commented Aug 8, 2023

Many of those modification are not needed.

You can use the following in your config files:

from IPython.terminal.prompts import Prompts, Token


class MyPrompts(Prompts):
    def in_prompt_tokens(self):
        sup = super().in_prompt_tokens()
        return [(Token.Prompt, "!")] + sup


c.TerminalInteractiveShell.prompts_class = MyPrompts

This will prepend a ! to your prompts.

Technically the only modification we need in the core is

--- a/IPython/terminal/interactiveshell.py
+++ b/IPython/terminal/interactiveshell.py
@@ -746,16 +746,24 @@ def get_message():
             # work around this.
             get_message = get_message()

+        cb_sig = inspect.signature(self.prompts.continuation_prompt_tokens)
+        # maybe check for **kwargs as well ? 
+        if "lineno" in sig.parameters:
+            continuation_prompt_cb = lambda width, lineno, is_soft_wrap: PygmentsTokens(
+                self.prompts.continuation_prompt_tokens(width, linen=lineno)
+            )
+        else:
+            # maybe emit deprecation warning ?
+            continuation_prompt_cb = lambda width, lineno, is_soft_wrap: PygmentsTokens(
+                self.prompts.continuation_prompt_tokens(width)
+            )
+
         options = {
             "complete_in_thread": False,
             "lexer": IPythonPTLexer(),
             "reserve_space_for_menu": self.space_for_menu,
             "message": get_message,
-            "prompt_continuation": (
-                lambda width, lineno, is_soft_wrap: PygmentsTokens(
-                    self.prompts.continuation_prompt_tokens(width)
-                )
-            ),
+            "prompt_continuation": continuation_prompt_cb,

that will check that lineno is a parameter and if not don't pass it to not break backward compatibility.

This is really the minimum we need in the core. If we are ok breaking compat we could not check with inspect and also add:

--- a/IPython/terminal/prompts.py
+++ b/IPython/terminal/prompts.py
@@ -37,11 +38,21 @@ def in_prompt_tokens(self):
     def _width(self):
         return fragment_list_width(self.in_prompt_tokens())

-    def continuation_prompt_tokens(self, width=None):
+    def continuation_prompt_tokens(self, width=None, *, lineno=None):

Let's try to get just that in, it should let you do all the customization you want from your config file.

I don't have problem breaking compatibility and always passing lineno ( and softwrap while we are at it). I doubt there are any prompt customisation.

Once this is done we can work on adding line numbers and making it togglelable.

@cohml
Copy link
Author

cohml commented Aug 8, 2023

Great! I'll get to work on this when I'm able, submitting PRs in stages if and as we decide to introduce additional complexity. Please assign to me.

By the way, I personally have made several custom prompts, though I don't often use them. I have them toggleable via magic functions. For example, there is one with 4 spaces prepended to every line for easy copy-and-paste to markdown, and another that displays all kinds of info like environment, working directory, and git branch:

image

But line numbering > these prompts, personally. So I'm happy to update them or say goodbye.

Carreau added a commit that referenced this issue Dec 17, 2023
This PR adds line numbers to the default prompt (#13965).

You can now display absolute, relative of both line number in the emacs and vi prompt by using the `TerminalInteractiveShell.prompt_line_number_format` option the at takes string to format with both
`line` (1 base int) and `rel_line` (int)`. For example:

`c.TerminalInteractiveShell.prompt_line_number_format='{line: 4d}/{rel_line:+03d} | '`
@cohml
Copy link
Author

cohml commented Jan 12, 2024

Resolved with release of 8.19

@cohml cohml closed this as completed Jan 12, 2024
@Carreau Carreau added this to the 8.19 milestone Jan 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants