biomejs/biome

☂️ Partial support for `.vue`, `.svelte` and `.astro` files

ematipico opened this issue · 16 comments

Description

What does partial support mean? It means that the objective is to parse only the JavaScript portions of the files, and ignore the rest.

Since everything works at workspace level, the support of these three files is pretty straightforward:

Astro

  • Capabilities
  • Parsing
  • Format file
  • Format on type
  • Format range
  • Code actions
  • Lint
  • Code actions
  • Fix all
  • Organize imports

Parsing and capabilities

It's safe to assume that Astro files are always TypeScript files. So it should be parsed and handled as a JsFileSource::ts()

Vue

  • Capabilities
  • Parsing
  • Format file
  • Format on type
  • Format range
  • Code actions
  • Lint
  • Code actions
  • Fix all
  • Organize imports

Parsing and capabilities

The JavaScript code is inside the `<script></script> tag. Let's use a regex to compile the start/end of the tags.

Let's use a simple check to understand the language, e.g. <script lang="ts">. Based on its value, we can compute the correct JsFileSource

Svelte

  • Capabilities
  • Parsing
  • Format file
  • Format on type
  • Format range
  • Code actions
  • Lint
  • Code actions
  • Fix all
  • Organize imports

Parsing and capabilities

The JavaScript code is inside the `<script></script> tag. Let's use a regex to compile the start/end of the tags.

Let's use a simple check to understand the language, e.g. <script lang="ts">. Based on its value, we can compute the correct JsFileSource

How to help/contribute

First, each language must have implemented capabilities and parsing. With those two, we can implement the rest of the features.

Use this PR as a blueprint to understand how to add handling of files with a particular extension: #1718

  • inside file_handlers/mod.rs, you'll have to add a new extension;
  • inside file_handlers/, you'll have to create a new file with the name of the extension to handle;
  • capabilities should all be None at the beginning, then we will add functions as we go, to handle all the various cases.

Please comment if you want to help, and with what. We want to have only one person working on the same thing.

For testing, use #1718 as a reference. And if you require more information, you can create a draft PR and follow up there with questions you might have.

Vue files can have multiple script tags afaik. Not sure about the others.

I will try implementing the Vue support. I'll open a draft PR soon

Is it safe to assume, that Astro JS Files are always Typescript? If you include something like define:vars prop to script, it becomes client-side only -> js.
https://docs.astro.build/en/reference/directives-reference/#isinline

Is it safe to assume, that Astro JS Files are always Typescript?

The initial objective is to parse only the frontmatter of the file, which is TypeScript. I got confirmation from Nate, who wrote the compiler.

The <script> tag will require more thoughts and care

All of them (Vue, Svelte and Astro) uses Volar. They might have solutions for most of your problems.

All of them (Vue, Svelte and Astro) uses Volar. They might have solutions for most of your problems.

Do you have more information to help us understand how Volar can help us?

@ematipico While updating the VS Code extension to handle the additional file types, I've discovered that formatting works correctly when using the CLI, but does not work as expected when going through the LSP.

Given the following Astro file (but applies to others too), the formatted, when invoked via the LSP will return only the "script" part, and remove everything else from the file.

- ---
- statement ( );
- statement();
- ---
- 
- <div></div>
+ statement();
+ statement();

Not sure if something more needs to be done here

#[tracing::instrument(level = "debug", skip(session), err)]
pub(crate) fn format(
session: &Session,
params: DocumentFormattingParams,
) -> Result<Option<Vec<TextEdit>>, LspError> {
let url = params.text_document.uri;
let rome_path = session.file_path(&url)?;
let doc = session.document(&url)?;
let file_features = session.workspace.file_features(SupportsFeatureParams {
path: rome_path.clone(),
feature: FeaturesBuilder::new().with_formatter().build(),
})?;
if file_features.supports_format() {
debug!("Formatting...");
let printed = session
.workspace
.format_file(FormatFileParams { path: rome_path })?;
let num_lines: u32 = doc.line_index.len();
let range = Range {
start: Position::default(),
end: Position {
line: num_lines,
character: 0,
},
};
let edits = vec![TextEdit {
range,
new_text: printed.into_code(),
}];
Ok(Some(edits))
} else {
notify_user(file_features, rome_path)
}
}

Yeah that makes sense. We still need to update the LSP side to stitch up the original content with the new content, like we do in the CLI

Now the LSP supports diagnostics and code actions for these files.

We just need to implement the rest of the features, which is going to be very straightforward.

If anyone wants help, this is going be very easy, even for new contributors. Feel free to comment here and I'll tell you how to land the support of the missing features.

I wouldn't mind helping out with the above, not sure where to start though

You could start with format_range and/or format_on_type for any of those files. The handlers of those files can be found here

https://github.com/biomejs/biome/tree/main/crates/biome_service/src/file_handlers

To implement the handler for a file - for example Astro, we have to create the new handlers. Let's do it for format_range. Create a new function here:

And update the capabilities here, with Some(format_range)

The types of the functions can be found here:

type Format = fn(&RomePath, AnyParse, SettingsHandle) -> Result<Printed, WorkspaceError>;
type FormatRange =
fn(&RomePath, AnyParse, SettingsHandle, TextRange) -> Result<Printed, WorkspaceError>;
type FormatOnType =
fn(&RomePath, AnyParse, SettingsHandle, TextSize) -> Result<Printed, WorkspaceError>;

The content of this new format_range function are slim. We just need to call what the JS handler does. So let's export this handler using pub(crate):

The content of the function format_range in the astro.rs file will look like this:

javascript::format_range( ) // pass the payload here

Take inspiration from this function:

fn format(
rome_path: &RomePath,
parse: AnyParse,
settings: SettingsHandle,
) -> Result<Printed, WorkspaceError> {
javascript::format(rome_path, parse, settings)
}

j commented

@ematipico quick question. Svelte script code blocks should have everything spaced but these changes move the entire block back a space to the start of the line. Can I have it add a space to the block?

<script>
  const foo =
</script>

Instead of

<script>
const foo =
</script>

@j

That's part of the limitations and expectations. This will get solved once we will support these files fully

Has it been released?

// package.json
    "@biomejs/biome": "^1.5.3-nightly.69f9031",
// biome.json
{
  "formatter": {
    "indentStyle": "space",
    "lineWidth": 120
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single"
    }
  }
}
> biome check --apply ./src

Fixed 4 file(s) in 16ms

Only able to scan .ts files, not .vue files.

j commented

@j

That's part of the limitations and expectations. This will get solved once we will support these files fully

@ematipico okay. I just gave this a try out of excitement and noticed it as the PR references what I'd expect out of the formatting.

@Shyam-Chen No, it will be released in the next minor