hoangKnLai / vscode-ipython

VSCode Extension integrating Editor with IPython console.
MIT License
18 stars 5 forks source link

An idea and feature request maybe #26

Open AmerM137 opened 1 year ago

AmerM137 commented 1 year ago

@hoangKnLai Thanks for the latest updates in 2023.8.0, I think this extension is much better than some of the other attempts I've seen to integrate IPython in VS Code. I have not used Spyder in two weeks! Sad but not sad since it feels great to be coding primarily in VS Code.

Here is an idea that I've been thinking about. I am not a big fan of the %load command. The "%load filename.py" string gets reprinted before the code is shown, and also you have to add multiple newlines after it to get ipython to execute. Digging through the code for %load, it looks like it sets the next prompt to the loaded code using shell.set_next_input, but it doesn't execute the code.

"lib\site-packages\ipython\core\magics\code.py" line 316, definition of "%load", see comments in BIG LETTERS

        opts,args = self.parse_options(arg_s,'yns:r:')
        search_ns = 'n' in opts
        # ******************CALL TO LOAD FILE CONTENTS*************************
        contents = self.shell.find_user_code(args, search_ns=search_ns)

        if 's' in opts:
            try:
                blocks, not_found = extract_symbols(contents, opts['s'])
            except SyntaxError:
                # non python code
                error("Unable to parse the input as valid Python code")
                return

            if len(not_found) == 1:
                warn('The symbol `%s` was not found' % not_found[0])
            elif len(not_found) > 1:
                warn('The symbols %s were not found' % get_text_list(not_found,
                                                                     wrap_item_with='`')
                )

            contents = '\n'.join(blocks)

        if 'r' in opts:
            ranges = opts['r'].replace(',', ' ')
            lines = contents.split('\n')
            slices = extract_code_ranges(ranges)
            contents = [lines[slice(*slc)] for slc in slices]
            contents = '\n'.join(strip_initial_indent(chain.from_iterable(contents)))

        l = len(contents)

        # 200 000 is ~ 2500 full 80 character lines
        # so in average, more than 5000 lines
        if l > 200000 and 'y' not in opts:
            try:
                ans = self.shell.ask_yes_no(("The text you're trying to load seems pretty big"\
                " (%d characters). Continue (y/[N]) ?" % l), default='n' )
            except StdinNotImplementedError:
                #assume yes if raw input not implemented
                ans = True

            if ans is False :
                print('Operation cancelled.')
                return

        contents = "# %load {}\n".format(arg_s) + contents
        # ******************CALL TO SET NEXT PROMPT TO CONTENTS*************************
        self.shell.set_next_input(contents, replace=True)

Ideally, you want a command that opens the file, reads the code, (optionally) prints it to stdout and executes the code. The %load command is mostly meant to load code from a file and leaving it up to the user to do whatever he needs after, so it doesn't execute the code (hence the multiple newlines needed I think).

There is another built-in magic command named "%paste" where you can just paste code directly from the clipboard and IPython will immediately execute it. However, the only issue with this is going through the clipboard. I like the write-to-file-then-load-code workflow better than clipboard and it's very fast on SSD, especially if running 50+ lines. So what we need is another magic command that loads from the file instead of the clipboard, and then executes the code immediately just like %paste does.

There isn't a built-in magic command that does this except "%run -i" but it feels like it has to check many things before it runs the code, and I am not sure if it has any side effects (it's one of the longest magic commands for sure with what looks like monkey patching the user's namespace). Given IPython lets you make your own magic command, I made one named "fpaste" that does just that. Here is the implementation.

# ref --> https://ipython.readthedocs.io/en/stable/config/custommagics.html
# ref --> see "%paste" magic definition by typing "%paste??" in IPython
# need to put this in the "startup" folder under ~/.ipython/profile_default
# "fpaste.py"
from IPython.core.magic import Magics, magics_class, line_magic
@magics_class
class MyMagics(Magics):
    @line_magic
    def fpaste(self, line):
        import sys
        from pathlib import Path
        # open file and read code written from VSC
        contents = Path(command_file_path).read_text() 
        # write lines to terminal with syntax highlighting, can be skipped entirely if we don't want to show code
        sys.stdout.write(self.shell.pycolorize(contents)) 
        # execute code in IPython, is independent of whether or not we write code to stdout
        self.shell.run_cell(contents, store_history=True)

get_ipython().register_magics(MyMagics)

This auto-loads the new "%fpaste" magic command whenever IPython is launched. Then in the extension.js in the VS Code extension folder, I changed the %load command to %fpaste command instead. The result is in my opinion a cleaner way to run the code! I hardcoded my command_file_path, that's why you don't see the filename after %fpaste.

image image

Now, I understand this requires a little tinkering with IPython since we need to make our own custom magic but I think it's both simple and worth it (can include it as part of the docs), so maybe this could be an opt-in thing only, or better, we can submit a pull request to IPython-main and make this one of the built-in magics in the next IPython release.

In addition, a bonus to this is that the "%fpaste" command requires sending only 1 newline. Imo, the less new lines we have to send the better since we don't have a proper IPython API in VS Code.

I am interested in your thoughts on this!

hoangKnLai commented 1 year ago

I have not used Spyder in two weeks! Sad but not sad since it feels great to be coding primarily in VS Code.

Same here. Spyder was also my transition from Matlab over Python and currently to VSCode. I will always have a soft spot for it. Many features on this extension are inspired by Spyder.

I am interested in your thoughts on this!

I have not thought about adding a magic command. It is an interesting idea and I think it is worth a pull request to the IPython team to consider. Though a better option is likely an addon to %run mode. Something like %load -r but %run -r 10-20 file.py instead.

Imo, the less new lines we have to send the better since we don't have a proper IPython API in VS Code.

Agree. This has been a thorn in my side.


Hmm, your thoughts give me some ideas for the more immediate update. So while you go digging with the IPython team, here are two near term paths I'm considering:

  1. Since we write the cell to a command.py we can also implement a %run .../cell.py and no line will show.
    a. Can be tricky to associate the results to a specific cell (hence the use of %load instead) b. It would also be good to add a configuration to enable/disable this mode c. It is equivalent to implement a less explicit %run -r 😄
  2. VSCode team has increased its support for Python related extension
    a. This might a more formal path to a proper API b. Will have to dig through the example and learn more

If you agree with option 1, create a feature request issue 😅.

hoangKnLai commented 1 year ago

Just released v2023.9.0 with quite a bit of refactoring and reorganization for future. One of them include the run section of code via a code.py.

I would love to know what you think!

AmerM137 commented 1 year ago

The refactoring looks really good. It's starting to look cleaner and cleaner by each release.

Here is the post on the ipython repo about this https://github.com/ipython/ipython/issues/14158. Let's see what they come back with. It would be great if they can implement certain things that make our life easier.