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 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 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<String>, 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<Int, String>()
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)
)

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
@Composable
fun <T : Any> loadable(load: suspend () -> T): MutableState<T?> {
fun <T : Any> loadable(load: () -> T): MutableState<T?> {
return loadableScoped { load() }
}
@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) }
LaunchedTask {
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 {
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 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<File>
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<Long>
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()
"<Error on opening the file>"
}
}
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()
}
/**

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 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)
}
}

Loading…
Cancel
Save