diff --git a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala index ed5432d1b53..ff276e43306 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala @@ -946,11 +946,12 @@ class Compilers( ): Option[PresentationCompiler] = { val pc = JavaPresentationCompiler() Some( - configure(pc, search).newInstance( - targetUri, - classpath.toAbsoluteClasspath.map(_.toNIO).toSeq.asJava, - log.asJava, - ) + configure(pc, search) + .newInstance( + targetUri, + classpath.toAbsoluteClasspath.map(_.toNIO).toSeq.asJava, + log.asJava, + ) ) } @@ -1119,7 +1120,7 @@ class Compilers( classpath, search, target.scalac.getTarget.getUri, - ) + ).withBuildTargetName(target.displayName) } def newCompiler( @@ -1138,11 +1139,12 @@ class Compilers( } val filteredOptions = plugins.filterSupportedOptions(options) - configure(pc, search).newInstance( - name, - classpath.asJava, - (log ++ filteredOptions).asJava, - ) + configure(pc, search) + .newInstance( + name, + classpath.asJava, + (log ++ filteredOptions).asJava, + ) } private def toDebugCompletionType( diff --git a/metals/src/main/scala/scala/meta/internal/metals/LoggerContext.scala b/metals/src/main/scala/scala/meta/internal/metals/LoggerContext.scala index 20dee8c73a2..da38b5dc93e 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/LoggerContext.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/LoggerContext.scala @@ -6,8 +6,11 @@ import scala.meta.internal.metals.utils.TimestampedFile object LoggerReporter extends Reporter { + override def name: String = "logger-report" override def create(report: => Report, ifVerbose: Boolean): Option[Path] = { - scribe.info(s"Report ${report.name}: ${report.fullText}") + scribe.info( + s"Report ${report.name}: ${report.fullText(withIdAndSummary = false)}" + ) None } diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala index 1c713e79a0f..2eed2086b75 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala @@ -627,13 +627,13 @@ object MetalsEnrichments try { Some(toAbsolutePath) } catch { - case NonFatal(e) => + case NonFatal(error) => reports.incognito.create( Report( "absolute-path", s"""|Uri: $value |""".stripMargin, - e, + error, ) ) None diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index ab0db75a147..11df0204c71 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -176,6 +176,13 @@ class MetalsLspService( implicit val reports: StdReportContext = new StdReportContext( folder.toNIO, + _.flatMap { uri => + for { + filePath <- uri.toAbsolutePathSafe + buildTargetId <- buildTargets.inverseSources(filePath) + name <- buildTargets.info(buildTargetId).map(_.getDisplayName()) + } yield name + }, ReportLevel.fromString(MetalsServerConfig.default.loglevel), ) diff --git a/metals/src/main/scala/scala/meta/internal/metals/doctor/Doctor.scala b/metals/src/main/scala/scala/meta/internal/metals/doctor/Doctor.scala index 9cf655b004b..7b9cc7a8302 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/doctor/Doctor.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/doctor/Doctor.scala @@ -2,6 +2,9 @@ package scala.meta.internal.metals.doctor import java.net.URLEncoder import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.text.SimpleDateFormat +import java.util.Date import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -30,9 +33,15 @@ import scala.meta.internal.metals.Messages.CheckDoctor import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.MtagsResolver import scala.meta.internal.metals.PopupChoiceReset +import scala.meta.internal.metals.Report +import scala.meta.internal.metals.ReportContext +import scala.meta.internal.metals.ReportFileName import scala.meta.internal.metals.ScalaTarget +import scala.meta.internal.metals.ServerCommands +import scala.meta.internal.metals.StdReportContext import scala.meta.internal.metals.Tables import scala.meta.internal.metals.clients.language.MetalsLanguageClient +import scala.meta.internal.metals.utils.TimestampedFile import scala.meta.io.AbsolutePath import ch.epfl.scala.bsp4j.BuildTargetIdentifier @@ -57,7 +66,7 @@ final class Doctor( maybeJDKVersion: Option[JdkVersion], folderName: String, buildTools: BuildTools, -)(implicit ec: ExecutionContext) { +)(implicit ec: ExecutionContext, rc: ReportContext) { private val hasProblems = new AtomicBoolean(false) private val problemResolver = new ProblemResolver( @@ -203,6 +212,7 @@ final class Doctor( ), None, List.empty, + getErrorReports(), ) } else { val allTargetsInfo = targetIds @@ -224,17 +234,39 @@ final class Doctor( None, Some(allTargetsInfo), explanations, + getErrorReports(), ) } } + private def getErrorReports(): List[ErrorReportInfo] = + for { + provider <- rc.all + report <- provider.getReports() + } yield { + val (name, buildTarget) = + ReportFileName.getReportNameAndBuildTarget(report) + ErrorReportInfo( + name, + report.timestamp, + report.toPath.toUri().toString(), + buildTarget, + Doctor.getErrorReportSummary(report, workspace).getOrElse(""), + provider.name, + ) + } + private def gotoBuildTargetCommand( workspace: AbsolutePath, buildTargetName: String, - ): String = { - val uriAsStr = FileDecoderProvider - .createBuildTargetURI(workspace, buildTargetName) - .toString + ): String = + goToCommand( + FileDecoderProvider + .createBuildTargetURI(workspace, buildTargetName) + .toString + ) + + private def goToCommand(uri: String): String = clientConfig .commandInHtmlFormat() .map(format => { @@ -242,11 +274,20 @@ final class Doctor( new l.Position(0, 0), new l.Position(0, 0), ) - val location = ClientCommands.WindowLocation(uriAsStr, range) + val location = ClientCommands.WindowLocation(uri, range) ClientCommands.GotoLocation.toCommandLink(location, format) }) - .getOrElse(uriAsStr) - } + .getOrElse(uri) + + private def zipReports(): Option[String] = + clientConfig + .commandInHtmlFormat() + .map(ServerCommands.ZipReports.toCommandLink(_)) + + private def createGithubIssue(): Option[String] = + clientConfig + .commandInHtmlFormat() + .map(ServerCommands.OpenIssue.toCommandLink(_)) private def resetChoiceCommand(choice: String): String = { val param = s"""["$choice"]""" @@ -304,6 +345,7 @@ final class Doctor( } val targetIds = allTargetIds() + val errorReports = getErrorReports().groupBy(_.buildTarget) if (targetIds.isEmpty) { html .element("p")( @@ -330,9 +372,12 @@ final class Doctor( .element("th")(_.text("Semanticdb")) .element("th")(_.text("Debugging")) .element("th")(_.text("Java support")) + .element("th")(_.text("Error reports")) .element("th")(_.text("Recommendation")) ) - ).element("tbody")(html => buildTargetRows(html, allTargetsInfo)) + ).element("tbody")(html => + buildTargetRows(html, allTargetsInfo, errorReports) + ) ) // Additional explanations @@ -342,17 +387,74 @@ final class Doctor( DoctorExplanation.SemanticDB.toHtml(html, allTargetsInfo) DoctorExplanation.Debugging.toHtml(html, allTargetsInfo) DoctorExplanation.JavaSupport.toHtml(html, allTargetsInfo) + + addErrorReportsInfo(html, errorReports) } } + private def addErrorReportsInfo( + html: HtmlBuilder, + errorReports: Map[Option[String], List[ErrorReportInfo]], + ) = { + html.element("h2")(_.text("Error reports:")) + errorReports.toVector + .sortWith { + case (Some(v1) -> _, Some(v2) -> _) => v1 < v2 + case (None -> _, _ -> _) => false + case (_ -> _, None -> _) => true + } + .foreach { case (optBuildTarget, reports) => + def name(default: String) = optBuildTarget.getOrElse(default) + html.element("details")(details => + details + .element("summary", s"id=reports-${name("other")}")( + _.element("b")( + _.text(s"${name("Other error reports")} (${reports.length}):") + ) + ) + .element("table") { table => + reports.foreach { report => + val reportName = report.name.replaceAll("[_-]", " ") + val dateTime = dateTimeFormat.format(new Date(report.timestamp)) + table.element("tr")(tr => + tr.element("td")(_.raw(Icons.unicode.folder)) + .element("td")(_.link(goToCommand(report.uri), reportName)) + .element("td")(_.text(dateTime)) + .element("td")(_.text(report.shortSummary)) + ) + } + } + ) + } + for { + zipReportsCommand <- zipReports() + createIssueCommand <- createGithubIssue() + } html.element("p")( + _.text( + "You can attach a single error report or a couple or reports in a zip file " + ) + .link(zipReportsCommand, "(create a zip file from anonymized reports)") + .text(" to your GitHub issue ") + .link(createIssueCommand, "(create a github issue)") + .text(" to help with debugging.") + ) + } + private def buildTargetRows( html: HtmlBuilder, infos: Seq[DoctorTargetInfo], + errorReports: Map[Option[String], List[ErrorReportInfo]], ): Unit = { infos .sortBy(f => (f.baseDirectory, f.name, f.dataKind)) .foreach { targetInfo => val center = "style='text-align: center'" + def addErrorReportText(html: HtmlBuilder) = + errorReports.getOrElse(Some(targetInfo.name), List.empty) match { + case Nil => html.text(Icons.unicode.check) + case _ => + html.link(s"#reports-${targetInfo.name}", Icons.unicode.alert) + } html.element("tr")( _.element("td")(_.link(targetInfo.gotoCommand, targetInfo.name)) .element("td")(_.text(targetInfo.targetType)) @@ -370,6 +472,7 @@ final class Doctor( _.text(targetInfo.debuggingStatus.explanation) ) .element("td", center)(_.text(targetInfo.javaStatus.explanation)) + .element("td", center)(addErrorReportText) .element("td")(_.raw(targetInfo.recommenedFix)) ) } @@ -531,8 +634,36 @@ final class Doctor( "Try removing the directories .metals/ and .bloop/, then restart metals And import the build again." private val buildServerNotResponsive = "Build server is not responding." + private val dateTimeFormat = new SimpleDateFormat("dd MMM HH:mm:ss") } case class DoctorVisibilityDidChangeParams( visible: Boolean ) + +object Doctor { + + /** + * Extracts the short summary [[scala.meta.internal.metals.Report.shortSummary]] + * from an error report file. + * @param file error report file + * @param root workspace folder root + * @return short summary or nothing if no summary or error while reading the file + */ + def getErrorReportSummary( + file: TimestampedFile, + root: AbsolutePath, + ): Option[String] = { + def decode(text: String) = + text.replace(StdReportContext.WORKSPACE_STR, root.toString()) + for { + lines <- Try(Files.readAllLines(file.toPath).asScala.toList).toOption + index = lines.lastIndexWhere(_.startsWith(Report.summaryTitle)) + if index >= 0 + } yield lines + .drop(index + 1) + .dropWhile(_ == "") + .map(decode) + .mkString("\n") + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/doctor/DoctorResults.scala b/metals/src/main/scala/scala/meta/internal/metals/doctor/DoctorResults.scala index 3c27255d2ee..71ff6b8b056 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/doctor/DoctorResults.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/doctor/DoctorResults.scala @@ -20,7 +20,7 @@ final case class DoctorResults( object DoctorResults { // Version of the Doctor json that is returned. - val version = 4 + val version = 5 } final case class DoctorFolderResults( @@ -29,6 +29,7 @@ final case class DoctorFolderResults( messages: Option[List[DoctorMessage]], targets: Option[Seq[DoctorTargetInfo]], explanations: List[Obj], + errorReports: List[ErrorReportInfo], ) { def toJson: Obj = { val json = ujson.Obj( @@ -40,6 +41,7 @@ final case class DoctorFolderResults( ) targets.foreach(targetList => json("targets") = targetList.map(_.toJson)) json("explanations") = explanations + json("errorReports") = errorReports.map(_.toJson) json } } @@ -152,3 +154,33 @@ final case class DoctorFolderHeader( base } } + +/** + * Information about an error report. + * @param name display name of the error + * @param timestamp date and time timestamp of the report + * @param uri error report file uri + * @param buildTarget optional build target that error is associated with + * @param shortSummary short error summary + * @param errorReportType one of "metals", "metals-full", "bloop" + */ +final case class ErrorReportInfo( + name: String, + timestamp: Long, + uri: String, + buildTarget: Option[String], + shortSummary: String, + errorReportType: String, +) { + def toJson: Obj = { + val json = ujson.Obj( + "name" -> name, + "timestamp" -> timestamp, + "uri" -> uri, + "shortSummary" -> shortSummary, + "errorReportType" -> errorReportType, + ) + buildTarget.foreach(json("buildTarget") = _) + json + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/logging/MetalsLogger.scala b/metals/src/main/scala/scala/meta/internal/metals/logging/MetalsLogger.scala index c55a149ce4f..e7edc5b2040 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/logging/MetalsLogger.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/logging/MetalsLogger.scala @@ -168,7 +168,7 @@ object MetalsLogger { private def limitKeptBackupLogs(workspaceFolder: AbsolutePath, limit: Int) = { val backupDir = backupLogsDir(workspaceFolder) - new LimitedFilesManager(backupDir.toNIO, limit, "log_").deleteOld() + new LimitedFilesManager(backupDir.toNIO, limit, "log_".r, "").deleteOld() } def newFileWriter(logfile: AbsolutePath): FileWriter = diff --git a/metals/src/main/scala/scala/meta/internal/parsing/Trees.scala b/metals/src/main/scala/scala/meta/internal/parsing/Trees.scala index c22b2926e71..4c9c6d1d4e7 100644 --- a/metals/src/main/scala/scala/meta/internal/parsing/Trees.scala +++ b/metals/src/main/scala/scala/meta/internal/parsing/Trees.scala @@ -145,6 +145,8 @@ final class Trees( Report( s"stackoverflow_${path.filename}", text, + s"Stack overflow in ${path.filename}", + path = Some(path.toURI.toString()), ) ) val message = diff --git a/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala b/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala index a74ce31ce5d..9cef353b2fb 100644 --- a/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala +++ b/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala @@ -202,7 +202,7 @@ class MetalsLanguageServer( metalsService.underlying = service folderPathsWithScala.foreach(folder => - new StdReportContext(folder.toNIO).cleanUpOldReports() + new StdReportContext(folder.toNIO, _ => None).cleanUpOldReports() ) service.initialize() diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java b/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java index 1427e3f2cbc..1d60f573ea0 100644 --- a/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java +++ b/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java @@ -43,7 +43,7 @@ public abstract class PresentationCompiler { * */ public CompletableFuture> semanticTokens(VirtualFileParams params) { - + return CompletableFuture.completedFuture(Collections.emptyList()); } @@ -208,6 +208,13 @@ public PresentationCompiler withReportsLoggerLevel(String level) { return this; }; + /** + * Set build target name. + */ + public PresentationCompiler withBuildTargetName(String buildTargetName) { + return this; + }; + /** * Provide a SymbolSearch to extract docstrings, java parameter names and Scala * default parameter values. @@ -252,7 +259,7 @@ public abstract PresentationCompiler withScheduledExecutorService( * better-monadic-for. */ public abstract PresentationCompiler newInstance(String buildTargetIdentifier, List classpath, - List options); + List options); // ============================= // Intentionally missing methods diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportContext.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportContext.scala index fb1a3c0cef3..dc48d9b7f98 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportContext.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportContext.scala @@ -4,6 +4,8 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import scala.util.matching.Regex + import scala.meta.internal.metals.utils.LimitedFilesManager import scala.meta.internal.metals.utils.TimestampedFile import scala.meta.internal.mtags.CommonMtagsEnrichments._ @@ -21,6 +23,7 @@ trait ReportContext { } trait Reporter { + def name: String def create(report: => Report, ifVerbose: Boolean = false): Option[Path] def cleanUpOldReports( maxReportsNumber: Int = StdReportContext.MAX_NUMBER_OF_REPORTS @@ -29,27 +32,36 @@ trait Reporter { def deleteAll(): Unit } -class StdReportContext(workspace: Path, level: ReportLevel = ReportLevel.Info) - extends ReportContext { +class StdReportContext( + workspace: Path, + resolveBuildTarget: Option[String] => Option[String], + level: ReportLevel = ReportLevel.Info +) extends ReportContext { val reportsDir: Path = workspace.resolve(StdReportContext.reportsDir) val unsanitized = new StdReporter( workspace, - StdReportContext.reportsDir.resolve("metals-full"), - level + StdReportContext.reportsDir, + resolveBuildTarget, + level, + "metals-full" ) val incognito = new StdReporter( workspace, - StdReportContext.reportsDir.resolve("metals"), - level + StdReportContext.reportsDir, + resolveBuildTarget, + level, + "metals" ) val bloop = new StdReporter( workspace, - StdReportContext.reportsDir.resolve("bloop"), - level + StdReportContext.reportsDir, + resolveBuildTarget, + level, + "bloop" ) override def cleanUpOldReports( @@ -65,29 +77,36 @@ class StdReportContext(workspace: Path, level: ReportLevel = ReportLevel.Info) } } -class StdReporter(workspace: Path, pathToReports: Path, level: ReportLevel) - extends Reporter { - private lazy val maybeReportsDir: Path = workspace.resolve(pathToReports) +class StdReporter( + workspace: Path, + pathToReports: Path, + resolveBuildTarget: Option[String] => Option[String], + level: ReportLevel, + override val name: String +) extends Reporter { + private lazy val maybeReportsDir: Path = + workspace.resolve(pathToReports).resolve(name) private lazy val reportsDir = maybeReportsDir.createDirectories() private val limitedFilesManager = new LimitedFilesManager( maybeReportsDir, StdReportContext.MAX_NUMBER_OF_REPORTS, - "r_.*_" + ReportFileName.pattern, + ".md" ) private lazy val userHome = Option(System.getProperty("user.home")) private var initialized = false private var reported = Set.empty[String] - private val idPrefix = "id: " def readInIds(): Unit = { reported = getReports().flatMap { report => val lines = Files.readAllLines(report.file.toPath()) if (lines.size() > 0) { lines.get(0) match { - case id if id.startsWith(idPrefix) => Some(id.stripPrefix(idPrefix)) + case id if id.startsWith(Report.idPrefix) => + Some(id.stripPrefix(Report.idPrefix)) case _ => None } } else None @@ -107,11 +126,10 @@ class StdReporter(workspace: Path, pathToReports: Path, level: ReportLevel) val sanitizedId = report.id.map(sanitize) if (sanitizedId.isDefined && reported.contains(sanitizedId.get)) None else { - val path = reportPath(report.name) + val path = reportPath(report) path.getParent.createDirectories() sanitizedId.foreach(reported += _) - val idString = sanitizedId.map(id => s"$idPrefix$id\n").getOrElse("") - path.writeText(s"$idString${sanitize(report.fullText)}") + path.writeText(sanitize(report.fullText(withIdAndSummary = true))) Some(path) } } @@ -124,10 +142,12 @@ class StdReporter(workspace: Path, pathToReports: Path, level: ReportLevel) .getOrElse(textAfterWokspaceReplace) } - private def reportPath(name: String): Path = { + private def reportPath(report: Report): Path = { val date = TimeFormatter.getDate() val time = TimeFormatter.getTime() - val filename = s"r_${name}_${time}" + val buildTargetPart = + resolveBuildTarget(report.path).map("_(" ++ _ ++ ")").getOrElse("") + val filename = s"r_${report.name}${buildTargetPart}_${time}.md" reportsDir.resolve(date).resolve(filename) } @@ -158,6 +178,7 @@ object StdReportContext { object EmptyReporter extends Reporter { + override def name = "empty-reporter" override def create(report: => Report, ifVerbose: Boolean): Option[Path] = None @@ -181,6 +202,8 @@ object EmptyReportContext extends ReportContext { case class Report( name: String, text: String, + shortSummary: String, + path: Option[String] = None, id: Option[String] = None, error: Option[Throwable] = None ) { @@ -190,24 +213,57 @@ case class Report( |$moreInfo"""".stripMargin ) - def fullText: String = + def fullText(withIdAndSummary: Boolean): String = { + val sb = new StringBuilder + if (withIdAndSummary) { + id.foreach(id => sb.append(s"${Report.idPrefix}$id\n")) + } + path.foreach(path => sb.append(s"$path\n")) error match { case Some(error) => - s"""|$error - |$text - | - |error stacktrace: - |${error.getStackTrace().mkString("\n\t")} - |""".stripMargin - case None => text + sb.append( + s"""|### $error + | + |$text + | + |#### Error stacktrace: + | + |``` + |${error.getStackTrace().mkString("\n\t")} + |``` + |""".stripMargin + ) + case None => sb.append(s"$text\n") } + if (withIdAndSummary) + sb.append(s"""|${Report.summaryTitle} + | + |$shortSummary""".stripMargin) + sb.result() + } } object Report { - def apply(name: String, text: String, id: String): Report = - Report(name, text, id = Some(id)) + + def apply( + name: String, + text: String, + error: Throwable, + path: Option[String] + ): Report = + Report( + name, + text, + shortSummary = error.toString(), + path = path, + error = Some(error) + ) + def apply(name: String, text: String, error: Throwable): Report = - Report(name, text, error = Some(error)) + Report(name, text, error, path = None) + + val idPrefix = "id: " + val summaryTitle = "#### Short summary: " } sealed trait ReportLevel { @@ -229,3 +285,17 @@ object ReportLevel { case _ => Info } } + +object ReportFileName { + val pattern: Regex = "r_(?[^()]*)(_\\((?.*)\\))?_".r + + def getReportNameAndBuildTarget( + file: TimestampedFile + ): (String, Option[String]) = + pattern.findPrefixMatchOf(file.name) match { + case None => (file.name, None) + case Some(foundMatch) => + (foundMatch.group("name"), Option(foundMatch.group("buildTarget"))) + } + +} diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala index 365b4f94db9..e2bff5dc7cc 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala @@ -4,14 +4,17 @@ import java.io.File import java.nio.file.Files import java.nio.file.Path +import scala.util.Try +import scala.util.matching.Regex + import scala.meta.internal.metals.TimeFormatter class LimitedFilesManager( directory: Path, fileLimit: Int, - prefixPattern: String + prefixRegex: Regex, + extension: String ) { - private val fileNameRegex = s"${prefixPattern}([-+]?[0-9]+)".r def getAllFiles(): List[TimestampedFile] = { if (Files.exists(directory) && Files.isDirectory(directory)) { @@ -46,10 +49,11 @@ class LimitedFilesManager( } private def timestampedFile(file: File): Option[TimestampedFile] = { - file.getName() match { - case fileNameRegex(time) => Some(TimestampedFile(file, time.toLong)) - case _: String => None - } + for { + reMatch <- prefixRegex.findPrefixMatchOf(file.getName()) + timeStr = reMatch.after.toString.stripSuffix(extension) + time <- Try(timeStr.toLong).toOption + } yield TimestampedFile(file, time) } private def filesWithDate(dir: File): List[TimestampedFile] = { @@ -71,11 +75,10 @@ class LimitedFilesManager( } object WithTimestamp { - private val prefix = prefixPattern.r def unapply(filename: String): Option[String] = { for { - prefixMatch <- prefix.findPrefixMatchOf(filename) - time = prefixMatch.after.toString + prefixMatch <- prefixRegex.findPrefixMatchOf(filename) + time = prefixMatch.after.toString.stripSuffix(extension) } yield time } } diff --git a/mtags-shared/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala b/mtags-shared/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala index 36fda09576c..0c9c59486d2 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala @@ -358,16 +358,25 @@ trait CommonMtagsEnrichments { s"""|range: ${r.start} - ${r.end} |uri: ${r.uri()} |text: - |$withMarkers""".stripMargin + |```scala + |$withMarkers + |``` + |""".stripMargin case o: OffsetParams => s"""|offset: ${o.offset()} |uri: ${o.uri()} |text: - |${textWithPosMarker(o.offset(), o.text())}""".stripMargin + |```scala + |${textWithPosMarker(o.offset(), o.text())} + |``` + |""".stripMargin case v => s"""|uri: ${v.uri()} |text: - |${v.text()}""".stripMargin + |```scala + |${v.text()} + |``` + |""".stripMargin } } } diff --git a/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala b/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala index 255cc2c2354..8f3fcd2c13e 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala @@ -202,7 +202,8 @@ abstract class CompilerAccess[Reporter, Compiler]( |action parameters: |${params.map(_.printed()).getOrElse("")} |""".stripMargin, - error + error, + path = params.map(_.uri().toString) ) val pathToReport = rc.unsanitized.create(report) diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/HoverProvider.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/HoverProvider.scala index 89b7a0bdff3..ad2cb19384c 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/HoverProvider.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/HoverProvider.scala @@ -55,20 +55,25 @@ class HoverProvider(val compiler: MetalsGlobal, params: OffsetParams)(implicit Report( "empty-hover-scala2", - s"""|$fileName - |pos: ${pos.toLsp} + s"""|pos: ${pos.toLsp} | |is error: $hasErroneousType |symbol: ${tree.symbol} |tpe: ${tree.tpe} | |tree: + |```scala |$tree + |``` | |full tree: + |```scala |${unit.body} + |``` |""".stripMargin, - s"$fileName::$posId" + s"empty hover in $fileName", + id = Some(s"$fileName::$posId"), + path = Some(fileName) ) } diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala index 5747b120294..70bf681a9cb 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala @@ -50,6 +50,7 @@ import org.eclipse.lsp4j.TextEdit case class ScalaPresentationCompiler( buildTargetIdentifier: String = "", + buildTargetName: Option[String] = None, classpath: Seq[Path] = Nil, options: List[String] = Nil, search: SymbolSearch = EmptySymbolSearch, @@ -69,9 +70,14 @@ case class ScalaPresentationCompiler( implicit val reportContex: ReportContext = folderPath - .map(new StdReportContext(_, reportsLevel)) + .map(new StdReportContext(_, _ => buildTargetName, reportsLevel)) .getOrElse(EmptyReportContext) + override def withBuildTargetName( + buildTargetName: String + ): ScalaPresentationCompiler = + copy(buildTargetName = Some(buildTargetName)) + override def withReportsLoggerLevel(level: String): PresentationCompiler = copy(reportsLevel = ReportLevel.fromString(level)) diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala index 93e6dfa938a..aef1b57b89e 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala @@ -26,15 +26,15 @@ class CompilerSearchVisitor( case err: AssertionError => logger.log(Level.WARNING, err.getMessage()) false - case NonFatal(e) => + case NonFatal(error) => reports.incognito.create( Report( "is_public", s"""Symbol: $sym""".stripMargin, - e, + error, ) ) - logger.log(Level.SEVERE, e.getMessage(), e) + logger.log(Level.SEVERE, error.getMessage(), error) false private def toSymbols( diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/HoverProvider.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/HoverProvider.scala index 85fff4374d8..29349df2dca 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/HoverProvider.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/HoverProvider.scala @@ -56,8 +56,7 @@ object HoverProvider: else path.head.sourcePos.start Report( "empty-hover-scala3", - s"""|$uri - |pos: ${pos.toLsp} + s"""|pos: ${pos.toLsp} | |tp: $tp |has error: ${tp.isError} @@ -66,11 +65,23 @@ object HoverProvider: |has error: ${tpw.isError} | |path: - |- ${path.map(_.toString()).mkString("\n- ")} + |${path + .map(tree => s"""|```scala + |$tree + |``` + |""".stripMargin) + .mkString("\n")} |trees: - |- ${trees.map(_.toString()).mkString("\n- ")} + |${trees + .map(tree => s"""|```scala + |$tree + |``` + |""".stripMargin) + .mkString("\n")} |""".stripMargin, - s"$uri::$posId", + s"empty hover in $uri", + id = Some(s"$uri::$posId"), + path = Some(uri.toString), ) end report reportContext.unsanitized.create(report, ifVerbose = true) diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala index b92ca56db8b..393a212aa28 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala @@ -31,6 +31,7 @@ import org.eclipse.{lsp4j as l} case class ScalaPresentationCompiler( buildTargetIdentifier: String = "", + buildTargetName: Option[String] = None, classpath: Seq[Path] = Nil, options: List[String] = Nil, search: SymbolSearch = EmptySymbolSearch, @@ -41,7 +42,7 @@ case class ScalaPresentationCompiler( reportsLevel: ReportLevel = ReportLevel.Info, ) extends PresentationCompiler: - def this() = this("", Nil, Nil) + def this() = this("", None, Nil, Nil) val scalaVersion = BuildInfo.scalaCompilerVersion @@ -49,9 +50,12 @@ case class ScalaPresentationCompiler( private val forbiddenDoubleOptions = Set("-release") given ReportContext = folderPath - .map(StdReportContext(_, reportsLevel)) + .map(StdReportContext(_, _ => buildTargetName, reportsLevel)) .getOrElse(EmptyReportContext) + override def withBuildTargetName(buildTargetName: String) = + copy(buildTargetName = Some(buildTargetName)) + override def withReportsLoggerLevel(level: String): PresentationCompiler = copy(reportsLevel = ReportLevel.fromString(level)) diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/printer/MetalsPrinter.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/printer/MetalsPrinter.scala index 3f2f9bb552f..a7e22f878ef 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/printer/MetalsPrinter.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/printer/MetalsPrinter.scala @@ -77,14 +77,11 @@ class MetalsPrinter( val report = Report( "short-name-error", s"""|Error while printing type, could not create short name for type: - | |$tpe - | - |Exception: - |${e.getMessage} - |${e.getStackTrace.mkString("\n")} |""".stripMargin, - tpe.typeSymbol.name.show, + e.toString, + id = Some(tpe.typeSymbol.name.show), + error = Some(e), ) reportContext.unsanitized.create(report, ifVerbose = false) tpe diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala index ca8e71bfd76..c6cafdbb439 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala @@ -784,7 +784,9 @@ class ScalaToplevelMtags( Report( "scala-toplevel-mtags", failMessage(expected), - s"""${input.path}:${newPosition}""" + s"expected $expected; obtained $currentToken", + id = Some(s"""${input.path}:${newPosition}"""), + path = Some(input.path) ) ) } diff --git a/tests/unit/src/main/scala/tests/TestingServer.scala b/tests/unit/src/main/scala/tests/TestingServer.scala index c189bcb29b9..f9d60750e9b 100644 --- a/tests/unit/src/main/scala/tests/TestingServer.scala +++ b/tests/unit/src/main/scala/tests/TestingServer.scala @@ -165,7 +165,8 @@ final case class TestingServer( lazy val fullServer = languageServer.getOldMetalsLanguageServer def server = fullServer.folderServices.head - implicit val reports: ReportContext = new StdReportContext(workspace.toNIO) + implicit val reports: ReportContext = + new StdReportContext(workspace.toNIO, _ => None) private lazy val trees = new Trees( buffers, diff --git a/tests/unit/src/main/scala/tests/TreeUtils.scala b/tests/unit/src/main/scala/tests/TreeUtils.scala index fede2762b09..91e3947e639 100644 --- a/tests/unit/src/main/scala/tests/TreeUtils.scala +++ b/tests/unit/src/main/scala/tests/TreeUtils.scala @@ -21,7 +21,8 @@ object TreeUtils { () => UserConfiguration(fallbackScalaVersion = scalaVersion), buildTargets, ) - implicit val reports = new StdReportContext(Paths.get(".").toAbsolutePath) + implicit val reports = + new StdReportContext(Paths.get(".").toAbsolutePath, _ => None) val trees = new Trees(buffers, selector) (buffers, trees) diff --git a/tests/unit/src/test/scala/tests/LogBackupSuite.scala b/tests/unit/src/test/scala/tests/LogBackupSuite.scala index 971a5a2bbca..e05c7b08188 100644 --- a/tests/unit/src/test/scala/tests/LogBackupSuite.scala +++ b/tests/unit/src/test/scala/tests/LogBackupSuite.scala @@ -25,7 +25,8 @@ class LogBackupSuite extends BaseSuite { def limitedFilesManager = new LimitedFilesManager( backupDir.toNIO, maxLogBackups, - "log_", + "log_".r, + "", ) override def beforeEach(context: BeforeEach): Unit = { diff --git a/tests/unit/src/test/scala/tests/ReportsSuite.scala b/tests/unit/src/test/scala/tests/ReportsSuite.scala index a4c090afe82..0df57467ac8 100644 --- a/tests/unit/src/test/scala/tests/ReportsSuite.scala +++ b/tests/unit/src/test/scala/tests/ReportsSuite.scala @@ -7,14 +7,17 @@ import java.nio.file.Paths import scala.meta.internal.metals.FolderReportsZippper import scala.meta.internal.metals.Icons import scala.meta.internal.metals.Report +import scala.meta.internal.metals.ReportFileName import scala.meta.internal.metals.StdReportContext import scala.meta.internal.metals.TimeFormatter import scala.meta.internal.metals.ZipReportsProvider +import scala.meta.internal.metals.doctor.Doctor import scala.meta.io.AbsolutePath class ReportsSuite extends BaseSuite { val workspace: AbsolutePath = AbsolutePath(Paths.get(".")) - val reportsProvider = new StdReportContext(workspace.toNIO) + val reportsProvider = + new StdReportContext(workspace.toNIO, _.map(_ => "build-target")) val folderReportsZippper: FolderReportsZippper = FolderReportsZippper(exampleBuildTargetsInfo, reportsProvider) @@ -29,6 +32,9 @@ class ReportsSuite extends BaseSuite { |${workspaceStr}/WrongFile.scala |""".stripMargin + def exampleReport(name: String, path: Option[String] = None): Report = + Report(name, exampleText(), "Test error report.", path) + override def afterEach(context: AfterEach): Unit = { reportsProvider.deleteAll() super.afterEach(context) @@ -36,10 +42,17 @@ class ReportsSuite extends BaseSuite { test("create-report") { val path = - reportsProvider.incognito.create(Report("test_error", exampleText())) + reportsProvider.incognito.create(exampleReport("test_error")) val obtained = new String(Files.readAllBytes(path.get), StandardCharsets.UTF_8) - assertNoDiff(exampleText(StdReportContext.WORKSPACE_STR), obtained) + assertNoDiff( + s"""|${exampleText(StdReportContext.WORKSPACE_STR)} + |#### Short summary: + | + |Test error report. + |""".stripMargin, + obtained, + ) assert(reportsProvider.incognito.getReports().length == 1) val dirsWithDate = reportsProvider.reportsDir.resolve("metals").toFile().listFiles() @@ -51,25 +64,42 @@ class ReportsSuite extends BaseSuite { ) } + test("get-name-summary-and-buildTarget") { + val report = exampleReport("test_error") + val report2 = exampleReport("test_error2", Some("")) + reportsProvider.incognito.create(report) + reportsProvider.incognito.create(report2) + val reports = reportsProvider.incognito + .getReports() + .map { report => + val (name, buildTarget) = + ReportFileName.getReportNameAndBuildTarget(report) + val summary = Doctor.getErrorReportSummary(report, workspace) + name -> (buildTarget, summary) + } + .toMap + assertEquals( + reports, + Map( + report.name -> (None, Some(report.shortSummary)), + report2.name -> (Some("build-target"), Some(report2.shortSummary)), + ), + ) + } + test("delete-old-reports") { reportsProvider.incognito.create( - Report("some_test_error_old", exampleText()) + exampleReport("some_test_error_old") ) reportsProvider.incognito.create( - Report( - "some_different_test_error_old", - exampleText(), - ) + exampleReport("some_different_test_error_old") ) Thread.sleep(2) // to make sure, that the new tests have a later timestamp reportsProvider.incognito.create( - Report("some_test_error_new", exampleText()) + exampleReport("some_test_error_new") ) reportsProvider.incognito.create( - Report( - "some_different_test_error_new", - exampleText(), - ) + exampleReport("some_different_test_error_new") ) val deleted = reportsProvider.incognito.cleanUpOldReports(2) deleted match { @@ -86,22 +116,27 @@ class ReportsSuite extends BaseSuite { test("save-with-id") { val testId = "test-id" val path = reportsProvider.incognito.create( - Report("test_error", exampleText(), testId) + Report("test_error", exampleText(), "Test error", id = Some(testId)) ) val obtained = new String(Files.readAllBytes(path.get), StandardCharsets.UTF_8) assertNoDiff( s"""|id: $testId - |${exampleText(StdReportContext.WORKSPACE_STR)}""".stripMargin, + |${exampleText(StdReportContext.WORKSPACE_STR)} + |#### Short summary: + | + |Test error + |""".stripMargin, obtained, ) val none1 = reportsProvider.incognito.create( - Report("test_error_again", exampleText(), testId) + Report("test_error_again", exampleText(), "Test error", id = Some(testId)) ) assert(none1.isEmpty) - val newReportsProvider = new StdReportContext(workspace.toNIO) + val newReportsProvider = + new StdReportContext(workspace.toNIO, _ => Some("buildTarget")) val none2 = newReportsProvider.incognito.create( - Report("test_error_again", exampleText(), testId) + Report("test_error_again", exampleText(), "Test error", id = Some(testId)) ) assert(none2.isEmpty) val reports = newReportsProvider.incognito.getReports() @@ -112,13 +147,8 @@ class ReportsSuite extends BaseSuite { } test("zip-reports") { - reportsProvider.incognito.create(Report("test_error", exampleText())) - reportsProvider.incognito.create( - Report( - "different_test_error", - exampleText(), - ) - ) + reportsProvider.incognito.create(exampleReport("test_error")) + reportsProvider.incognito.create(exampleReport("different_test_error")) val pathToReadMe = ZipReportsProvider.zip(List(folderReportsZippper)) val zipPath = reportsProvider.reportsDir.resolve(StdReportContext.ZIP_FILE_NAME)