diff --git a/Cargo.lock b/Cargo.lock index 98a71798ad50fd..04729bd7b4de7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3119,9 +3119,9 @@ dependencies = [ "deno_semver", "deno_terminal", "deno_unsync", - "dissimilar", "futures", "http 1.4.0", + "imara-diff", "import_map", "indexmap 2.12.0", "jsonc-parser 0.28.0", @@ -5917,14 +5917,24 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.2.1" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ "byteorder-lite", "quick-error 2.0.1", ] +[[package]] +name = "imara-diff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f01d462f766df78ab820dd06f5eb700233c51f0f4c2e846520eaf4ba6aa5c5c" +dependencies = [ + "hashbrown 0.15.5", + "memchr", +] + [[package]] name = "import_map" version = "0.25.0" diff --git a/Cargo.toml b/Cargo.toml index 394fc1d03f6bc8..73cda895972db2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -362,6 +362,7 @@ dprint-plugin-markdown = "=0.20.0" dprint-plugin-typescript = "=0.95.15" env_logger = "=0.11.6" fancy-regex = "=0.14.0" +imara-diff = "=0.2.0" libsui = "0.12.6" malva = "=0.12.1" markup_fmt = "=0.22.0" diff --git a/libs/resolver/Cargo.toml b/libs/resolver/Cargo.toml index 2713e9a49a5c38..7e9437971b780f 100644 --- a/libs/resolver/Cargo.toml +++ b/libs/resolver/Cargo.toml @@ -49,9 +49,9 @@ deno_permissions = { workspace = true, optional = true } deno_semver.workspace = true deno_terminal.workspace = true deno_unsync.workspace = true -dissimilar.workspace = true futures.workspace = true http = { workspace = true, optional = true } +imara-diff.workspace = true import_map.workspace = true indexmap.workspace = true jsonc-parser.workspace = true diff --git a/libs/resolver/display.rs b/libs/resolver/display.rs index 32e271fcdc0ea4..4336567dba802a 100644 --- a/libs/resolver/display.rs +++ b/libs/resolver/display.rs @@ -6,8 +6,8 @@ use std::fmt::Write as _; use deno_terminal::colors; -use dissimilar::Chunk; -use dissimilar::diff as difference; +use imara_diff::Diff; +use imara_diff::InternedInput; /// Print diff of the same file_path, before and after formatting. /// @@ -28,25 +28,25 @@ pub fn diff(orig_text: &str, edit_text: &str) -> String { DiffBuilder::build(&orig_text, &edit_text) } -struct DiffBuilder { +struct DiffBuilder<'a> { + input: InternedInput<&'a str>, output: String, line_number_width: usize, orig_line: usize, edit_line: usize, - orig: String, - edit: String, - has_changes: bool, } -impl DiffBuilder { - pub fn build(orig_text: &str, edit_text: &str) -> String { - let mut diff_builder = DiffBuilder { +impl<'a> DiffBuilder<'a> { + pub fn build(orig_text: &'a str, edit_text: &'a str) -> String { + let input = InternedInput::new(orig_text, edit_text); + let mut diff = Diff::compute(imara_diff::Algorithm::Histogram, &input); + diff.postprocess_lines(&input); + + let diff_builder = DiffBuilder { + input, output: String::new(), orig_line: 1, edit_line: 1, - orig: String::new(), - edit: String::new(), - has_changes: false, line_number_width: { let line_count = std::cmp::max( orig_text.split('\n').count(), @@ -55,97 +55,115 @@ impl DiffBuilder { line_count.to_string().chars().count() }, }; - - let chunks = difference(orig_text, edit_text); - diff_builder.handle_chunks(chunks); - diff_builder.output + diff_builder.handle_diff(diff) } - fn handle_chunks<'a>(&'a mut self, chunks: Vec>) { - for chunk in chunks { - match chunk { - Chunk::Delete(s) => { - let split = s.split('\n').enumerate(); - for (i, s) in split { - if i > 0 { - self.orig.push('\n'); - } - self.orig.push_str(&fmt_rem_text_highlight(s)); - } - self.has_changes = true - } - Chunk::Insert(s) => { - let split = s.split('\n').enumerate(); - for (i, s) in split { - if i > 0 { - self.edit.push('\n'); - } - self.edit.push_str(&fmt_add_text_highlight(s)); - } - self.has_changes = true - } - Chunk::Equal(s) => { - let split = s.split('\n').enumerate(); - for (i, s) in split { - if i > 0 { - self.flush_changes(); - } - self.orig.push_str(&fmt_rem_text(s)); - self.edit.push_str(&fmt_add_text(s)); - } - } + fn handle_diff(mut self, diff: Diff) -> String { + let mut prev_before_end: u32 = 0; + let mut is_first_hunk = true; + + for hunk in diff.hunks() { + // Skip unchanged lines between hunks + let gap_len = (hunk.before.start - prev_before_end) as usize; + if gap_len > 0 && !is_first_hunk { + writeln!( + self.output, + "{:width$}{} {}", + "", + colors::gray(" |"), + colors::gray("..."), + width = self.line_number_width + ) + .unwrap(); } + self.orig_line += gap_len; + self.edit_line += gap_len; + is_first_hunk = false; + + // Interleave deleted/inserted line pairs, then emit remaining + let del_count = hunk.before.len(); + let ins_count = hunk.after.len(); + let paired = std::cmp::min(del_count, ins_count); + + for i in 0..paired { + let del_idx = hunk.before.start + i as u32; + let s = self.input.interner[self.input.before[del_idx as usize]]; + self.write_rem_line(s); + let ins_idx = hunk.after.start + i as u32; + let s = self.input.interner[self.input.after[ins_idx as usize]]; + self.write_add_line(s); + } + // Remaining unpaired deletes + for del_idx in (hunk.before.start + paired as u32)..hunk.before.end { + let s = self.input.interner[self.input.before[del_idx as usize]]; + self.write_rem_line(s); + } + // Remaining unpaired inserts + for ins_idx in (hunk.after.start + paired as u32)..hunk.after.end { + let s = self.input.interner[self.input.after[ins_idx as usize]]; + self.write_add_line(s); + } + + prev_before_end = hunk.before.end; } - self.flush_changes(); + self.output } - fn flush_changes(&mut self) { - if self.has_changes { - self.write_line_diff(); - - self.orig_line += self.orig.split('\n').count(); - self.edit_line += self.edit.split('\n').count(); - self.has_changes = false; - } else { - self.orig_line += 1; - self.edit_line += 1; + fn write_rem_line(&mut self, text: &str) { + let (text, has_newline) = match text.strip_suffix('\n') { + Some(t) => (t, true), + None => (text, false), + }; + write!( + self.output, + "{:width$}{} ", + self.orig_line, + colors::gray(" |"), + width = self.line_number_width + ) + .unwrap(); + self.output.push_str(&fmt_rem()); + self.output.push_str(&fmt_rem_text_highlight(text)); + self.output.push('\n'); + if !has_newline { + self.write_no_newline_marker(); } - - self.orig.clear(); - self.edit.clear(); + self.orig_line += 1; } - fn write_line_diff(&mut self) { - let split = self.orig.split('\n').enumerate(); - for (i, s) in split { - write!( - self.output, - "{:width$}{} ", - self.orig_line + i, - colors::gray(" |"), - width = self.line_number_width - ) - .unwrap(); - self.output.push_str(&fmt_rem()); - self.output.push_str(s); - self.output.push('\n'); + fn write_add_line(&mut self, text: &str) { + let (text, has_newline) = match text.strip_suffix('\n') { + Some(t) => (t, true), + None => (text, false), + }; + write!( + self.output, + "{:width$}{} ", + self.edit_line, + colors::gray(" |"), + width = self.line_number_width + ) + .unwrap(); + self.output.push_str(&fmt_add()); + self.output.push_str(&fmt_add_text_highlight(text)); + self.output.push('\n'); + if !has_newline { + self.write_no_newline_marker(); } + self.edit_line += 1; + } - let split = self.edit.split('\n').enumerate(); - for (i, s) in split { - write!( - self.output, - "{:width$}{} ", - self.edit_line + i, - colors::gray(" |"), - width = self.line_number_width - ) - .unwrap(); - self.output.push_str(&fmt_add()); - self.output.push_str(s); - self.output.push('\n'); - } + fn write_no_newline_marker(&mut self) { + writeln!( + self.output, + "{:width$}{} {}", + "", + colors::gray(" |"), + colors::gray("\\ No newline at end of file"), + width = self.line_number_width + ) + .unwrap(); } } @@ -153,10 +171,6 @@ fn fmt_add() -> String { colors::green_bold("+").to_string() } -fn fmt_add_text(x: &str) -> String { - colors::green(x).to_string() -} - fn fmt_add_text_highlight(x: &str) -> String { colors::black_on_green(x).to_string() } @@ -165,10 +179,6 @@ fn fmt_rem() -> String { colors::red_bold("-").to_string() } -fn fmt_rem_text(x: &str) -> String { - colors::red(x).to_string() -} - fn fmt_rem_text_highlight(x: &str) -> String { colors::white_on_red(x).to_string() } @@ -256,7 +266,9 @@ mod tests { "console.log(\"Hello World\");", concat!( "1 | -console.log('Hello World')\n", + " | \\ No newline at end of file\n", "1 | +console.log(\"Hello World\");\n", + " | \\ No newline at end of file\n", ), ); @@ -268,11 +280,13 @@ mod tests { "2 | -\n", "3 | -\n", "4 | -\n", - "5 | -console.log(\n", - "1 | +console.log(\n", + " | ...\n", "6 | -'Hello World'\n", "2 | +\"Hello World\"\n", - "7 | -)\n3 | +);\n", + "7 | -)\n", + " | \\ No newline at end of file\n", + "3 | +);\n", + " | \\ No newline at end of file\n", ), ); } @@ -284,8 +298,8 @@ mod tests { "test\nsome line text test\n", concat!( "2 | -some line text test\n", + " | \\ No newline at end of file\n", "2 | +some line text test\n", - "3 | +\n", ), ); } @@ -295,6 +309,55 @@ mod tests { run_test("test\n", "test\r\n", " | Text differed by line endings.\n"); } + #[test] + fn test_lockfile_diff() { + // Simulates the frozen lockfile diff scenario where adding a new + // dependency inserts lines while matching braces remain unchanged. + let before = r#"{ + "version": "5", + "packages": { + "npm:@denotest/add@1": "1.0.0" + }, + "npm": { + "@denotest/add@1.0.0": { + "integrity": "abc", + "tarball": "http://localhost/add/1.0.0.tgz" + } + } +}"#; + let after = r#"{ + "version": "5", + "packages": { + "npm:@denotest/add@1": "1.0.0", + "npm:@denotest/subtract@1": "1.0.0" + }, + "npm": { + "@denotest/add@1.0.0": { + "integrity": "abc", + "tarball": "http://localhost/add/1.0.0.tgz" + }, + "@denotest/subtract@1.0.0": { + "integrity": "def", + "tarball": "http://localhost/subtract/1.0.0.tgz" + } + } +}"#; + run_test( + before, + after, + concat!( + " 4 | - \"npm:@denotest/add@1\": \"1.0.0\"\n", + " 4 | + \"npm:@denotest/add@1\": \"1.0.0\",\n", + " 5 | + \"npm:@denotest/subtract@1\": \"1.0.0\"\n", + " | ...\n", + "11 | + },\n", + "12 | + \"@denotest/subtract@1.0.0\": {\n", + "13 | + \"integrity\": \"def\",\n", + "14 | + \"tarball\": \"http://localhost/subtract/1.0.0.tgz\"\n", + ), + ); + } + fn run_test(diff_text1: &str, diff_text2: &str, expected_output: &str) { assert_eq!( test_util::strip_ansi_codes(&diff(diff_text1, diff_text2,)), diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_cache.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_cache.out index dc90d5d8fb3901..2b18fc74a1a926 100644 --- a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_cache.out +++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_cache.out @@ -3,9 +3,8 @@ changes: 4 | - "npm:@denotest/add@1": "1.0.0" 4 | + "npm:@denotest/add@1": "1.0.0", 5 | + "npm:@denotest/subtract@1": "1.0.0" -10 | - } + | ... 11 | + }, 12 | + "@denotest/subtract@1.0.0": { 13 | + "integrity": "[WILDCARD]", 14 | + "tarball": "http://localhost:4260/@denotest/subtract/1.0.0.tgz" -15 | + } diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_http.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_http.out index 5549c902b8ae72..01383dbff897a1 100644 --- a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_http.out +++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_http.out @@ -1,11 +1,9 @@ Download http://localhost:4545/welcome.ts error: Uncaught (in promise) TypeError: The lockfile is out of date. Run `deno install --frozen=false`, or rerun with `--frozen=false` to update it. changes: -11 | - } 11 | + }, 12 | + "remote": { 13 | + "http://localhost:4545/welcome.ts": "7353d5fcbc36c45d26bcbca478cf973092523b07c45999f41319820092b4de31" -14 | + } const _ = await import(scheme + "localhost:4545/welcome.ts"); ^ at [WILDCARD] \ No newline at end of file diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_jsr.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_jsr.out index c22b529edb9983..26f3f0f34fc81d 100644 --- a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_jsr.out +++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_jsr.out @@ -3,14 +3,13 @@ Download http://127.0.0.1:4250/@denotest/add/1.0.0_meta.json Download http://127.0.0.1:4250/@denotest/add/1.0.0/mod.ts error: Uncaught (in promise) TypeError: The lockfile is out of date. Run `deno install --frozen=false`, or rerun with `--frozen=false` to update it. changes: - 4 | - "npm:@denotest/add@1": "1.0.0" 4 | + "jsr:@denotest/add@1": "1.0.0", - 5 | + "npm:@denotest/add@1": "1.0.0" - 6 | + }, + | ... 7 | + "jsr": { 8 | + "@denotest/add@1.0.0": { 9 | + "integrity": "[WILDCARD]" 10 | + } +11 | + }, const { add } = await import(scheme + "@denotest/add@1"); ^ - at [WILDCARD] \ No newline at end of file + at [WILDCARD] diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_npm.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_npm.out index 25e72c1e53d899..884eb8d6217eb9 100644 --- a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_npm.out +++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_npm.out @@ -4,12 +4,11 @@ changes: 4 | - "npm:@denotest/add@1": "1.0.0" 4 | + "npm:@denotest/add@1": "1.0.0", 5 | + "npm:@denotest/subtract@1": "1.0.0" -10 | - } + | ... 11 | + }, 12 | + "@denotest/subtract@1.0.0": { 13 | + "integrity": "[WILDCARD]", 14 | + "tarball": "http://localhost:4260/@denotest/subtract/1.0.0.tgz" -15 | + } const { subtract } = await import(scheme + "@denotest/subtract@1"); ^ at [WILDCARD] diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_cache.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_cache.out index 9c0fa7edcc9cd3..fae5a40c563e87 100644 --- a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_cache.out +++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_cache.out @@ -1,12 +1,7 @@ error: The lockfile is out of date. Run `deno install --frozen=false`, or rerun with `--frozen=false` to update it. changes: - 4 | - "jsr:@denotest/add@1": "1.0.0" - 5 | - }, - 6 | - "jsr": { 4 | + "jsr:@denotest/add@0.2.0": "0.2.0", - 5 | + "jsr:@denotest/add@1": "1.0.0" - 6 | + }, - 7 | + "jsr": { + | ... 8 | + "@denotest/add@0.2.0": { 9 | + "integrity": "[WILDCARD]" 10 | + }, diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_run.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_run.out index fe628a454a7cfb..8c000631b3ed58 100644 --- a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_run.out +++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_run.out @@ -2,13 +2,8 @@ Download http://127.0.0.1:4250/@denotest/add/0.2.0_meta.json Download http://127.0.0.1:4250/@denotest/add/0.2.0/mod.ts error: The lockfile is out of date. Run `deno install --frozen=false`, or rerun with `--frozen=false` to update it. changes: - 4 | - "jsr:@denotest/add@1": "1.0.0" - 5 | - }, - 6 | - "jsr": { 4 | + "jsr:@denotest/add@0.2.0": "0.2.0", - 5 | + "jsr:@denotest/add@1": "1.0.0" - 6 | + }, - 7 | + "jsr": { + | ... 8 | + "@denotest/add@0.2.0": { 9 | + "integrity": "[WILDCARD]" 10 | + }, diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_run.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_run.out index fe268456b32c7c..7baeb0d652a8b3 100644 --- a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_run.out +++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_run.out @@ -4,9 +4,8 @@ changes: 4 | - "npm:@denotest/add@1": "1.0.0" 4 | + "npm:@denotest/add@1": "1.0.0", 5 | + "npm:@denotest/subtract@1": "1.0.0" -10 | - } + | ... 11 | + }, 12 | + "@denotest/subtract@1.0.0": { 13 | + "integrity": "[WILDCARD]", 14 | + "tarball": "http://localhost:4260/@denotest/subtract/1.0.0.tgz" -15 | + } diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed.out b/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed.out index 84b2c8194a5b45..d27fef4fd9211d 100644 --- a/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed.out +++ b/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed.out @@ -4,7 +4,7 @@ changes: 4 | - "npm:@denotest/add@1": "1.0.0" 4 | + "npm:@denotest/add@1": "1.0.0", 5 | + "npm:@denotest/bin@0.7.0": "0.7.0" -10 | - } + | ... 11 | + }, 12 | + "@denotest/bin@0.7.0": { 13 | + "integrity": "sha512-RAE7sQrdTUuV4KdDAshObhsULXb2QjTjfRg/KbzE9asZV8dUmwbPZy2kfmE2CunPo8+6DvwPklXFJ4PQi0Usuw==", @@ -17,4 +17,3 @@ changes: 20 | + "dependencies": [ 21 | + "npm:@denotest/bin@0.7.0" 22 | + ] -23 | + } diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed_install.out b/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed_install.out index e185239aa8be00..cb20dc4321eccb 100644 --- a/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed_install.out +++ b/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed_install.out @@ -3,7 +3,7 @@ changes: 4 | - "npm:@denotest/add@1": "1.0.0" 4 | + "npm:@denotest/add@1": "1.0.0", 5 | + "npm:@denotest/bin@0.7.0": "0.7.0" -10 | - } + | ... 11 | + }, 12 | + "@denotest/bin@0.7.0": { 13 | + "integrity": "sha512-RAE7sQrdTUuV4KdDAshObhsULXb2QjTjfRg/KbzE9asZV8dUmwbPZy2kfmE2CunPo8+6DvwPklXFJ4PQi0Usuw==", @@ -16,4 +16,3 @@ changes: 20 | + "dependencies": [ 21 | + "npm:@denotest/bin@0.7.0" 22 | + ] -23 | + } diff --git a/tests/specs/lockfile/frozen_lockfile/no_lockfile_run.out b/tests/specs/lockfile/frozen_lockfile/no_lockfile_run.out index 29a11dce70dad2..e4f98796c6ff99 100644 --- a/tests/specs/lockfile/frozen_lockfile/no_lockfile_run.out +++ b/tests/specs/lockfile/frozen_lockfile/no_lockfile_run.out @@ -1,7 +1,6 @@ Download http://localhost:4260/@denotest%2fadd error: The lockfile is out of date. Run `deno install --frozen=false`, or rerun with `--frozen=false` to update it. changes: - 1 | - 1 | +{ 2 | + "version": "5", 3 | + "specifiers": { @@ -14,4 +13,3 @@ changes: 10 | + } 11 | + } 12 | +} -13 | +