Repository: dprint/dprint-plugin-typescript (rust)

Issue:
Add ability to force the import statement in one line
What about adding options to keep the import statement always on one line. [There is a discussion](https://github.com/prettier/prettier/issues/5995) for Prettier.

Pull request description:
feat: add `importDeclaration.forceSingleLine` and `exportDeclaration.forceSingleLine`
Closes #365

Patch:
diff --git a/deployment/schema.json b/deployment/schema.json
index c0b5760..f6eff19 100644
--- a/deployment/schema.json
+++ b/deployment/schema.json
@@ -237,6 +237,18 @@
         "description": ""
       }]
     },
+    "forceSingleLine": {
+      "description": "If code should be forced to be on a single line if able.",
+      "type": "boolean",
+      "default": false,
+      "oneOf": [{
+        "const": true,
+        "description": ""
+      }, {
+        "const": false,
+        "description": ""
+      }]
+    },
     "sortOrder": {
       "description": "The kind of sort ordering to use.",
       "type": "string",
@@ -1210,6 +1222,12 @@
     },
     "variableStatement.preferSingleLine": {
       "$ref": "#/definitions/preferSingleLine"
+    },
+    "exportDeclaration.forceSingleLine": {
+      "$ref": "#/definitions/forceSingleLine"
+    },
+    "importDeclaration.forceSingleLine": {
+      "$ref": "#/definitions/forceSingleLine"
     }
   }
 }
diff --git a/src/configuration/builder.rs b/src/configuration/builder.rs
index 607242d..6c80cf9 100644
--- a/src/configuration/builder.rs
+++ b/src/configuration/builder.rs
@@ -713,6 +713,16 @@ impl ConfigurationBuilder {
     self.insert("whileStatement.preferHanging", value.into())
   }
 
+  /* force single line */
+
+  pub fn export_declaration_force_single_line(&mut self, value: bool) -> &mut Self {
+    self.insert("exportDeclaration.forceSingleLine", value.into())
+  }
+
+  pub fn import_declaration_force_single_line(&mut self, value: bool) -> &mut Self {
+    self.insert("importDeclaration.forceSingleLine", value.into())
+  }
+
   /* member spacing */
 
   pub fn enum_declaration_member_spacing(&mut self, value: MemberSpacing) -> &mut Self {
@@ -1140,6 +1150,9 @@ mod tests {
       .type_parameters_prefer_single_line(false)
       .union_and_intersection_type_prefer_single_line(false)
       .variable_statement_prefer_single_line(false)
+      /* force single line */
+      .export_declaration_force_single_line(true)
+      .import_declaration_force_single_line(true)
       /* space settings */
       .binary_expression_space_surrounding_bitwise_and_arithmetic_operator(true)
       .comment_line_force_space_after_slashes(false)
@@ -1185,7 +1198,7 @@ mod tests {
       .while_statement_space_around(true);
 
     let inner_config = config.get_inner_config();
-    assert_eq!(inner_config.len(), 167);
+    assert_eq!(inner_config.len(), 169);
     let diagnostics = resolve_config(inner_config, &resolve_global_config(ConfigKeyMap::new(), &Default::default()).config).diagnostics;
     assert_eq!(diagnostics.len(), 0);
   }
diff --git a/src/configuration/resolve_config.rs b/src/configuration/resolve_config.rs
index 9ebe840..47c48f8 100644
--- a/src/configuration/resolve_config.rs
+++ b/src/configuration/resolve_config.rs
@@ -224,6 +224,9 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
     type_parameters_prefer_single_line: get_value(&mut config, "typeParameters.preferSingleLine", prefer_single_line, &mut diagnostics),
     union_and_intersection_type_prefer_single_line: get_value(&mut config, "unionAndIntersectionType.preferSingleLine", prefer_single_line, &mut diagnostics),
     variable_statement_prefer_single_line: get_value(&mut config, "variableStatement.preferSingleLine", prefer_single_line, &mut diagnostics),
+    /* force single line */
+    import_declaration_force_single_line: get_value(&mut config, "importDeclaration.forceSingleLine", false, &mut diagnostics),
+    export_declaration_force_single_line: get_value(&mut config, "exportDeclaration.forceSingleLine", false, &mut diagnostics),
     /* space settings */
     binary_expression_space_surrounding_bitwise_and_arithmetic_operator: get_value(
       &mut config,
diff --git a/src/configuration/types.rs b/src/configuration/types.rs
index 79aee73..447712d 100644
--- a/src/configuration/types.rs
+++ b/src/configuration/types.rs
@@ -508,6 +508,11 @@ pub struct Configuration {
   pub union_and_intersection_type_prefer_single_line: bool,
   #[serde(rename = "variableStatement.preferSingleLine")]
   pub variable_statement_prefer_single_line: bool,
+  /* force single line */
+  #[serde(rename = "importDeclaration.forceSingleLine")]
+  pub import_declaration_force_single_line: bool,
+  #[serde(rename = "exportDeclaration.forceSingleLine")]
+  pub export_declaration_force_single_line: bool,
 
   /* use space separator */
   #[serde(rename = "binaryExpression.spaceSurroundingBitwiseAndArithmeticOperator")]
diff --git a/src/generation/generate.rs b/src/generation/generate.rs
index 662aa7a..0b29a13 100644
--- a/src/generation/generate.rs
+++ b/src/generation/generate.rs
@@ -986,10 +986,12 @@ fn gen_export_named_decl<'a>(node: &'a NamedExport, context: &mut Context<'a>) -
     }
   }
 
-  let should_single_line = default_export.is_none()
-    && namespace_export.is_none()
-    && named_exports.len() <= 1
-    && node.start_line_fast(context.program) == node.end_line_fast(context.program);
+  let force_single_line = context.config.export_declaration_force_single_line && !contains_line_or_multiline_comment(&node.into(), &context.program);
+  let should_single_line = force_single_line
+    || (default_export.is_none()
+      && namespace_export.is_none()
+      && named_exports.len() <= 1
+      && node.start_line_fast(context.program) == node.end_line_fast(context.program));
 
   // generate
   let mut items = PrintItems::new();
@@ -1166,10 +1168,12 @@ fn gen_import_decl<'a>(node: &'a ImportDecl, context: &mut Context<'a>) -> Print
     }
   }
 
-  let should_single_line = default_import.is_none()
-    && namespace_import.is_none()
-    && named_imports.len() <= 1
-    && node.start_line_fast(context.program) == node.end_line_fast(context.program);
+  let force_single_line = context.config.import_declaration_force_single_line && !contains_line_or_multiline_comment(&node.into(), &context.program);
+  let should_single_line = force_single_line
+    || (default_import.is_none()
+      && namespace_import.is_none()
+      && named_imports.len() <= 1
+      && node.start_line_fast(context.program) == node.end_line_fast(context.program));
   let has_named_imports = !named_imports.is_empty() || {
     let from_keyword = context.token_finder.get_previous_token_if_from_keyword(node.src);
     if let Some(from_keyword) = from_keyword {
diff --git a/src/generation/swc/helpers.rs b/src/generation/swc/helpers.rs
new file mode 100644
index 0000000..937b4ca
--- /dev/null
+++ b/src/generation/swc/helpers.rs
@@ -0,0 +1,27 @@
+use deno_ast::swc::common::comments::CommentKind;
+use deno_ast::view::Node;
+use deno_ast::view::Program;
+use deno_ast::RootNode;
+use deno_ast::SourceRanged;
+use deno_ast::SourceRangedForSpanned;
+
+/** Gets if the node contains a line comment or multi-line block comment */
+pub fn contains_line_or_multiline_comment(node: &Node, program: &Program) -> bool {
+  let tokens = node.tokens_fast(program);
+  let comments = program.comment_container();
+  for (i, token) in tokens.iter().enumerate() {
+    if i > 0 {
+      let mut leading_comments = comments.leading_comments(token.start());
+      if leading_comments.any(|c| c.kind == CommentKind::Line || c.text.contains('\n')) {
+        return true;
+      }
+    }
+    if i < tokens.len() - 1 {
+      let mut trailing_comments = comments.trailing_comments(token.start());
+      if trailing_comments.any(|c| c.kind == CommentKind::Line || c.text.contains('\n')) {
+        return true;
+      }
+    }
+  }
+  false
+}
diff --git a/src/generation/swc/mod.rs b/src/generation/swc/mod.rs
index 2ffb0da..7d2e7a0 100644
--- a/src/generation/swc/mod.rs
+++ b/src/generation/swc/mod.rs
@@ -2,8 +2,10 @@ mod extensions;
 mod flatten_assign_expr;
 mod flatten_binary_expr;
 mod flatten_member_like_expr;
+mod helpers;
 
 pub use extensions::*;
 pub use flatten_assign_expr::*;
 pub use flatten_binary_expr::*;
 pub use flatten_member_like_expr::*;
+pub use helpers::*;


Test patch:
diff --git a/tests/specs/declarations/export/ExportDeclaration_ForceSingleLine_True.txt b/tests/specs/declarations/export/ExportDeclaration_ForceSingleLine_True.txt
new file mode 100644
index 0000000..5b69971
--- /dev/null
+++ b/tests/specs/declarations/export/ExportDeclaration_ForceSingleLine_True.txt
@@ -0,0 +1,28 @@
+~~ exportDeclaration.forceSingleLine: true, lineWidth: 40 ~~
+== should keep export decls on same line unless they contain a line comment ==
+export { testing, a, b, c, d, e } from "./test.ts";
+export { testing, a, b, c, d, e // test
+} from "./test.ts";
+export { a, b, c, d, e /* this is ok though testing testing */ } from "./test.ts";
+export { a, b, c, d, e /* and
+not ok */ } from "./test.ts";
+
+[expect]
+export { a, b, c, d, e, testing } from "./test.ts";
+export {
+    a,
+    b,
+    c,
+    d,
+    e, // test
+    testing,
+} from "./test.ts";
+export { a, b, c, d, e /* this is ok though testing testing */ } from "./test.ts";
+export {
+    a,
+    b,
+    c,
+    d,
+    e, /* and
+not ok */
+} from "./test.ts";
diff --git a/tests/specs/declarations/import/ImportDeclaration_ForceSingleLine_True.txt b/tests/specs/declarations/import/ImportDeclaration_ForceSingleLine_True.txt
new file mode 100644
index 0000000..7b81b7c
--- /dev/null
+++ b/tests/specs/declarations/import/ImportDeclaration_ForceSingleLine_True.txt
@@ -0,0 +1,28 @@
+~~ importDeclaration.forceSingleLine: true, lineWidth: 40 ~~
+== should keep import decls on same line unless they contain a line comment ==
+import { testing, a, b, c, d, e } from "./test.ts";
+import { testing, a, b, c, d, e // test
+} from "./test.ts";
+import { a, b, c, d, e /* this is ok though testing testing */ } from "./test.ts";
+import { a, b, c, d, e /* and
+not ok */ } from "./test.ts";
+
+[expect]
+import { a, b, c, d, e, testing } from "./test.ts";
+import {
+    a,
+    b,
+    c,
+    d,
+    e, // test
+    testing,
+} from "./test.ts";
+import { a, b, c, d, e /* this is ok though testing testing */ } from "./test.ts";
+import {
+    a,
+    b,
+    c,
+    d,
+    e, /* and
+not ok */
+} from "./test.ts";