diff --git a/metals/src/main/scala/scala/meta/internal/metals/FormattingProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/FormattingProvider.scala index 20a9d9d970b..688135c6b61 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FormattingProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FormattingProvider.scala @@ -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 @@ -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) @@ -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 _ => } } @@ -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( @@ -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) { @@ -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) @@ -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) { @@ -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 {