Skip to content

improvement: Generate braceless Scala 3 templates in NewFileProvider - #8542

Open
jozanek wants to merge 3 commits into
scalameta:mainfrom
jozanek:improvement/3243-braceless-new-file-scala3
Open

improvement: Generate braceless Scala 3 templates in NewFileProvider#8542
jozanek wants to merge 3 commits into
scalameta:mainfrom
jozanek:improvement/3243-braceless-new-file-scala3

Conversation

@jozanek

@jozanek jozanek commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Implements the NewFileProvider half of #3243: the New Scala File command (including the Package Object kind) can now emit brace-free Scala 3 stubs.

Rather than a boolean opt-in, the style is inferred automatically so it works with zero configuration and matches the surrounding project:

Kind Braceful Braceless
class / object / trait class Foo {} class Foo
package object package object foo {} package object foo
enum enum Color { case } enum Color: / case

How the style is chosen (new-files-braceless-syntax setting)

Only Scala 3 is ever braceless. Beyond that:

  1. Compiler capability first — if the compiler forbids significant indentation (-no-indent, -old-syntax, or a -source:*-migration mode), braces are always used, overriding everything below.
  2. new-files-braceless-syntax (default auto):
    • auto — match the style of nearby existing sources: the target package directory and its enclosing directories up to the source root, inspecting each directory's .scala files in a stable, filename-sorted order and stopping at the first top-level definition with a determinate style. Falls back to the -indent scalac option, then braces.
    • always — always braceless (Scala 3).
    • never — always braces (fully backward-compatible).

Braces remain the outcome for braceful projects, so existing users see no change unless their project is already braceless.

Bodyless empty stubs

Empty class/trait/object/package-object stubs are generated bodyless (class Foo, not class Foo: + empty body, which is a parse error in Scala 3); a bodyless declaration compiles and the cursor lands after the name. enum keeps the existing incomplete-case template (a bodyless enum doesn't compile either).

Scope (intentionally narrowed — does not close #3243)

The issue also names ImplementAbstractMembers. That is not included here: its Scala 3 stub generation lives in the upstream dotty presentation compiler (shipped via scala3-compiler), and it already respects/produces braceless style for existing files. Making "Implement all members" braceless from scratch would require an upstream dotty change (and a PresentationCompilerConfig flag it consumes), not a Metals-only change. Leaving #3243 open to track that half.

Summary by CodeRabbit

  • New Features
    • Added Scala 3 braceless/optional-braces syntax for newly created files (classes, traits, objects, enums, and package objects), with braces-based syntax preserved when braceless isn’t applicable.
    • Introduced the new-files-braceless-syntax setting (auto, always, never), including automatic style inference when auto.
  • Bug Fixes
    • Prevented invalid package object syntax when generating braceless templates.
  • Tests
    • Expanded unit tests to validate braceless detection and new-file generation across configurations and compiler/layout conditions.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0d168bbe-227f-4f0f-9d08-9fe39060b3b4

📥 Commits

Reviewing files that changed from the base of the PR and between 96d94e2 and a716982.

📒 Files selected for processing (1)
  • tests/unit/src/test/scala/tests/NewFileLspSuite.scala
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/src/test/scala/tests/NewFileLspSuite.scala

📝 Walkthrough

Walkthrough

Adds configurable Scala 3 braceless syntax generation for new files. NewFileProvider applies auto, always, or never policies, compiler-option checks, and nearby-source style detection. Package objects use a bodyless form when braceless syntax is selected, with configuration, detection, and LSP coverage.

Changes

Braceless New File Generation

Layer / File(s) Summary
Configuration and source-style detection
metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala, metals/src/main/scala/scala/meta/internal/metals/newScalaFile/BracelessSyntax.scala
Adds the braceless-syntax configuration ADT, serialization and parsing, option registration, and source-tree detection of braced versus braceless top-level definitions.
Braceless template selection
metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala, metals/src/main/scala/scala/meta/internal/metals/PackageProvider.scala
Selects braceless syntax for eligible Scala 3 targets using configuration, compiler options, and nearby sources, then generates colon-indented class and enum templates or bodyless package objects.
Service configuration wiring
metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala
Passes the current userConfig supplier to NewFileProvider.
Generation and configuration tests
tests/unit/src/test/scala/tests/BracelessSyntaxSuite.scala, tests/unit/src/test/scala/tests/NewFileLspSuite.scala, tests/unit/src/test/scala/tests/UserConfigurationSuite.scala
Tests syntax detection, Scala 3 generation policies, compiler-option behavior, package objects, Scala 2 compatibility, and configuration output.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Client as LSP Client
    participant MetalsLspService
    participant NewFileProvider
    participant BracelessSyntax
    participant PackageProvider

    Client->>MetalsLspService: Create new Scala file
    MetalsLspService->>NewFileProvider: create file with userConfig supplier
    NewFileProvider->>BracelessSyntax: inspect nearby source style
    BracelessSyntax-->>NewFileProvider: style preference
    NewFileProvider->>NewFileProvider: apply policy and compiler options
    NewFileProvider->>PackageProvider: packageStatement(braceless)
    PackageProvider-->>NewFileProvider: package-object text
    NewFileProvider-->>MetalsLspService: generated file content
    MetalsLspService-->>Client: created file
Loading

Possibly related PRs

  • scalameta/metals#8209: Updates the NewFileProvider construction path with an additional constructor parameter.

Suggested reviewers: tgodzik, zielinsky, olafurpg

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: generating braceless Scala 3 templates in NewFileProvider.
Linked Issues check ✅ Passed The PR covers the NewFileProvider portion of #3243 and adds the configurability needed for optional-braces generation.
Out of Scope Changes check ✅ Passed All changes support Scala 3 braceless-template generation, configuration, or tests; no unrelated scope is apparent.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jozanek
jozanek marked this pull request as ready for review June 16, 2026 09:37
|inlay-hints.closing-labels.enable boolean false Should display closing label hints for methods/classes/objects next to their closing braces
|enable-semantic-highlighting boolean true Use semantic tokens highlight
|enable-indent-on-paste boolean false Indent snippets when pasted.
|use-braceless-syntax-for-new-files boolean false Use braceless syntax for newly generated Scala 3 files

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could avoid the setting altogether, since it might be hard to discover for users. We could, potentially, check first scala file that exists already within the source root. Then we can see if the tree there is using braces within any topelevel symbols. If no other files are present the default could be handled by -no-indent or -indent flag flag set by the user and then finally use braces for the final default.

What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, done. Dropped the setting and infer the style:

  • Existing sources: a token detector checks the nearest sources (target package up to the source root), then falls back to the -indent scalac option, then braces.
  • Compiler: -no-indent / -old-syntax / migration mode always force braces.
  • Override: kept a light new-files-braceless-syntax: auto | always | never (default auto) so empty or mixed projects can opt in or force a choice.

Braceful projects are unchanged. ImplementAbstractMembers is untouched since its Scala 3 generation is upstream in dotty.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unit/src/test/scala/tests/NewFileLspSuite.scala (1)

571-589: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test name doesn't match what it verifies.

"scala2-always-braces" never passes bracelessSyntax = Some("always"); it only exercises the default Auto policy on Scala 2 next to a braceless-looking sibling. The actual "always override + existing sibling on Scala 2" combination (implied by the name) is untested — the closest existing test (braceless-syntax-always-ignored-on-scala2, lines 557-569) has no sibling files.

✏️ Suggested fix: either rename or add the missing parameter
-  // Scala 2 never uses braceless, even next to a braceless-looking sibling.
-  checkScala("scala2-always-braces")(
+  // Scala 2 never uses braceless, even with `always` set and a braceless-looking sibling.
+  checkScala("scala2-always-braces")(
     directory = Some("a/src/main/scala/foo/"),
     fileType = Right(Class),
     fileName = Right("Foo"),
     expectedFilePath = "a/src/main/scala/foo/Foo.scala",
     expectedContent = s"""|package foo
                           |
                           |class Foo {
                           |$indent
                           |}
                           |""".stripMargin,
     existingFiles = """|/a/src/main/scala/foo/Existing.scala
                        |package foo
                        |
                        |object Existing:
                        |  def value = 1
                        |""".stripMargin,
+    bracelessSyntax = Some("always"),
   )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/src/test/scala/tests/NewFileLspSuite.scala` around lines 571 -
589, Rename the test currently labeled "scala2-always-braces" to reflect that it
verifies Scala 2’s default Auto policy beside a braceless-looking sibling, or
pass bracelessSyntax = Some("always") to make the name accurate. Prefer also
adding a separate test covering the explicit always override with an existing
sibling, complementing "braceless-syntax-always-ignored-on-scala2".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/unit/src/test/scala/tests/NewFileLspSuite.scala`:
- Around line 571-589: Rename the test currently labeled "scala2-always-braces"
to reflect that it verifies Scala 2’s default Auto policy beside a
braceless-looking sibling, or pass bracelessSyntax = Some("always") to make the
name accurate. Prefer also adding a separate test covering the explicit always
override with an existing sibling, complementing
"braceless-syntax-always-ignored-on-scala2".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d748c773-7a48-4352-8060-c5584e7ba0cf

📥 Commits

Reviewing files that changed from the base of the PR and between a193887 and 96d94e2.

📒 Files selected for processing (7)
  • metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala
  • metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala
  • metals/src/main/scala/scala/meta/internal/metals/newScalaFile/BracelessSyntax.scala
  • metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala
  • tests/unit/src/test/scala/tests/BracelessSyntaxSuite.scala
  • tests/unit/src/test/scala/tests/NewFileLspSuite.scala
  • tests/unit/src/test/scala/tests/UserConfigurationSuite.scala
🚧 Files skipped from review as they are similar to previous changes (2)
  • metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala
  • metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Scala3] Option for generating code using optional-braces

2 participants