From ae4020c9c2a860de956894c9968369e28f2abc46 Mon Sep 17 00:00:00 2001 From: Nikolay Igotti Date: Mon, 2 Nov 2020 17:46:36 +0300 Subject: [PATCH] Use memory mapping and filter hidden files. (#44) --- .../org/jetbrains/codeviewer/platform/File.kt | 2 +- .../jetbrains/codeviewer/ui/editor/Editor.kt | 23 +++--- .../org/jetbrains/codeviewer/util/Loadable.kt | 4 +- .../jetbrains/codeviewer/util/TextLines.kt | 9 ++- .../jetbrains/codeviewer/platform/JvmFile.kt | 80 +++++++++---------- .../kotlin/org/jetbrains/codeviewer/main.kt | 20 +++-- 6 files changed, 77 insertions(+), 61 deletions(-) diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/File.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/File.kt index 970291bfd5..8cb45b2de4 100644 --- a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/File.kt +++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/File.kt @@ -11,5 +11,5 @@ interface File { val children: List val hasChildren: Boolean - suspend fun readLines(backgroundScope: CoroutineScope): TextLines + fun readLines(scope: CoroutineScope): TextLines } \ No newline at end of file diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt index 79897fb207..062cf78483 100644 --- a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt +++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt @@ -4,12 +4,13 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import kotlinx.coroutines.CoroutineScope import org.jetbrains.codeviewer.platform.File +import org.jetbrains.codeviewer.util.EmptyTextLines import org.jetbrains.codeviewer.util.SingleSelection import org.jetbrains.codeviewer.util.afterSet class Editor( val fileName: String, - val lines: suspend (backgroundScope: CoroutineScope) -> Lines, + val lines: (backgroundScope: CoroutineScope) -> Lines, ) { var close: (() -> Unit)? = null lateinit var selection: SingleSelection @@ -26,7 +27,7 @@ class Editor( interface Lines { val lineNumberDigitCount: Int get() = size.toString().length val size: Int - suspend fun get(index: Int): Line + fun get(index: Int): Line } class Content(val value: State, val isCode: Boolean) @@ -35,22 +36,24 @@ class Editor( fun Editor(file: File) = Editor( fileName = file.name ) { backgroundScope -> - val textLines = file.readLines(backgroundScope) - val indexToEditedText = mutableMapOf() + val textLines = try { + file.readLines(backgroundScope) + } catch (e: Throwable) { + e.printStackTrace() + EmptyTextLines + } val isCode = file.name.endsWith(".kt", ignoreCase = true) - suspend fun content(index: Int): Editor.Content { - val text = indexToEditedText[index] ?: textLines.get(index) - val state = mutableStateOf(text).afterSet { - indexToEditedText[index] = it - } + fun content(index: Int): Editor.Content { + val text = textLines.get(index) + val state = mutableStateOf(text) return Editor.Content(state, isCode) } object : Editor.Lines { override val size get() = textLines.size - override suspend fun get(index: Int) = Editor.Line( + override fun get(index: Int) = Editor.Line( number = index + 1, content = content(index) ) diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/Loadable.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/Loadable.kt index a8ed0a8082..0565cbdf03 100644 --- a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/Loadable.kt +++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/Loadable.kt @@ -5,12 +5,12 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @Composable -fun loadable(load: suspend () -> T): MutableState { +fun loadable(load: () -> T): MutableState { return loadableScoped { load() } } @Composable -fun loadableScoped(load: suspend CoroutineScope.() -> T): MutableState { +fun loadableScoped(load: CoroutineScope.() -> T): MutableState { val state: MutableState = remember { mutableStateOf(null) } LaunchedTask { try { diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/TextLines.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/TextLines.kt index dbcfc263b1..ce5361f017 100644 --- a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/TextLines.kt +++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/TextLines.kt @@ -2,5 +2,12 @@ package org.jetbrains.codeviewer.util interface TextLines { val size: Int - suspend fun get(index: Int): String + fun get(index: Int): String +} + +object EmptyTextLines : TextLines { + override val size: Int + get() = 0 + + override fun get(index: Int): String = "" } \ No newline at end of file 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 df4ad90b10..69b9387d6a 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 @@ -6,9 +6,11 @@ import androidx.compose.runtime.setValue import kotlinx.coroutines.* import org.jetbrains.codeviewer.util.TextLines import java.io.FileInputStream +import java.io.FilenameFilter import java.io.IOException import java.io.RandomAccessFile 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 @@ -17,72 +19,67 @@ fun java.io.File.toProjectFile(): File = object : File { override val children: List get() = this@toProjectFile - .listFiles() + .listFiles(FilenameFilter { _, name -> !name.startsWith(".")}) .orEmpty() .map { it.toProjectFile() } override val hasChildren: Boolean get() = isDirectory && listFiles()?.size ?: 0 > 0 - override suspend fun readLines(backgroundScope: CoroutineScope): TextLines { - // linePositions can be very big, so we are using IntList instead of List - val linePositions = IntList() + + 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()) + } + + val lineStartPositions = IntList() + var size by mutableStateOf(0) - val refreshJob = backgroundScope.launch { + val refreshJob = scope.launch { delay(100) - size = linePositions.size + size = lineStartPositions.size while (true) { delay(1000) - size = linePositions.size + size = lineStartPositions.size } } - backgroundScope.launch { - readLinePositions(linePositions) + scope.launch(Dispatchers.IO) { + readLinePositions(lineStartPositions) refreshJob.cancel() - size = linePositions.size + size = lineStartPositions.size } return object : TextLines { override val size get() = size - override suspend fun get(index: Int): String { - return withContext(Dispatchers.IO) { - val position = linePositions[index] - try { - RandomAccessFile(this@toProjectFile, "rws").use { - it.seek(position.toLong()) - // NOTE: it isn't efficient, but simple - String( - it.readLine() - .toCharArray() - .map(Char::toByte) - .toByteArray(), - Charsets.UTF_8 - ) - } - } catch (e: IOException) { - e.printStackTrace() - "" - } - } + 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) + return StandardCharsets.UTF_8.decode(slice).toString() } } } } -@Suppress("BlockingMethodInNonBlockingContext") -private suspend fun java.io.File.readLinePositions(list: IntList) = withContext(Dispatchers.IO) { +private fun java.io.File.readLinePositions( + starts: IntList +) { require(length() <= Int.MAX_VALUE) { "Files with size over ${Int.MAX_VALUE} aren't supported" } val averageLineLength = 200 - list.clear(length().toInt() / averageLineLength) - - var isBeginOfLine = true - var position = 0L + starts.clear(length().toInt() / averageLineLength) try { FileInputStream(this@readLinePositions).use { @@ -90,22 +87,25 @@ private suspend fun java.io.File.readLinePositions(list: IntList) = withContext( 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) { - list.add(position.toInt()) + starts.add(position.toInt()) } isBeginOfLine = byte.toChar() == '\n' position++ } + channel.close() } } catch (e: IOException) { e.printStackTrace() - list.clear(1) - list.add(0) + starts.clear(1) + starts.add(0) } - list.compact() + starts.compact() } /** diff --git a/examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt b/examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt index 6732985ded..63033b9d96 100644 --- a/examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt +++ b/examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt @@ -6,19 +6,25 @@ import androidx.compose.ui.unit.IntSize import org.jetbrains.codeviewer.ui.MainView import java.awt.image.BufferedImage import javax.imageio.ImageIO +import javax.swing.SwingUtilities.invokeLater @OptIn(ExperimentalLayout::class) -fun main() = Window( - title = "Code Viewer", - size = IntSize(1280, 768), - icon = loadImageResource("ic_launcher.png") -) { - MainView() +fun main() { + invokeLater { + Window( + title = "Code Viewer", + size = IntSize(1280, 768), + icon = loadImageResource("ic_launcher.png") + ) { + MainView() + } + } } + @Suppress("SameParameterValue") private fun loadImageResource(path: String): BufferedImage { val resource = Thread.currentThread().contextClassLoader.getResource(path) requireNotNull(resource) { "Resource $path not found" } return resource.openStream().use(ImageIO::read) -} \ No newline at end of file +}