Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,9 @@ abstract class MetalsLspService(
scalaVersionSelector,
clientConfig.icons(),
clientConfig.isReadClipboardProvider(),
buildTargets,
trees,
() => userConfig,
onCreate = path => {
onCreate(path)
onChange(List(path))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class PackageProvider(
def packageStatement(
path: AbsolutePath,
fileContent: String = "",
braceless: Boolean = false,
): Option[NewFileTemplate] = {

def packageObjectStatement(
Expand All @@ -63,12 +64,20 @@ class PackageProvider(
packageParts.lastOption.map { packageObjectName =>
val indent = " "
val backtickedName = wrap(packageObjectName)
NewFileTemplate(
s"""|${packageDeclaration}package object $backtickedName {
|${indent}@@
|}
|""".stripMargin
)
// A bodyless `package object foo` compiles, whereas `package object foo:`
// with an empty indented region is a parse error.
if (braceless)
NewFileTemplate(
s"""|${packageDeclaration}package object $backtickedName@@
|""".stripMargin
)
else
NewFileTemplate(
s"""|${packageDeclaration}package object $backtickedName {
|${indent}@@
|}
|""".stripMargin
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ case class UserConfiguration(
inlayHintsOptions: InlayHintsOptions = InlayHintsOptions(Map.empty),
enableStripMarginOnTypeFormatting: Boolean = true,
enableIndentOnPaste: Boolean = false,
newFilesBracelessSyntax: NewFilesBracelessSyntax =
NewFilesBracelessSyntax.Auto,
enableSemanticHighlighting: Boolean = true,
excludedPackages: Option[List[String]] = None,
fallbackScalaVersion: Option[String] = None,
Expand Down Expand Up @@ -114,6 +116,11 @@ case class UserConfiguration(
)
),
Some(("enableIndentOnPaste", enableIndentOnPaste)),
Some(
"newFilesBracelessSyntax" -> newFilesBracelessSyntax
.toString()
.toLowerCase()
),
Some(
(
"enableSemanticHighlighting",
Expand Down Expand Up @@ -468,6 +475,20 @@ object UserConfiguration {
|""".stripMargin,
isBoolean = true,
),
UserConfigurationOption(
"new-files-braceless-syntax",
"auto",
"always",
"Braceless syntax for newly generated Scala 3 files",
"""|Whether new Scala 3 files (from the "New Scala File" command, including the
|"Package Object" kind) use optional-braces / significant-indentation syntax
|instead of curly braces. "auto" matches the style of nearby existing sources
|(the target package directory and its enclosing directories up to the source
|root, falling back to the `-indent` / `-no-indent` compiler option, then
|braces); "always" always uses braceless syntax; "never" always uses braces.
|Has no effect on Scala 2 sources.""".stripMargin,
values = Some(List("auto", "always", "never")),
),
UserConfigurationOption(
"fallback-scala-version",
BuildInfo.scala3,
Expand Down Expand Up @@ -844,6 +865,14 @@ object UserConfiguration {
getBooleanKey("enable-strip-margin-on-type-formatting").getOrElse(true)
val enableIndentOnPaste =
getBooleanKey("enable-indent-on-paste").getOrElse(true)
val newFilesBracelessSyntax =
getStringKey("new-files-braceless-syntax").map(
_.trim().toLowerCase()
) match {
case Some("always") => NewFilesBracelessSyntax.Always
case Some("never") => NewFilesBracelessSyntax.Never
case _ => NewFilesBracelessSyntax.Auto
}
val enableSemanticHighlighting =
getBooleanKey("enable-semantic-highlighting").getOrElse(true)
val excludedPackages =
Expand Down Expand Up @@ -931,6 +960,7 @@ object UserConfiguration {
inlayHintsOptions,
enableStripMarginOnTypeFormatting,
enableIndentOnPaste,
newFilesBracelessSyntax,
enableSemanticHighlighting,
excludedPackages,
defaultScalaVersion,
Expand Down Expand Up @@ -993,3 +1023,16 @@ object AutoImportBuildKind {
case object Initial extends AutoImportBuildKind
case object All extends AutoImportBuildKind
}

sealed trait NewFilesBracelessSyntax
object NewFilesBracelessSyntax {

/** Match the style already used in the project. */
case object Auto extends NewFilesBracelessSyntax

/** Always generate braceless (significant-indentation) Scala 3 code. */
case object Always extends NewFilesBracelessSyntax

/** Always generate braced code. */
case object Never extends NewFilesBracelessSyntax
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package scala.meta.internal.metals.newScalaFile

import scala.annotation.tailrec

import scala.meta._
import scala.meta.tokens.{Token => T}

/**
* Detects whether a Scala source prefers the optional-braces
* (significant-indentation) style over curly braces.
*
* This lets `NewFileProvider` match the style of the surrounding project when
* generating new files, instead of relying on a user setting.
*/
object BracelessSyntax {

/**
* Whether `tree` prefers braceless syntax, based on its first top-level type
* definition that has a body.
*
* - `Some(true)` the first such definition uses significant indentation
* - `Some(false)` it uses curly braces
* - `None` no top-level definition with a body was found
*/
def prefersBraceless(tree: Tree): Option[Boolean] =
topLevelTemplates(tree).flatMap(bodyStyle).nextOption()

/** Templates of top-level type definitions, descending into packages. */
private def topLevelTemplates(tree: Tree): Iterator[Template] =
tree match {
case Source(stats) => stats.iterator.flatMap(topLevelTemplates)
case Pkg(_, stats) => stats.iterator.flatMap(topLevelTemplates)
case t: Pkg.Object => Iterator.single(t.templ)
case t: Defn.Class => Iterator.single(t.templ)
case t: Defn.Trait => Iterator.single(t.templ)
case t: Defn.Object => Iterator.single(t.templ)
case t: Defn.Enum => Iterator.single(t.templ)
case _ => Iterator.empty
}

/**
* Whether a template body is opened by significant indentation rather than a
* brace. We look for the first `{` or `:` at the top level of the template,
* skipping anything nested in the parent constructor's parentheses/brackets
* (and thus ignoring a member's own braces, which come after the opener).
*/
private def bodyStyle(template: Template): Option[Boolean] = {
@tailrec
def loop(tokens: List[T], depth: Int): Option[Boolean] =
tokens match {
case Nil => None
case (_: T.LeftParen | _: T.LeftBracket) :: rest =>
loop(rest, depth + 1)
case (_: T.RightParen | _: T.RightBracket) :: rest =>
loop(rest, depth - 1)
case (_: T.LeftBrace) :: _ if depth == 0 => Some(false)
case (_: T.Colon) :: _ if depth == 0 => Some(true)
case _ :: rest => loop(rest, depth)
}
loop(template.tokens.toList, 0)
}
}
Loading
Loading