diff --git a/lib/src/cellar/SourceFetcher.scala b/lib/src/cellar/SourceFetcher.scala index 9162a16..60308c5 100644 --- a/lib/src/cellar/SourceFetcher.scala +++ b/lib/src/cellar/SourceFetcher.scala @@ -1,8 +1,9 @@ package cellar -import cats.effect.IO +import cats.effect.{IO, Resource} import coursierapi.Repository import fs2.io.file.Path +import fs2.io.readInputStream import java.util.zip.ZipFile import scala.jdk.CollectionConverters.* @@ -21,7 +22,7 @@ object SourceFetcher: case None => IO.pure(Left(s"No sources JAR published for '${coord.render}'.")) case Some(jar) => - IO.blocking(extractLines(jar, sourceFilePath, startLine, endLine)) + extractLines(jar, sourceFilePath, startLine, endLine) } private def extractLines( @@ -29,20 +30,26 @@ object SourceFetcher: sourceFilePath: String, startLine: Int, endLine: Int - ): Either[String, SourceResult] = + ): IO[Either[String, SourceResult]] = val normalizedSource = sourceFilePath.replace('\\', '/') - val zip = ZipFile(jar.toNioPath.toFile) - try - val entry = zip.entries().asScala.find { e => - !e.isDirectory && normalizedSource.endsWith(e.getName) - } - entry match + Resource.fromAutoCloseable(IO.blocking(ZipFile(jar.toNioPath.toFile))).use { zip => + IO.blocking { + zip.entries().asScala + .find(e => !e.isDirectory && normalizedSource.endsWith(e.getName)) + .map(e => (e.getName, zip.getInputStream(e))) + }.flatMap { case None => - Left(s"Source file not found in JAR (looked for suffix of '$normalizedSource').") - case Some(e) => - val allLines = scala.io.Source.fromInputStream(zip.getInputStream(e), "UTF-8") - .getLines().toIndexedSeq - val extracted = allLines.slice(startLine, endLine + 1) - Right(SourceResult(e.getName, startLine, endLine, extracted)) - finally - zip.close() + IO.pure(Left(s"Source file not found in JAR (looked for suffix of '$normalizedSource').")) + case Some((name, is)) => + readInputStream(IO.pure(is), chunkSize = 65536) + .through(fs2.text.utf8.decode) + .through(fs2.text.lines) + .compile + .toVector + .map { allLines => + val extracted = if endLine == Int.MaxValue then allLines.drop(startLine) + else allLines.slice(startLine, endLine + 1) + Right(SourceResult(name, startLine, endLine, extracted)) + } + } + } diff --git a/lib/test/src/cellar/IntegrationTest.scala b/lib/test/src/cellar/IntegrationTest.scala index 42de821..84a8ab4 100644 --- a/lib/test/src/cellar/IntegrationTest.scala +++ b/lib/test/src/cellar/IntegrationTest.scala @@ -218,7 +218,7 @@ class IntegrationTest extends CatsEffectSuite: ) } - test("get-source: Java class returns java code block"): + test("get-source: Java class returns java code block with source body"): TestFixtures.assumeFixturesAvailable() val console = CapturingConsole() given Console[IO] = console @@ -230,7 +230,9 @@ class IntegrationTest extends CatsEffectSuite: ) .map { code => assertEquals(code, ExitCode.Success) - assert(console.outBuf.toString.contains("```java"), s"Output: ${console.outBuf}") + val out = console.outBuf.toString + assert(out.contains("```java"), s"Output: $out") + assert(out.contains("getDefault"), s"Expected source body in: $out") } test("get-source: trait with same-file companion returns both"):