jupyterlab-contrib / jupyterlab_code_formatter

A JupyterLab plugin to facilitate invocation of code formatters.
https://jupyterlab-code-formatter.readthedocs.io/
MIT License
853 stars 55 forks source link

Add Rustfmt support #225

Closed UebelAndre closed 2 years ago

UebelAndre commented 3 years ago

If possible, it'd be nice to have official support for running the Rust code formatter, Rustfmt. I think support for rustfmt in this plugin would satisfy https://github.com/google/evcxr/issues/51 (the inspiration for opening this feature request).

ryantam626 commented 2 years ago

Sorry for the late reply, was extremely burnt out and just switched off.

Thanks for the suggestion! I think this is achievable, will try to do it in the coming days.

ryantam626 commented 2 years ago

I have added support in 1.5.1

UebelAndre commented 2 years ago

Thank you so much!

jonasbb commented 2 years ago

I tested the rustfmt support today, but sadly found it not to be working. First of, it is awesome, that this software already tries to support evcxr and rustfmt. I could get the formatting to work, but only by using the documentation and writing my own formatter.

from jupyterlab_code_formatter.formatters import (
    BaseFormatter,
    SERVER_FORMATTERS,
    command_exist,
)

class ExampleCustomFormatter(BaseFormatter):

    label = "Apply rustfmt Formatter"

    @property
    def importable(self) -> bool:
        return command_exist("rustfmt")

    def format_code(self, code: str, notebook: bool, **options) -> str:
        # Rust code can end in a ?, such that the default handler does not work.
        # It escapes the line with # and breaks Rust syntax.
        import subprocess

        code_fn = f"fn main() {{\n{code}\n}}"

        process = subprocess.run(
            [
                "rustfmt",
            ],
            input=code_fn,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=True,
            check=False,
        )

        if process.returncode != 0:
            logger.info("An error with rustfmt has ocurred:")
            logger.info(process.stderr)

            return code
        else:
            # Remove the fn main() wrapper and indentation
            return process.stdout.replace("\n    ", "\n")[
                len("fn main() {\n") : -len("\n}\n")
            ]

SERVER_FORMATTERS["example"] = ExampleCustomFormatter()

I hope these are all the relevant changes to the current code:

  1. rustfmt does not support formatting only snippets of code. It always needs to be run on a file. In the snippet above the code is wrapped like

    fn main() {
        <code>
    }

    This makes it syntactically a full file and rustfmt knows how to format it. The closing bracket needs to be on a separate line, as otherwise a line comment would comment it out. The fn wrapping and the indention need to be removed before returning the formatted code.

  2. Checking stderr for rustfmt failure is not very good. rustfmt has unstable configuration options. If these are set, a warning is printed to stderr. However, formatting still functions and the return code will be set to 0.
  3. The example in the documentation uses @handle_line_ending_and_magic, which does not work with Rust code. The HelpEscaper triggers on Rust code. Rust has a ? operator, which can be at the end of a line. If this happens, the HelpEscaper runs and the resulting code is no longer valid Rust.
  4. In the documentation https://ryantam626.github.io/jupyterlab_code_formatter/custom-formatter.html it says to write the custom formatter in ~/.jupyter/jupyter_notebook_config.py. This did not work. In my case using ~/.jupyter/jupyter_lab_config.py worked.

I hope this helps somebody.