From 48288ea145ea536a3e47cecfe1578335a90a49fd Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Fri, 24 Mar 2023 10:14:46 +0100 Subject: [PATCH] Fix selection in codeviewer example (#2898) * Fix selection in codeviewer example * Limit line count on view layer * Fix last line ending * Fix missing last empty line in file * Refactor reading file * Add extra endPosition condition * Polish removing line endings --- .../codeviewer/ui/editor/EditorView.kt | 1 + .../jetbrains/codeviewer/platform/JvmFile.kt | 98 ++++++++++++------- 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorView.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorView.kt index af895553ee..4a05ba5012 100644 --- a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorView.kt +++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorView.kt @@ -138,6 +138,7 @@ private fun LineContent(content: Editor.Content, modifier: Modifier, settings: S }, fontSize = settings.fontSize, fontFamily = Fonts.jetbrainsMono(), + maxLines = 1, modifier = modifier, softWrap = false ) diff --git a/examples/codeviewer/common/src/jvmMain/kotlin/org/jetbrains/codeviewer/platform/JvmFile.kt b/examples/codeviewer/common/src/jvmMain/kotlin/org/jetbrains/codeviewer/platform/JvmFile.kt index 97423323cf..43a731f33f 100644 --- a/examples/codeviewer/common/src/jvmMain/kotlin/org/jetbrains/codeviewer/platform/JvmFile.kt +++ b/examples/codeviewer/common/src/jvmMain/kotlin/org/jetbrains/codeviewer/platform/JvmFile.kt @@ -9,13 +9,16 @@ import java.io.FileInputStream import java.io.FilenameFilter import java.io.IOException import java.io.RandomAccessFile +import java.nio.ByteBuffer import java.nio.channels.FileChannel import java.nio.charset.StandardCharsets fun java.io.File.toProjectFile(): File = object : File { - override val name: String get() = this@toProjectFile.name + override val name: String + get() = this@toProjectFile.name - override val isDirectory: Boolean get() = this@toProjectFile.isDirectory + override val isDirectory: Boolean + get() = this@toProjectFile.isDirectory override val children: List get() = this@toProjectFile @@ -23,31 +26,34 @@ fun java.io.File.toProjectFile(): File = object : File { .orEmpty() .map { it.toProjectFile() } + private val numberOfFiles + get() = listFiles()?.size ?: 0 + override val hasChildren: Boolean - get() = isDirectory && listFiles()?.size ?: 0 > 0 + get() = isDirectory && numberOfFiles > 0 override fun readLines(scope: CoroutineScope): TextLines { var byteBufferSize: Int val byteBuffer = RandomAccessFile(this@toProjectFile, "r").use { file -> byteBufferSize = file.length().toInt() - file.channel - .map(FileChannel.MapMode.READ_ONLY, 0, file.length()) + file.channel.map(FileChannel.MapMode.READ_ONLY, 0, file.length()) } val lineStartPositions = IntList() - var size by mutableStateOf(0) + // In case of big files, update number of lines periodically val refreshJob = scope.launch { delay(100) size = lineStartPositions.size - while (true) { + while (isActive) { delay(1000) size = lineStartPositions.size } } + // Find indexes where lines starts in background scope.launch(Dispatchers.IO) { readLinePositions(lineStartPositions) refreshJob.cancel() @@ -58,22 +64,37 @@ fun java.io.File.toProjectFile(): File = object : File { override val size get() = size override fun get(index: Int): String { - val startPosition = lineStartPositions[index] - val length = if (index + 1 < size) lineStartPositions[index + 1] - startPosition else - byteBufferSize - startPosition - // Only JDK since 13 has slice() method we need, so do ugly for now. - byteBuffer.position(startPosition) - val slice = byteBuffer.slice() - slice.limit(length) + val position = lineRange(index) + val slice = byteBuffer.slice(position.first, position.last - position.first) return StandardCharsets.UTF_8.decode(slice).toString() } + + private fun lineRange(index: Int): IntRange { + val startPosition = lineStartPositions[index] + val nextLineIndex = index + 1 + var endPosition = if (nextLineIndex < size) lineStartPositions[nextLineIndex] else byteBufferSize + + // Remove line endings from the range + while (endPosition > startPosition) { + val lastSymbol = byteBuffer[endPosition - 1] + when (lastSymbol.toInt().toChar()) { + '\n', '\r' -> endPosition-- + else -> break + } + } + return startPosition..endPosition + } } } } -private fun java.io.File.readLinePositions( - starts: IntList -) { +// Backport slice from JDK 13 +private fun ByteBuffer.slice(index: Int, length: Int): ByteBuffer { + position(index) + return slice().limit(length) +} + +private fun java.io.File.readLinePositions(starts: IntList) { require(length() <= Int.MAX_VALUE) { "Files with size over ${Int.MAX_VALUE} aren't supported" } @@ -82,22 +103,8 @@ private fun java.io.File.readLinePositions( starts.clear(length().toInt() / averageLineLength) try { - FileInputStream(this@readLinePositions).use { - val channel = it.channel - val ib = channel.map( - FileChannel.MapMode.READ_ONLY, 0, channel.size() - ) - var isBeginOfLine = true - var position = 0L - while (ib.hasRemaining()) { - val byte = ib.get() - if (isBeginOfLine) { - starts.add(position.toInt()) - } - isBeginOfLine = byte.toInt().toChar() == '\n' - position++ - } - channel.close() + for (i in readLinePositions()) { + starts.add(i) } } catch (e: IOException) { e.printStackTrace() @@ -108,6 +115,31 @@ private fun java.io.File.readLinePositions( starts.compact() } +private fun java.io.File.readLinePositions() = sequence { + require(length() <= Int.MAX_VALUE) { + "Files with size over ${Int.MAX_VALUE} aren't supported" + } + readBuffer { + yield(position()) + while (hasRemaining()) { + val byte = get() + if (byte.isChar('\n')) { + yield(position()) + } + } + } +} + +private inline fun java.io.File.readBuffer(block: ByteBuffer.() -> Unit) { + FileInputStream(this).use { stream -> + stream.channel.use { channel -> + channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()).block() + } + } +} + +private fun Byte.isChar(char: Char) = toInt().toChar() == char + /** * Compact version of List (without unboxing Int and using IntArray under the hood) */