Skip to content

Commit

Permalink
improvement: Try to use scala CLI format if it exists
Browse files Browse the repository at this point in the history
Scalafmt needs to habe a config file to run formatting. Previously, metals required it to be in the main workspace directory or to be defined via configuration. Now, we will also try to use the default scalafmt config from Scala CLI
  • Loading branch information
tgodzik committed Oct 26, 2023
1 parent a2316bc commit 3ef771b
Showing 1 changed file with 151 additions and 129 deletions.
280 changes: 151 additions & 129 deletions metals/src/main/scala/scala/meta/internal/metals/FormattingProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import org.scalafmt.dynamic.ScalafmtDynamicError
import org.scalafmt.interfaces.PositionException
import org.scalafmt.interfaces.Scalafmt
import org.scalafmt.interfaces.ScalafmtReporter
import scala.meta.io.RelativePath

/**
* Implement text formatting using Scalafmt
Expand Down Expand Up @@ -68,6 +69,7 @@ final class FormattingProvider(
)
}

private val defaultScalafmtLocation = RelativePath(".scalafmt.conf")
private var scalafmt = FormattingProvider.newScalafmt()
private val reporterPromise =
new AtomicReference[Option[Promise[Boolean]]](None)
Expand All @@ -82,19 +84,21 @@ final class FormattingProvider(
// Warms up the Scalafmt instance so that the first formatting request responds faster.
// Does nothing if there is no .scalafmt.conf or there is no configured version setting.
def load(): Unit = {
if (scalafmtConf(workspace).isFile && !Testing.isEnabled) {
try {
scalafmt.format(
scalafmtConf(workspace).toNIO,
Paths.get("Main.scala"),
"object Main {}",
)
} catch {
case e: ScalafmtDynamicError =>
scribe.debug(
s"Scalafmt issue while warming up due to config issue: ${e.getMessage()}"
scalafmtConf(workspace) match {
case Some(conf) if !Testing.isEnabled =>
try {
scalafmt.format(
conf.toNIO,
Paths.get("Main.scala"),
"object Main {}",
)
}
} catch {
case e: ScalafmtDynamicError =>
scribe.debug(
s"Scalafmt issue while warming up due to config issue: ${e.getMessage()}"
)
}
case _ =>

}
}
Expand All @@ -104,47 +108,48 @@ final class FormattingProvider(
projectRoot: AbsolutePath,
token: CancelChecker,
): Future[util.List[l.TextEdit]] = {
scalafmt = scalafmt.withReporter(activeReporter)
scalafmt = scalafmt.withReporter(activeReporter(projectRoot))
reset(token)
val input = path.toInputFromBuffers(buffers)
if (!scalafmtConf(projectRoot).isFile) {
handleMissingFile(scalafmtConf(projectRoot)).map {
case true =>
runFormat(path, projectRoot, input).asJava
case false =>
Collections.emptyList[l.TextEdit]()
}
} else {
val result = runFormat(path, projectRoot, input)
if (token.isCancelled) {
statusBar.addMessage(
s"${icons.info}Scalafmt cancelled by editor, try saving file again"
)
}
reporterPromise.get() match {
case Some(promise) =>
// Wait until "update .scalafmt.conf" dialogue has completed
// before returning future.
promise.future.map {
case true if !token.isCancelled =>
runFormat(path, projectRoot, input).asJava
case _ => result.asJava
}
case None =>
Future.successful(result.asJava)
}
scalafmtConf(projectRoot) match {
case None =>
handleMissingFile(projectRoot).map {
case true =>
runFormat(path, projectRoot, input).asJava
case false =>
Collections.emptyList[l.TextEdit]()
}
case Some(config) =>
val result = runFormat(path, config, input)
if (token.isCancelled) {
statusBar.addMessage(
s"${icons.info}Scalafmt cancelled by editor, try saving file again"
)
}
reporterPromise.get() match {
case Some(promise) =>
// Wait until "update .scalafmt.conf" dialogue has completed
// before returning future.
promise.future.map {
case true if !token.isCancelled =>
runFormat(path, projectRoot, input).asJava
case _ => result.asJava
}
case None =>
Future.successful(result.asJava)
}
}
}

private def runFormat(
path: AbsolutePath,
projectRoot: AbsolutePath,
scalafmtConf: AbsolutePath,
input: Input,
): List[l.TextEdit] = {
val fullDocumentRange = Position.Range(input, 0, input.chars.length).toLsp
val formatted =
try {
scalafmt.format(scalafmtConf(projectRoot).toNIO, path.toNIO, input.text)
scalafmt.format(scalafmtConf.toNIO, path.toNIO, input.text)
} catch {
case e: ScalafmtDynamicError =>
scribe.debug(
Expand Down Expand Up @@ -210,14 +215,16 @@ final class FormattingProvider(
} else Future.successful(None)
}

private def handleMissingFile(path: AbsolutePath): Future[Boolean] = {
private def handleMissingFile(projectRoot: AbsolutePath): Future[Boolean] = {
if (!tables.dismissedNotifications.CreateScalafmtFile.isDismissed) {
val params = MissingScalafmtConf.params()
client.showMessageRequest(params).asScala.map { item =>
if (item == MissingScalafmtConf.createFile) {
Files.createDirectories(path.toNIO.getParent)
Files
.write(path.toNIO, initialConfig().getBytes(StandardCharsets.UTF_8))
.write(
projectRoot.resolve(defaultScalafmtLocation).toNIO,
initialConfig().getBytes(StandardCharsets.UTF_8),
)
client.showMessage(MissingScalafmtConf.fixedParams(isCancelled))
true
} else if (item == Messages.notNow) {
Expand Down Expand Up @@ -359,15 +366,14 @@ final class FormattingProvider(

private def checkIfDialectUpgradeRequired(
config: ScalafmtConfig,
projectRoot: AbsolutePath,
configPath: AbsolutePath,
): Future[Unit] = {
if (tables.dismissedNotifications.UpdateScalafmtConf.isDismissed)
Future.unit
else {
Future(inspectDialectRewrite(config)).flatMap {
case Some(rewrite) =>
val canUpdate =
rewrite.canUpdate && scalafmtConf(projectRoot).isInside(projectRoot)
val canUpdate = rewrite.canUpdate && configPath.isInside(workspace)
val params =
UpdateScalafmtConf.params(rewrite.maxDialect, canUpdate)

Expand All @@ -378,11 +384,10 @@ final class FormattingProvider(

client.showMessageRequest(params).asScala.map { item =>
if (item == UpdateScalafmtConf.letUpdate) {
val text =
scalafmtConf(projectRoot).toInputFromBuffers(buffers).text
val text = configPath.toInputFromBuffers(buffers).text
val updatedText = rewrite.rewrite(text)
Files.write(
scalafmtConf(projectRoot).toNIO,
configPath.toNIO,
updatedText.getBytes(StandardCharsets.UTF_8),
)
} else if (item == Messages.notNow) {
Expand All @@ -398,101 +403,118 @@ final class FormattingProvider(
}

def validateWorkspace(projectRoot: AbsolutePath): Future[Unit] = {
if (scalafmtConf(projectRoot).exists) {
val text = scalafmtConf(projectRoot).toInputFromBuffers(buffers).text
ScalafmtConfig.parse(text) match {
case Failure(e) =>
scribe.error(s"Failed to parse ${scalafmtConf(projectRoot)}", e)
Future.unit
case Success(values) =>
checkIfDialectUpgradeRequired(values, projectRoot)
}
} else {
Future.unit
scalafmtConf(projectRoot) match {
case Some(conf) =>
val text = conf.toInputFromBuffers(buffers).text
ScalafmtConfig.parse(text) match {
case Failure(e) =>
scribe.error(s"Failed to parse ${conf}", e)
Future.unit
case Success(values) =>
checkIfDialectUpgradeRequired(values, conf)
}
case None =>
Future.unit
}
}

private def scalafmtConf(projectRoot: AbsolutePath): AbsolutePath = {
private def scalafmtConf(projectRoot: AbsolutePath): Option[AbsolutePath] = {
val configpath = userConfig().scalafmtConfigPath
configpath.getOrElse(projectRoot.resolve(".scalafmt.conf"))
val default: Option[AbsolutePath] = {
val defaultLocation = projectRoot.resolve(defaultScalafmtLocation)
lazy val scalacliDefault =
projectRoot.resolve(".scala-build/.scalafmt.conf")
if (defaultLocation.exists) Some(defaultLocation)
else if (scalacliDefault.exists) Some(scalacliDefault)
else None
}
configpath.orElse(default)
}

private val activeReporter: ScalafmtReporter = new ScalafmtReporter {
private var downloadingScalafmt = Promise[Unit]()
override def error(file: Path, message: String): Unit = {
scribe.error(s"scalafmt: $file: $message")
if (file == scalafmtConf(workspace).toNIO) {
downloadingScalafmt.trySuccess(())
if (message.contains("failed to resolve Scalafmt version")) {
client.showMessage(MissingScalafmtVersion.failedToResolve(message))
}
val input = scalafmtConf(workspace).toInputFromBuffers(buffers)
val pos = Position.Range(input, 0, input.chars.length)
client.publishDiagnostics(
new l.PublishDiagnosticsParams(
file.toUri.toString,
Collections.singletonList(
new l.Diagnostic(
new l.Range(
new l.Position(0, 0),
new l.Position(pos.endLine, pos.endColumn),
private def activeReporter(projectRoot: AbsolutePath): ScalafmtReporter =
new ScalafmtReporter {
private var downloadingScalafmt = Promise[Unit]()
override def error(file: Path, message: String): Unit = {
scribe.error(s"scalafmt: $file: $message")
scalafmtConf(projectRoot) match {
case Some(conf) if file == conf.toNIO =>
downloadingScalafmt.trySuccess(())
if (message.contains("failed to resolve Scalafmt version")) {
client.showMessage(
MissingScalafmtVersion.failedToResolve(message)
)
}
val input = conf.toInputFromBuffers(buffers)
val pos = Position.Range(input, 0, input.chars.length)
client.publishDiagnostics(
new l.PublishDiagnosticsParams(
file.toUri.toString,
Collections.singletonList(
new l.Diagnostic(
new l.Range(
new l.Position(0, 0),
new l.Position(pos.endLine, pos.endColumn),
),
message,
l.DiagnosticSeverity.Error,
"scalafmt",
)
),
message,
l.DiagnosticSeverity.Error,
"scalafmt",
)
),
)
)
)
case _ =>
}
}
}

override def error(file: Path, e: Throwable): Unit = {
downloadingScalafmt.trySuccess(())
e match {
case p: PositionException =>
statusBar.addMessage(
s"${icons.alert}line ${p.startLine() + 1}: ${p.shortMessage()}"
)
scribe.error(s"scalafmt: ${p.longMessage()}")
case _ =>
scribe.error(s"scalafmt: $file", e)
override def error(file: Path, e: Throwable): Unit = {
downloadingScalafmt.trySuccess(())
e match {
case p: PositionException =>
statusBar.addMessage(
s"${icons.alert}line ${p.startLine() + 1}: ${p.shortMessage()}"
)
scribe.error(s"scalafmt: ${p.longMessage()}")
case _ =>
scribe.error(s"scalafmt: $file", e)
}
}
override def excluded(file: Path): Unit = {
scribe.info(
s"scalafmt: excluded $file (to format this file, update `project.excludeFilters` in .scalafmt.conf)"
)
}
}
override def excluded(file: Path): Unit = {
scribe.info(
s"scalafmt: excluded $file (to format this file, update `project.excludeFilters` in .scalafmt.conf)"
)
}

override def parsedConfig(config: Path, scalafmtVersion: String): Unit = {
downloadingScalafmt.trySuccess(())
clearDiagnostics(AbsolutePath(config))
}
override def parsedConfig(config: Path, scalafmtVersion: String): Unit = {
downloadingScalafmt.trySuccess(())
clearDiagnostics(AbsolutePath(config))
}

override def missingVersion(config: Path, defaultVersion: String): Unit = {
val promise = Promise[Boolean]()
reporterPromise.set(Some(promise))
promise.completeWith(handleMissingVersion(AbsolutePath(config)))
super.missingVersion(config, defaultVersion)
}
override def missingVersion(
config: Path,
defaultVersion: String,
): Unit = {
val promise = Promise[Boolean]()
reporterPromise.set(Some(promise))
promise.completeWith(handleMissingVersion(AbsolutePath(config)))
super.missingVersion(config, defaultVersion)
}

def downloadOutputStreamWriter(): OutputStreamWriter =
new OutputStreamWriter(downloadOutputStream())
def downloadOutputStreamWriter(): OutputStreamWriter =
new OutputStreamWriter(downloadOutputStream())

def downloadOutputStream(): OutputStream = {
downloadingScalafmt.trySuccess(())
downloadingScalafmt = Promise()
statusBar.trackSlowFuture(
"Loading Scalafmt",
downloadingScalafmt.future,
)
System.out
}
override def downloadWriter(): PrintWriter = {
new PrintWriter(downloadOutputStream())
def downloadOutputStream(): OutputStream = {
downloadingScalafmt.trySuccess(())
downloadingScalafmt = Promise()
statusBar.trackSlowFuture(
"Loading Scalafmt",
downloadingScalafmt.future,
)
System.out
}
override def downloadWriter(): PrintWriter = {
new PrintWriter(downloadOutputStream())
}
}
}
}

object FormattingProvider {
Expand Down

0 comments on commit 3ef771b

Please sign in to comment.