-
Notifications
You must be signed in to change notification settings - Fork 88
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
feat: implement grit format
command
#595
base: main
Are you sure you want to change the base?
Changes from 19 commits
561ce1d
03d22b2
7b49f63
fc431ec
7f767a4
3b0de9e
5761689
203de80
18e038f
ea82025
5ae0ebd
b744fdd
f9b05e9
5e697ad
1807c3c
570ff55
828a2b5
3dcc719
9a13522
1c11a9f
e97fba8
54076c7
b049607
a977855
f593bdd
9e7c24b
0af5bf0
409dcb3
7bb2622
692673d
ac8a0a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
use crate::{ | ||
resolver::{resolve_from_cwd, Source}, | ||
ux::format_diff, | ||
}; | ||
use anyhow::{anyhow, ensure, Context, Result}; | ||
use biome_grit_formatter::context::GritFormatOptions; | ||
use clap::Args; | ||
use colored::Colorize; | ||
use marzano_gritmodule::{config::ResolvedGritDefinition, parser::PatternFileExt}; | ||
use serde::Serialize; | ||
|
||
#[derive(Args, Debug, Serialize, Clone)] | ||
pub struct FormatArgs { | ||
/// Write formats to file instead of just showing them | ||
#[clap(long)] | ||
pub write: bool, | ||
} | ||
|
||
pub async fn run_format(arg: &FormatArgs) -> Result<()> { | ||
let (mut resolved, _) = resolve_from_cwd(&Source::Local).await?; | ||
// sort to have consistent output for tests | ||
resolved.sort(); | ||
|
||
let file_path_to_resolved = group_resolved_patterns_by_group(resolved); | ||
for (file_path, resovled_patterns) in file_path_to_resolved { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be easily executed in parallel, however the tests that verify stdout will fail due to inconsistencies in the output There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of changing the input to make tests happy, another option is to sort the test output by line so it is stabilized. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
if let Err(error) = | ||
format_file_resovled_patterns(file_path.clone(), resovled_patterns, arg.clone()).await | ||
{ | ||
eprintln!("couldn't format '{}': {error:?}", file_path) | ||
} | ||
} | ||
Ok(()) | ||
} | ||
|
||
fn group_resolved_patterns_by_group( | ||
resolved: Vec<ResolvedGritDefinition>, | ||
) -> Vec<(String, Vec<ResolvedGritDefinition>)> { | ||
resolved.into_iter().fold(Vec::new(), |mut acc, resolved| { | ||
let file_path = &resolved.config.path; | ||
if let Some((_, resolved_patterns)) = acc | ||
.iter_mut() | ||
.find(|(resolv_file_path, _)| resolv_file_path == file_path) | ||
{ | ||
resolved_patterns.push(resolved); | ||
} else { | ||
acc.push((file_path.clone(), vec![resolved])); | ||
} | ||
acc | ||
}) | ||
} | ||
|
||
async fn format_file_resovled_patterns( | ||
Arian8j2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
file_path: String, | ||
patterns: Vec<ResolvedGritDefinition>, | ||
arg: FormatArgs, | ||
) -> Result<()> { | ||
// patterns has atleast one resolve so unwrap is safe | ||
let first_pattern = patterns.first().unwrap(); | ||
Arian8j2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// currently all patterns has raw data so unwrap is safe | ||
let first_pattern_raw_data = first_pattern.config.raw.as_ref().unwrap(); | ||
let old_file_content = &first_pattern_raw_data.content; | ||
|
||
let new_file_content = match first_pattern_raw_data.format { | ||
PatternFileExt::Yaml => format_yaml_file(old_file_content)?, | ||
PatternFileExt::Grit => format_grit_code(old_file_content)?, | ||
PatternFileExt::Md => { | ||
let hunks = patterns | ||
.iter() | ||
.map(format_pattern_as_hunk_changes) | ||
.collect::<Result<Vec<HunkChange>>>()?; | ||
apply_hunk_changes(old_file_content, hunks) | ||
} | ||
}; | ||
|
||
if &new_file_content == old_file_content { | ||
return Ok(()); | ||
} | ||
|
||
if arg.write { | ||
tokio::fs::write(file_path, new_file_content) | ||
.await | ||
.with_context(|| "could not write to file")?; | ||
} else { | ||
println!( | ||
"{}:\n{}", | ||
file_path.bold(), | ||
format_diff(old_file_content, &new_file_content) | ||
); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
fn format_yaml_file(file_content: &str) -> Result<String> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @morgante Is it ok to also format the whole There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because we need to parse the whole There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not not serialize/deserialize the whole body. Round-tripping is lossy. We'd also lose any comments in the yaml. I understand finding and replacing just the bytes is harder but it's very much possible. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the problem is in finding the code, for example consider this version: 0.0.1
patterns:
- name: aspect_ratio_yaml
description: Yaml version of aspect_ratio.md
body: |
language css
`a { $props }` where {
$props <: contains `aspect-ratio: $x`
}
- file: ./others/test_move_import.md i can easily get the grit code:
and format it no problem, but then changing the body with new formatted code is difficult because how i find the old grit code in the file? each line is prefixed with indent spaces:
i need to know how much space i need to put before the code (i used to hard code this in the past commits but then it introduces other problems), thats the hard part, and i couldn't think of a way that is not hacky and breakable There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i could try to find the code like this: for i in 1..10 {
let indent = " ".repeat(i);
let grit_code_with_indent = prefix_each_line(indent, grit_code)
if let Some(pos) = yaml_file_content.position(grit_code_with_indent) {
// ...
break
}
} but it's a bit hacky There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We actually have a lot of utils in GritQL itself for handling indentation—tested here—so I think you should be able to leverage those. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice hint, GritQL really shines in these issues but i don't know if it's overkill or not for this, btw i updated the code, now it uses a grit pattern to find and replace yaml bodies with formatted ones |
||
// deserializing manually and not using `SerializedGritConfig` because | ||
// i don't want to remove any fields that `SerializedGritConfig` don't have such as 'version' | ||
let mut config: serde_yaml::Value = | ||
serde_yaml::from_str(file_content).with_context(|| "couldn't parse yaml file")?; | ||
let patterns = config | ||
.get_mut("patterns") | ||
.ok_or_else(|| anyhow!("couldn't find patterns in yaml file"))? | ||
.as_sequence_mut() | ||
.ok_or_else(|| anyhow!("patterns in yaml file are not sequence"))?; | ||
for pattern in patterns { | ||
let Some(body) = pattern.get_mut("body") else { | ||
continue; | ||
}; | ||
if let serde_yaml::Value::String(body_str) = body { | ||
*body_str = format_grit_code(body_str)?; | ||
// extra new line at end of grit body looks more readable | ||
body_str.push('\n'); | ||
} | ||
} | ||
Ok(serde_yaml::to_string(&config)?) | ||
} | ||
|
||
fn format_pattern_as_hunk_changes(pattern: &ResolvedGritDefinition) -> Result<HunkChange> { | ||
let formatted_grit_code = format_grit_code(&pattern.body)?; | ||
let body_range = pattern | ||
.config | ||
.range | ||
.ok_or_else(|| anyhow!("pattern doesn't have config range"))?; | ||
Ok(HunkChange { | ||
starting_byte: body_range.start_byte as usize, | ||
ending_byte: body_range.end_byte as usize, | ||
new_content: formatted_grit_code, | ||
}) | ||
} | ||
|
||
/// format grit code using `biome` | ||
fn format_grit_code(source: &str) -> Result<String> { | ||
let parsed = biome_grit_parser::parse_grit(source); | ||
ensure!( | ||
parsed.diagnostics().is_empty(), | ||
"biome couldn't parse: {}", | ||
parsed | ||
.diagnostics() | ||
.iter() | ||
.map(|diag| diag.message.to_string()) | ||
.collect::<Vec<_>>() | ||
.join("\n") | ||
); | ||
|
||
let options = GritFormatOptions::default(); | ||
let doc = biome_grit_formatter::format_node(options, &parsed.syntax()) | ||
.with_context(|| "biome couldn't format")?; | ||
Ok(doc.print()?.into_code()) | ||
} | ||
|
||
/// Represent a hunk of text that needs to be changed | ||
#[derive(Debug)] | ||
struct HunkChange { | ||
starting_byte: usize, | ||
ending_byte: usize, | ||
new_content: String, | ||
} | ||
|
||
/// returns a new string that applies hunk changes | ||
fn apply_hunk_changes(input: &str, mut hunks: Vec<HunkChange>) -> String { | ||
if hunks.is_empty() { | ||
return input.to_string(); | ||
} | ||
hunks.sort_by_key(|hunk| hunk.starting_byte); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you're sorting it, just reverse the order (apply last to first) and use replace_range. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice idea, Done |
||
let mut buffer = String::new(); | ||
let mut last_ending_byte = 0; | ||
for (index, hunk) in hunks.iter().enumerate() { | ||
buffer.push_str(&input[last_ending_byte..hunk.starting_byte]); | ||
buffer.push_str(&hunk.new_content); | ||
last_ending_byte = hunk.ending_byte; | ||
|
||
if index == hunks.len() - 1 { | ||
buffer.push_str(&input[last_ending_byte..]); | ||
} | ||
} | ||
buffer | ||
} | ||
Arian8j2 marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
version: 0.0.1 | ||
patterns: | ||
- name: aspect_ratio_yaml | ||
description: Yaml version of aspect_ratio.md | ||
body: | | ||
language css | ||
|
||
`a { $props }` where { | ||
$props <: contains `aspect-ratio: $x` | ||
} | ||
|
||
- file: ./others/test_move_import.md |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
--- | ||
private: true | ||
tags: [private] | ||
--- | ||
```grit | ||
language js | ||
`sanitizeFilePath` as $s where { | ||
move_import(`sanitizeFilePath`, `'@getgrit/universal'`) | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
--- | ||
title: Aspect ratio | ||
--- | ||
|
||
```grit | ||
language css | ||
|
||
`a { $props }` where { | ||
$props <: contains `aspect-ratio: $x` | ||
} | ||
``` | ||
|
||
## Matches the right selector and declaration block | ||
|
||
```css | ||
a { | ||
width: calc(100% - 80px); | ||
aspect-ratio: 1/2; | ||
font-size: calc(10px + (56 - 10) * ((100vw - 320px) / (1920 - 320))); | ||
} | ||
|
||
#some-id { | ||
some-property: 5px; | ||
} | ||
|
||
a.b ~ c.d { | ||
} | ||
.e.f + .g.h { | ||
} | ||
|
||
@font-face { | ||
font-family: 'Open Sans'; | ||
src: url('/a') format('woff2'), url('/b/c') format('woff'); | ||
} | ||
``` | ||
|
||
```css | ||
a { | ||
width: calc(100% - 80px); | ||
aspect-ratio: 1/2; | ||
font-size: calc(10px + (56 - 10) * ((100vw - 320px) / (1920 - 320))); | ||
} | ||
|
||
#some-id { | ||
some-property: 5px; | ||
} | ||
|
||
a.b ~ c.d { | ||
} | ||
.e.f + .g.h { | ||
} | ||
|
||
@font-face { | ||
font-family: 'Open Sans'; | ||
src: url('/a') format('woff2'), url('/b/c') format('woff'); | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
language json | ||
|
||
pattern upgrade_dependency($target_dep, $target_version, $dependency_key) { | ||
or { | ||
`$key: $value` where { | ||
$key <: `"$target_dep"`, | ||
$value => `"$target_version"` | ||
}, | ||
pair($key, $value) where { | ||
$key <: `"$dependency_key"`, | ||
$value <: object($properties) where { | ||
$properties <: not contains pair(key=$dep_key) where { | ||
$dep_key <: contains `$target_dep` | ||
}, | ||
$properties => `"$target_dep": "$target_version",\n$properties` | ||
} | ||
} | ||
} | ||
} | ||
|
||
pattern console_method_to_info($method) { | ||
`console.$method($message)` => `console.info($message)` | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@morgante Is this still important because
biome
depends onserde
version1.0.217
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's avoid changing this in the same PR if we can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after looking deeper into this, i found out that the main branch doesn't even use
serde
version under1.0.171
😄, cargo lock of main branch shows:even in the very first commit 79a5365, cargo lock was:
the current cargo toml is:
this allows versions
>= 1.0.164, < 2.0.0
according to the cargo book, for exact1.0.164
version the toml should looked like:or for under
1.0.171
it should looked like this:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok this is fine then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok then do you want me to remove that comment to avoid confusion later?