From 2e73f4745364de006e7d6d993cbe4972cb0d5cea Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 13 Nov 2024 21:13:59 -0500 Subject: [PATCH] Avoid duplicating first-entry comments in `uv add` (#9109) ## Summary Closes https://github.com/astral-sh/uv/issues/9105. --- crates/uv-workspace/src/pyproject_mut.rs | 88 ++++++++++++- crates/uv/tests/it/edit.rs | 149 +++++++++++++++++++++++ 2 files changed, 236 insertions(+), 1 deletion(-) diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 47bf5ec1142d..9b8cc150d24d 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -957,6 +957,23 @@ pub fn add_dependency( let decor = value.decor_mut(); + // If we're adding to the end of the list, treat trailing comments as leading comments + // on the added dependency. + // + // For example, given: + // ```toml + // dependencies = [ + // "anyio", # trailing comment + // ] + // ``` + // + // If we add `flask` to the end, we want to retain the comment on `anyio`: + // ```toml + // dependencies = [ + // "anyio", # trailing comment + // "flask", + // ] + // ``` if index == deps.len() { decor.set_prefix(deps.trailing().clone()); deps.set_trailing(""); @@ -979,7 +996,76 @@ pub fn add_dependency( .unwrap() .clone(); - deps.get_mut(index).unwrap().decor_mut().set_prefix(prefix); + // However, if the prefix includes a comment, we don't want to duplicate it. + // Depending on the location of the comment, we either want to leave it as-is, or + // attach it to the entry that's being moved to the next line. + // + // For example, given: + // ```toml + // dependencies = [ # comment + // "flask", + // ] + // ``` + // + // If we add `anyio` to the beginning, we want to retain the comment on the open + // bracket: + // ```toml + // dependencies = [ # comment + // "anyio", + // "flask", + // ] + // ``` + // + // However, given: + // ```toml + // dependencies = [ + // # comment + // "flask", + // ] + // ``` + // + // If we add `anyio` to the beginning, we want the comment to move down with the + // existing entry: + // entry: + // ```toml + // dependencies = [ + // "anyio", + // # comment + // "flask", + // ] + if let Some(prefix) = prefix.as_str() { + // Treat anything before the first own-line comment as a prefix on the new + // entry; anything after the first own-line comment is a prefix on the existing + // entry. + // + // This is equivalent to using the first and last line content as the prefix for + // the new entry, and the rest as the prefix for the existing entry. + if let Some((first_line, rest)) = prefix.split_once(['\r', '\n']) { + // Determine the appropriate newline character. + let newline = { + let mut chars = prefix[first_line.len()..].chars(); + match (chars.next(), chars.next()) { + (Some('\r'), Some('\n')) => "\r\n", + (Some('\r'), _) => "\r", + (Some('\n'), _) => "\n", + _ => "\n", + } + }; + let last_line = rest.lines().last().unwrap_or_default(); + let prefix = format!("{first_line}{newline}{last_line}"); + deps.get_mut(index).unwrap().decor_mut().set_prefix(prefix); + + let prefix = format!("{newline}{rest}"); + deps.get_mut(index + 1) + .unwrap() + .decor_mut() + .set_prefix(prefix); + } else { + deps.get_mut(index).unwrap().decor_mut().set_prefix(prefix); + } + } else { + deps.get_mut(index).unwrap().decor_mut().set_prefix(prefix); + } } reformat_array_multiline(deps); diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 5a95afcf8c95..cabbacd50f32 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -7681,3 +7681,152 @@ fn add_preserves_trailing_depth() -> Result<()> { Ok(()) } + +#[test] +fn add_preserves_first_own_line_comment() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + # comment + "sniffio==1.3.1", + ] + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("charset-normalizer"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + charset-normalizer==3.3.2 + + sniffio==1.3.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "charset-normalizer>=3.3.2", + # comment + "sniffio==1.3.1", + ] + "### + ); + }); + Ok(()) +} + +#[test] +fn add_preserves_first_line_bracket_comment() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ # comment + "sniffio==1.3.1", + ] + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("charset-normalizer"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + charset-normalizer==3.3.2 + + sniffio==1.3.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ # comment + "charset-normalizer>=3.3.2", + "sniffio==1.3.1", + ] + "### + ); + }); + Ok(()) +} + +#[test] +fn add_no_indent() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ +"sniffio==1.3.1" + ] + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("charset-normalizer"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + charset-normalizer==3.3.2 + + sniffio==1.3.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "charset-normalizer>=3.3.2", + "sniffio==1.3.1", + ] + "### + ); + }); + Ok(()) +}