Browse Source

Use memory mapping and filter hidden files. (#44)

pull/45/head
Nikolay Igotti 4 years ago committed by GitHub
parent
commit
ae4020c9c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/File.kt
  2. 23
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt
  3. 4
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/Loadable.kt
  4. 9
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/TextLines.kt
  5. 80
      examples/codeviewer/common/src/jvmMain/kotlin/org/jetbrains/codeviewer/platform/JvmFile.kt
  6. 20
      examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt

2
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/File.kt

@ -11,5 +11,5 @@ interface File {
val children: List<File> val children: List<File>
val hasChildren: Boolean val hasChildren: Boolean
suspend fun readLines(backgroundScope: CoroutineScope): TextLines fun readLines(scope: CoroutineScope): TextLines
} }

23
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 androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.jetbrains.codeviewer.platform.File import org.jetbrains.codeviewer.platform.File
import org.jetbrains.codeviewer.util.EmptyTextLines
import org.jetbrains.codeviewer.util.SingleSelection import org.jetbrains.codeviewer.util.SingleSelection
import org.jetbrains.codeviewer.util.afterSet import org.jetbrains.codeviewer.util.afterSet
class Editor( class Editor(
val fileName: String, val fileName: String,
val lines: suspend (backgroundScope: CoroutineScope) -> Lines, val lines: (backgroundScope: CoroutineScope) -> Lines,
) { ) {
var close: (() -> Unit)? = null var close: (() -> Unit)? = null
lateinit var selection: SingleSelection lateinit var selection: SingleSelection
@ -26,7 +27,7 @@ class Editor(
interface Lines { interface Lines {
val lineNumberDigitCount: Int get() = size.toString().length val lineNumberDigitCount: Int get() = size.toString().length
val size: Int val size: Int
suspend fun get(index: Int): Line fun get(index: Int): Line
} }
class Content(val value: State<String>, val isCode: Boolean) class Content(val value: State<String>, val isCode: Boolean)
@ -35,22 +36,24 @@ class Editor(
fun Editor(file: File) = Editor( fun Editor(file: File) = Editor(
fileName = file.name fileName = file.name
) { backgroundScope -> ) { backgroundScope ->
val textLines = file.readLines(backgroundScope) val textLines = try {
val indexToEditedText = mutableMapOf<Int, String>() file.readLines(backgroundScope)
} catch (e: Throwable) {
e.printStackTrace()
EmptyTextLines
}
val isCode = file.name.endsWith(".kt", ignoreCase = true) val isCode = file.name.endsWith(".kt", ignoreCase = true)
suspend fun content(index: Int): Editor.Content { fun content(index: Int): Editor.Content {
val text = indexToEditedText[index] ?: textLines.get(index) val text = textLines.get(index)
val state = mutableStateOf(text).afterSet { val state = mutableStateOf(text)
indexToEditedText[index] = it
}
return Editor.Content(state, isCode) return Editor.Content(state, isCode)
} }
object : Editor.Lines { object : Editor.Lines {
override val size get() = textLines.size override val size get() = textLines.size
override suspend fun get(index: Int) = Editor.Line( override fun get(index: Int) = Editor.Line(
number = index + 1, number = index + 1,
content = content(index) content = content(index)
) )

4
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/Loadable.kt

@ -5,12 +5,12 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@Composable @Composable
fun <T : Any> loadable(load: suspend () -> T): MutableState<T?> { fun <T : Any> loadable(load: () -> T): MutableState<T?> {
return loadableScoped { load() } return loadableScoped { load() }
} }
@Composable @Composable
fun <T : Any> loadableScoped(load: suspend CoroutineScope.() -> T): MutableState<T?> { fun <T : Any> loadableScoped(load: CoroutineScope.() -> T): MutableState<T?> {
val state: MutableState<T?> = remember { mutableStateOf(null) } val state: MutableState<T?> = remember { mutableStateOf(null) }
LaunchedTask { LaunchedTask {
try { try {

9
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/TextLines.kt

@ -2,5 +2,12 @@ package org.jetbrains.codeviewer.util
interface TextLines { interface TextLines {
val size: Int 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 = ""
} }

80
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 kotlinx.coroutines.*
import org.jetbrains.codeviewer.util.TextLines import org.jetbrains.codeviewer.util.TextLines
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FilenameFilter
import java.io.IOException import java.io.IOException
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
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
@ -17,72 +19,67 @@ fun java.io.File.toProjectFile(): File = object : File {
override val children: List<File> override val children: List<File>
get() = this@toProjectFile get() = this@toProjectFile
.listFiles() .listFiles(FilenameFilter { _, name -> !name.startsWith(".")})
.orEmpty() .orEmpty()
.map { it.toProjectFile() } .map { it.toProjectFile() }
override val hasChildren: Boolean override val hasChildren: Boolean
get() = isDirectory && listFiles()?.size ?: 0 > 0 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<Long> override fun readLines(scope: CoroutineScope): TextLines {
val linePositions = IntList() 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) var size by mutableStateOf(0)
val refreshJob = backgroundScope.launch { val refreshJob = scope.launch {
delay(100) delay(100)
size = linePositions.size size = lineStartPositions.size
while (true) { while (true) {
delay(1000) delay(1000)
size = linePositions.size size = lineStartPositions.size
} }
} }
backgroundScope.launch { scope.launch(Dispatchers.IO) {
readLinePositions(linePositions) readLinePositions(lineStartPositions)
refreshJob.cancel() refreshJob.cancel()
size = linePositions.size size = lineStartPositions.size
} }
return object : TextLines { return object : TextLines {
override val size get() = size override val size get() = size
override suspend fun get(index: Int): String { override fun get(index: Int): String {
return withContext(Dispatchers.IO) { val startPosition = lineStartPositions[index]
val position = linePositions[index] val length = if (index + 1 < size) lineStartPositions[index + 1] - startPosition else
try { byteBufferSize - startPosition
RandomAccessFile(this@toProjectFile, "rws").use { // Only JDK since 13 has slice() method we need, so do ugly for now.
it.seek(position.toLong()) byteBuffer.position(startPosition)
// NOTE: it isn't efficient, but simple val slice = byteBuffer.slice()
String( slice.limit(length)
it.readLine() return StandardCharsets.UTF_8.decode(slice).toString()
.toCharArray()
.map(Char::toByte)
.toByteArray(),
Charsets.UTF_8
)
}
} catch (e: IOException) {
e.printStackTrace()
"<Error on opening the file>"
}
}
} }
} }
} }
} }
@Suppress("BlockingMethodInNonBlockingContext") private fun java.io.File.readLinePositions(
private suspend fun java.io.File.readLinePositions(list: IntList) = withContext(Dispatchers.IO) { 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"
} }
val averageLineLength = 200 val averageLineLength = 200
list.clear(length().toInt() / averageLineLength) starts.clear(length().toInt() / averageLineLength)
var isBeginOfLine = true
var position = 0L
try { try {
FileInputStream(this@readLinePositions).use { FileInputStream(this@readLinePositions).use {
@ -90,22 +87,25 @@ private suspend fun java.io.File.readLinePositions(list: IntList) = withContext(
val ib = channel.map( val ib = channel.map(
FileChannel.MapMode.READ_ONLY, 0, channel.size() FileChannel.MapMode.READ_ONLY, 0, channel.size()
) )
var isBeginOfLine = true
var position = 0L
while (ib.hasRemaining()) { while (ib.hasRemaining()) {
val byte = ib.get() val byte = ib.get()
if (isBeginOfLine) { if (isBeginOfLine) {
list.add(position.toInt()) starts.add(position.toInt())
} }
isBeginOfLine = byte.toChar() == '\n' isBeginOfLine = byte.toChar() == '\n'
position++ position++
} }
channel.close()
} }
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
list.clear(1) starts.clear(1)
list.add(0) starts.add(0)
} }
list.compact() starts.compact()
} }
/** /**

20
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 org.jetbrains.codeviewer.ui.MainView
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import javax.imageio.ImageIO import javax.imageio.ImageIO
import javax.swing.SwingUtilities.invokeLater
@OptIn(ExperimentalLayout::class) @OptIn(ExperimentalLayout::class)
fun main() = Window( fun main() {
title = "Code Viewer", invokeLater {
size = IntSize(1280, 768), Window(
icon = loadImageResource("ic_launcher.png") title = "Code Viewer",
) { size = IntSize(1280, 768),
MainView() icon = loadImageResource("ic_launcher.png")
) {
MainView()
}
}
} }
@Suppress("SameParameterValue") @Suppress("SameParameterValue")
private fun loadImageResource(path: String): BufferedImage { private fun loadImageResource(path: String): BufferedImage {
val resource = Thread.currentThread().contextClassLoader.getResource(path) val resource = Thread.currentThread().contextClassLoader.getResource(path)
requireNotNull(resource) { "Resource $path not found" } requireNotNull(resource) { "Resource $path not found" }
return resource.openStream().use(ImageIO::read) return resource.openStream().use(ImageIO::read)
} }

Loading…
Cancel
Save