Browse Source

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
pull/2920/head
Ivan Matkov 2 years ago committed by GitHub
parent
commit
48288ea145
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorView.kt
  2. 98
      examples/codeviewer/common/src/jvmMain/kotlin/org/jetbrains/codeviewer/platform/JvmFile.kt

1
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, fontSize = settings.fontSize,
fontFamily = Fonts.jetbrainsMono(), fontFamily = Fonts.jetbrainsMono(),
maxLines = 1,
modifier = modifier, modifier = modifier,
softWrap = false softWrap = false
) )

98
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.FilenameFilter
import java.io.IOException import java.io.IOException
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
fun java.io.File.toProjectFile(): File = object : File { 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<File> override val children: List<File>
get() = this@toProjectFile get() = this@toProjectFile
@ -23,31 +26,34 @@ fun java.io.File.toProjectFile(): File = object : File {
.orEmpty() .orEmpty()
.map { it.toProjectFile() } .map { it.toProjectFile() }
private val numberOfFiles
get() = listFiles()?.size ?: 0
override val hasChildren: Boolean override val hasChildren: Boolean
get() = isDirectory && listFiles()?.size ?: 0 > 0 get() = isDirectory && numberOfFiles > 0
override fun readLines(scope: CoroutineScope): TextLines { override fun readLines(scope: CoroutineScope): TextLines {
var byteBufferSize: Int var byteBufferSize: Int
val byteBuffer = RandomAccessFile(this@toProjectFile, "r").use { file -> val byteBuffer = RandomAccessFile(this@toProjectFile, "r").use { file ->
byteBufferSize = file.length().toInt() byteBufferSize = file.length().toInt()
file.channel file.channel.map(FileChannel.MapMode.READ_ONLY, 0, file.length())
.map(FileChannel.MapMode.READ_ONLY, 0, file.length())
} }
val lineStartPositions = IntList() val lineStartPositions = IntList()
var size by mutableStateOf(0) var size by mutableStateOf(0)
// In case of big files, update number of lines periodically
val refreshJob = scope.launch { val refreshJob = scope.launch {
delay(100) delay(100)
size = lineStartPositions.size size = lineStartPositions.size
while (true) { while (isActive) {
delay(1000) delay(1000)
size = lineStartPositions.size size = lineStartPositions.size
} }
} }
// Find indexes where lines starts in background
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
readLinePositions(lineStartPositions) readLinePositions(lineStartPositions)
refreshJob.cancel() refreshJob.cancel()
@ -58,22 +64,37 @@ fun java.io.File.toProjectFile(): File = object : File {
override val size get() = size override val size get() = size
override fun get(index: Int): String { override fun get(index: Int): String {
val startPosition = lineStartPositions[index] val position = lineRange(index)
val length = if (index + 1 < size) lineStartPositions[index + 1] - startPosition else val slice = byteBuffer.slice(position.first, position.last - position.first)
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)
return StandardCharsets.UTF_8.decode(slice).toString() 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( // Backport slice from JDK 13
starts: IntList 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) { require(length() <= Int.MAX_VALUE) {
"Files with size over ${Int.MAX_VALUE} aren't supported" "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) starts.clear(length().toInt() / averageLineLength)
try { try {
FileInputStream(this@readLinePositions).use { for (i in readLinePositions()) {
val channel = it.channel starts.add(i)
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()
} }
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
@ -108,6 +115,31 @@ private fun java.io.File.readLinePositions(
starts.compact() 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<Int> (without unboxing Int and using IntArray under the hood) * Compact version of List<Int> (without unboxing Int and using IntArray under the hood)
*/ */

Loading…
Cancel
Save