Browse Source

Merge pull request #36 from JetBrains/codeviewer

Code Viewer Example
pull/34/head
Igor Demin 4 years ago committed by GitHub
parent
commit
c7d1446f9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      examples/codeviewer/.gitignore
  2. 21
      examples/codeviewer/.run/desktop.run.xml
  3. 7
      examples/codeviewer/README.md
  4. 25
      examples/codeviewer/android/build.gradle.kts
  5. 23
      examples/codeviewer/android/src/main/AndroidManifest.xml
  6. 191
      examples/codeviewer/android/src/main/assets/data/EditorView.kt
  7. 32
      examples/codeviewer/android/src/main/java/org/jetbrains/codeviewer/MainActivity.kt
  8. 15
      examples/codeviewer/android/src/main/res/drawable/ic_launcher_foreground.xml
  9. 5
      examples/codeviewer/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  10. 5
      examples/codeviewer/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  11. 4
      examples/codeviewer/android/src/main/res/values/ic_launcher_background.xml
  12. 3
      examples/codeviewer/android/src/main/res/values/strings.xml
  13. 23
      examples/codeviewer/build.gradle.kts
  14. 66
      examples/codeviewer/common/build.gradle.kts
  15. 2
      examples/codeviewer/common/src/androidMain/AndroidManifest.xml
  16. 4
      examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/File.kt
  17. 12
      examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt
  18. 31
      examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt
  19. 21
      examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt
  20. 26
      examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt
  21. 6
      examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt
  22. 15
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/File.kt
  23. 12
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt
  24. 15
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt
  25. 21
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt
  26. 9
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt
  27. 6
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt
  28. 11
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/CodeViewer.kt
  29. 105
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/CodeViewerView.kt
  30. 38
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/MainView.kt
  31. 64
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Fonts.kt
  32. 11
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Settings.kt
  33. 32
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Theme.kt
  34. 58
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt
  35. 34
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorEmptyView.kt
  36. 72
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorTabsView.kt
  37. 191
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorView.kt
  38. 32
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editors.kt
  39. 72
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/filetree/FileTree.kt
  40. 128
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/filetree/FileTreeView.kt
  41. 48
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/statusbar/StatusBar.kt
  42. 11
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/LayoutModifiers.kt
  43. 33
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/LazyColumnFor.kt
  44. 25
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/Loadable.kt
  45. 9
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/SingleSelection.kt
  46. 14
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/State.kt
  47. 6
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/TextLines.kt
  48. 91
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/VerticalSplittable.kt
  49. BIN
      examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_bold.ttf
  50. BIN
      examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_bold_italic.ttf
  51. BIN
      examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_extrabold.ttf
  52. BIN
      examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_extrabold_italic.ttf
  53. BIN
      examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_italic.ttf
  54. BIN
      examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_medium.ttf
  55. BIN
      examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_medium_italic.ttf
  56. BIN
      examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_regular.ttf
  57. 5
      examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/File.kt
  58. 33
      examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt
  59. 25
      examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt
  60. 48
      examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt
  61. 26
      examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt
  62. 7
      examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt
  63. 145
      examples/codeviewer/common/src/jvmMain/kotlin/org/jetbrains/codeviewer/platform/JvmFile.kt
  64. 27
      examples/codeviewer/desktop/build.gradle.kts
  65. 24
      examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt
  66. BIN
      examples/codeviewer/desktop/src/jvmMain/resources/ic_launcher.png
  67. 21
      examples/codeviewer/gradle.properties
  68. BIN
      examples/codeviewer/gradle/wrapper/gradle-wrapper.jar
  69. 5
      examples/codeviewer/gradle/wrapper/gradle-wrapper.properties
  70. 183
      examples/codeviewer/gradlew
  71. 100
      examples/codeviewer/gradlew.bat
  72. 1
      examples/codeviewer/settings.gradle.kts

15
examples/codeviewer/.gitignore vendored

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build/
/captures
.externalNativeBuild
.cxx

21
examples/codeviewer/.run/desktop.run.xml

@ -0,0 +1,21 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="desktop" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/desktop" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="run" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
</component>

7
examples/codeviewer/README.md

@ -0,0 +1,7 @@
MPP Code Viewer example for desktop/android written in Multiplatform Compose library.
To run desktop application execute in a terminal:
`./gradlew desktop:run`
To install android application on device/emulator:
'./gradlew installDebug'

25
examples/codeviewer/android/build.gradle.kts

@ -0,0 +1,25 @@
plugins {
id("com.android.application")
kotlin("android")
id("org.jetbrains.compose")
}
android {
compileSdkVersion(30)
defaultConfig {
minSdkVersion(26)
targetSdkVersion(30)
versionCode = 1
versionName = "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
dependencies {
implementation(project(":common"))
}

23
examples/codeviewer/android/src/main/AndroidManifest.xml

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jetbrains.codeviewer">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:name="MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

191
examples/codeviewer/android/src/main/assets/data/EditorView.kt

@ -0,0 +1,191 @@
/**
* This file is an example (we can open it in android application)
*/
package org.jetbrains.codeviewer.ui.editor
import androidx.compose.foundation.AmbientContentColor
import androidx.compose.foundation.Text
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawOpacity
import androidx.compose.ui.platform.DensityAmbient
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.annotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import org.jetbrains.codeviewer.platform.SelectionContainer
import org.jetbrains.codeviewer.platform.VerticalScrollbar
import org.jetbrains.codeviewer.platform.WithoutSelection
import org.jetbrains.codeviewer.ui.common.AppTheme
import org.jetbrains.codeviewer.ui.common.Fonts
import org.jetbrains.codeviewer.ui.common.Settings
import org.jetbrains.codeviewer.util.LazyColumnFor
import org.jetbrains.codeviewer.util.loadable
import org.jetbrains.codeviewer.util.loadableScoped
import org.jetbrains.codeviewer.util.withoutWidthConstraints
import kotlin.text.Regex.Companion.fromLiteral
@Composable
fun EditorView(model: Editor, settings: Settings) = key(model) {
with (DensityAmbient.current) {
SelectionContainer {
Surface(
Modifier.fillMaxSize(),
color = AppTheme.colors.backgroundDark,
) {
val lines by loadableScoped(model.lines)
if (lines != null) {
Box {
Lines(lines!!, settings)
Box(
Modifier
.offset(
x = settings.fontSize.toDp() * 0.5f * settings.maxLineSymbols
)
.width(1.dp)
.fillMaxHeight()
.background(AppTheme.colors.backgroundLight)
)
}
} else {
CircularProgressIndicator(
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
}
}
}
@Composable
private fun Lines(lines: Editor.Lines, settings: Settings) = with(DensityAmbient.current) {
val maxNumber = remember(lines.lineNumberDigitCount) {
(1..lines.lineNumberDigitCount).joinToString(separator = "") { "9" }
}
Box(Modifier.fillMaxSize()) {
val scrollState = rememberLazyListState()
val lineHeight = settings.fontSize.toDp() * 1.6f
LazyColumnFor(
lines.size,
modifier = Modifier.fillMaxSize(),
state = scrollState,
itemContent = { index ->
val line: Editor.Line? by loadable { lines.get(index) }
Box(Modifier.height(lineHeight)) {
if (line != null) {
Line(Modifier.align(Alignment.CenterStart), maxNumber, line!!, settings)
}
}
}
)
VerticalScrollbar(
Modifier.align(Alignment.CenterEnd),
scrollState,
lines.size,
lineHeight
)
}
}
// Поддержка русского языка
// دعم اللغة العربية
// 中文支持
@Composable
private fun Line(modifier: Modifier, maxNumber: String, line: Editor.Line, settings: Settings) {
Row(modifier = modifier) {
WithoutSelection {
Box {
LineNumber(maxNumber, Modifier.drawOpacity(0f), settings)
LineNumber(line.number.toString(), Modifier.align(Alignment.CenterEnd), settings)
}
}
LineContent(
line.content,
modifier = Modifier
.weight(1f)
.withoutWidthConstraints()
.padding(start = 28.dp, end = 12.dp),
settings = settings
)
}
}
@Composable
private fun LineNumber(number: String, modifier: Modifier, settings: Settings) = Text(
text = number,
fontSize = settings.fontSize,
fontFamily = Fonts.jetbrainsMono(),
color = AmbientContentColor.current.copy(alpha = 0.30f),
modifier = modifier.padding(start = 12.dp)
)
@Composable
private fun LineContent(content: Editor.Content, modifier: Modifier, settings: Settings) = Text(
text = if (content.isCode) {
codeString(content.value.value)
} else {
AnnotatedString(content.value.value)
},
fontSize = settings.fontSize,
fontFamily = Fonts.jetbrainsMono(),
modifier = modifier,
softWrap = false
)
private fun codeString(str: String) = annotatedString {
withStyle(AppTheme.code.simple) {
append(str.replace("\t", " "))
addStyle(AppTheme.code.punctuation, ":")
addStyle(AppTheme.code.punctuation, "=")
addStyle(AppTheme.code.punctuation, "\"")
addStyle(AppTheme.code.punctuation, "[")
addStyle(AppTheme.code.punctuation, "]")
addStyle(AppTheme.code.punctuation, "{")
addStyle(AppTheme.code.punctuation, "}")
addStyle(AppTheme.code.punctuation, "(")
addStyle(AppTheme.code.punctuation, ")")
addStyle(AppTheme.code.punctuation, ",")
addStyle(AppTheme.code.keyword, "fun ")
addStyle(AppTheme.code.keyword, "val ")
addStyle(AppTheme.code.keyword, "var ")
addStyle(AppTheme.code.keyword, "private ")
addStyle(AppTheme.code.keyword, "internal ")
addStyle(AppTheme.code.keyword, "for ")
addStyle(AppTheme.code.keyword, "expect ")
addStyle(AppTheme.code.keyword, "actual ")
addStyle(AppTheme.code.keyword, "import ")
addStyle(AppTheme.code.keyword, "package ")
addStyle(AppTheme.code.value, "true")
addStyle(AppTheme.code.value, "false")
addStyle(AppTheme.code.value, Regex("[0-9]*"))
addStyle(AppTheme.code.annotation, Regex("^@[a-zA-Z_]*"))
addStyle(AppTheme.code.comment, Regex("^\\s*//.*"))
}
}
private fun AnnotatedString.Builder.addStyle(style: SpanStyle, regexp: String) {
addStyle(style, fromLiteral(regexp))
}
private fun AnnotatedString.Builder.addStyle(style: SpanStyle, regexp: Regex) {
for (result in regexp.findAll(toString())) {
addStyle(style, result.range.first, result.range.last + 1)
}
}

32
examples/codeviewer/android/src/main/java/org/jetbrains/codeviewer/MainActivity.kt

@ -0,0 +1,32 @@
package org.jetbrains.codeviewer
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.platform.setContent
import org.jetbrains.codeviewer.platform._HomeFolder
import org.jetbrains.codeviewer.ui.MainView
import java.io.File
import java.io.FileOutputStream
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
copyAssets()
_HomeFolder = filesDir
setContent {
MainView()
}
}
private fun copyAssets() {
for (filename in assets.list("data")!!) {
assets.open("data/$filename").use { assetStream ->
val file = File(filesDir, filename)
FileOutputStream(file).use { fileStream ->
assetStream.copyTo(fileStream)
}
}
}
}
}

15
examples/codeviewer/android/src/main/res/drawable/ic_launcher_foreground.xml

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
android:tint="#FFFFFF">
<group android:scaleX="2.61"
android:scaleY="2.61"
android:translateX="22.68"
android:translateY="22.68">
<path
android:fillColor="@android:color/white"
android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
</group>
</vector>

5
examples/codeviewer/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

5
examples/codeviewer/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

4
examples/codeviewer/android/src/main/res/values/ic_launcher_background.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#3C3F41</color>
</resources>

3
examples/codeviewer/android/src/main/res/values/strings.xml

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Code Viewer</string>
</resources>

23
examples/codeviewer/build.gradle.kts

@ -0,0 +1,23 @@
buildscript {
repositories {
google()
jcenter()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
dependencies {
// TODO/migrateToMaster 0.1.0-dev104 is built from "unmerged" branch,
// replace it by version from androidx-master-dev when scrollbars will be merged
classpath("org.jetbrains.compose:compose-gradle-plugin:0.1.0-dev104")
classpath("com.android.tools.build:gradle:4.0.1")
classpath(kotlin("gradle-plugin", version = "1.4.0"))
}
}
allprojects {
repositories {
google()
jcenter()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
}

66
examples/codeviewer/common/build.gradle.kts

@ -0,0 +1,66 @@
import org.jetbrains.compose.compose
plugins {
id("com.android.library")
kotlin("multiplatform")
id("org.jetbrains.compose")
}
kotlin {
android()
jvm("desktop")
sourceSets {
named("commonMain") {
dependencies {
api(compose.runtime)
api(compose.foundation)
api(compose.material)
api(compose.materialIconsExtended)
}
}
named("androidMain") {
kotlin.srcDirs("src/jvmMain/kotlin")
dependencies {
api("androidx.appcompat:appcompat:1.1.0")
api("androidx.core:core-ktx:1.3.1")
}
}
named("desktopMain") {
kotlin.srcDirs("src/jvmMain/kotlin")
resources.srcDirs("src/commonMain/resources")
dependencies {
api(compose.desktop.common)
}
}
}
}
project.extensions.findByType<com.android.build.gradle.LibraryExtension>()!!.apply {
sourceSets.findByName("main")?.apply {
res.srcDirs("src/commonMain/resources")
}
}
android {
compileSdkVersion(30)
defaultConfig {
minSdkVersion(21)
targetSdkVersion(30)
versionCode = 1
versionName = "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
sourceSets {
named("main") {
manifest.srcFile("src/androidMain/AndroidManifest.xml")
res.srcDirs("src/androidMain/res")
}
}
}

2
examples/codeviewer/common/src/androidMain/AndroidManifest.xml

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="org.jetbrains.codeviewer.common"/>

4
examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/File.kt

@ -0,0 +1,4 @@
package org.jetbrains.codeviewer.platform
lateinit var _HomeFolder: java.io.File
actual val HomeFolder: File get() = _HomeFolder.toProjectFile()

12
examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt

@ -0,0 +1,12 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
actual fun Modifier.pointerMoveFilter(
onEnter: () -> Boolean,
onExit: () -> Boolean,
onMove: (Offset) -> Boolean
): Modifier = this
actual fun Modifier.cursorForHorizontalResize() = this

31
examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt

@ -0,0 +1,31 @@
package org.jetbrains.codeviewer.platform
import android.graphics.BitmapFactory
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageAsset
import androidx.compose.ui.graphics.asImageAsset
import androidx.compose.ui.platform.ContextAmbient
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import java.io.InputStream
import java.net.URL
@Composable
actual fun imageResource(res: String): ImageAsset {
val context = ContextAmbient.current
val id = context.resources.getIdentifier(res, "drawable", context.packageName)
return androidx.compose.ui.res.imageResource(id)
}
actual suspend fun imageFromUrl(url: String): ImageAsset {
val bytes = URL(url).openStream().buffered().use(InputStream::readBytes)
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size).asImageAsset()
}
@Composable
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font {
val context = ContextAmbient.current
val id = context.resources.getIdentifier(res, "font", context.packageName)
return androidx.compose.ui.text.font.font(id, weight, style)
}

21
examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt

@ -0,0 +1,21 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
@Composable
actual fun VerticalScrollbar(
modifier: Modifier,
scrollState: ScrollState
) = Unit
@Composable
actual fun VerticalScrollbar(
modifier: Modifier,
scrollState: LazyListState,
itemCount: Int,
averageItemSize: Dp
) = Unit

26
examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt

@ -0,0 +1,26 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.selection.Selection
import androidx.compose.ui.selection.SelectionContainer
@Composable
actual fun SelectionContainer(children: @Composable () -> Unit) {
val selection = remember { mutableStateOf<Selection?>(null) }
SelectionContainer(
selection = selection.value,
onSelectionChange = { selection.value = it },
children = children
)
}
@Composable
actual fun WithoutSelection(children: @Composable () -> Unit) {
SelectionContainer(
selection = null,
onSelectionChange = {},
children = children
)
}

6
examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt

@ -0,0 +1,6 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.runtime.Composable
@Composable
actual fun PlatformTheme(content: @Composable () -> Unit) = content()

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

@ -0,0 +1,15 @@
package org.jetbrains.codeviewer.platform
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.codeviewer.util.TextLines
expect val HomeFolder: File
interface File {
val name: String
val isDirectory: Boolean
val children: List<File>
val hasChildren: Boolean
suspend fun readLines(backgroundScope: CoroutineScope): TextLines
}

12
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt

@ -0,0 +1,12 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
expect fun Modifier.pointerMoveFilter(
onEnter: () -> Boolean = { true },
onExit: () -> Boolean = { true },
onMove: (Offset) -> Boolean = { true }
): Modifier
expect fun Modifier.cursorForHorizontalResize(): Modifier

15
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt

@ -0,0 +1,15 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageAsset
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
@Composable
expect fun imageResource(res: String): ImageAsset
expect suspend fun imageFromUrl(url: String): ImageAsset
@Composable
expect fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font

21
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt

@ -0,0 +1,21 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
@Composable
expect fun VerticalScrollbar(
modifier: Modifier,
scrollState: ScrollState
)
@Composable
expect fun VerticalScrollbar(
modifier: Modifier,
scrollState: LazyListState,
itemCount: Int,
averageItemSize: Dp
)

9
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt

@ -0,0 +1,9 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.runtime.Composable
@Composable
expect fun SelectionContainer(children: @Composable () -> Unit)
@Composable
expect fun WithoutSelection(children: @Composable () -> Unit)

6
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt

@ -0,0 +1,6 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.runtime.Composable
@Composable
expect fun PlatformTheme(content: @Composable () -> Unit)

11
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/CodeViewer.kt

@ -0,0 +1,11 @@
package org.jetbrains.codeviewer.ui
import org.jetbrains.codeviewer.ui.common.Settings
import org.jetbrains.codeviewer.ui.editor.Editors
import org.jetbrains.codeviewer.ui.filetree.FileTree
class CodeViewer(
val editors: Editors,
val fileTree: FileTree,
val settings: Settings
)

105
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/CodeViewerView.kt

@ -0,0 +1,105 @@
package org.jetbrains.codeviewer.ui
import androidx.compose.animation.animate
import androidx.compose.animation.core.Spring.StiffnessLow
import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.AmbientContentColor
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.drawLayer
import androidx.compose.ui.unit.dp
import org.jetbrains.codeviewer.ui.editor.EditorEmptyView
import org.jetbrains.codeviewer.ui.editor.EditorTabsView
import org.jetbrains.codeviewer.ui.editor.EditorView
import org.jetbrains.codeviewer.ui.filetree.FileTreeView
import org.jetbrains.codeviewer.ui.filetree.FileTreeViewTabView
import org.jetbrains.codeviewer.ui.statusbar.StatusBar
import org.jetbrains.codeviewer.util.SplitterState
import org.jetbrains.codeviewer.util.VerticalSplittable
@Composable
fun CodeViewerView(model: CodeViewer) {
val panelState = remember { PanelState() }
val animatedSize = if (panelState.splitter.isResizing) {
if (panelState.isExpanded) panelState.expandedSize else panelState.collapsedSize
} else {
animate(
if (panelState.isExpanded) panelState.expandedSize else panelState.collapsedSize,
SpringSpec(stiffness = StiffnessLow)
)
}
VerticalSplittable(
Modifier.fillMaxSize(),
panelState.splitter,
onResize = {
panelState.expandedSize =
(panelState.expandedSize + it).coerceAtLeast(panelState.expandedSizeMin)
}
) {
ResizablePanel(Modifier.width(animatedSize).fillMaxHeight(), panelState) {
Column {
FileTreeViewTabView()
FileTreeView(model.fileTree)
}
}
Box {
if (model.editors.active != null) {
Column(Modifier.fillMaxSize()) {
EditorTabsView(model.editors)
Box(Modifier.weight(1f)) {
EditorView(model.editors.active!!, model.settings)
}
StatusBar(model.settings)
}
} else {
EditorEmptyView()
}
}
}
}
private class PanelState {
val collapsedSize = 24.dp
var expandedSize by mutableStateOf(300.dp)
val expandedSizeMin = 90.dp
var isExpanded by mutableStateOf(true)
val splitter = SplitterState()
}
@Composable
private fun ResizablePanel(
modifier: Modifier,
state: PanelState,
content: @Composable () -> Unit,
) {
val alpha = animate(if (state.isExpanded) 1f else 0f, SpringSpec(stiffness = StiffnessLow))
Box(modifier) {
Box(Modifier.fillMaxSize().drawLayer(alpha = alpha)) {
content()
}
Icon(
if (state.isExpanded) Icons.Default.ArrowBack else Icons.Default.ArrowForward,
tint = AmbientContentColor.current,
modifier = Modifier
.padding(top = 4.dp)
.width(24.dp)
.clickable {
state.isExpanded = !state.isExpanded
}
.padding(4.dp)
.align(Alignment.TopEnd)
)
}
}

38
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/MainView.kt

@ -0,0 +1,38 @@
package org.jetbrains.codeviewer.ui
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import org.jetbrains.codeviewer.platform.HomeFolder
import org.jetbrains.codeviewer.platform.PlatformTheme
import org.jetbrains.codeviewer.platform.WithoutSelection
import org.jetbrains.codeviewer.ui.common.AppTheme
import org.jetbrains.codeviewer.ui.common.Settings
import org.jetbrains.codeviewer.ui.editor.Editors
import org.jetbrains.codeviewer.ui.filetree.FileTree
@Composable
fun MainView() {
val codeViewer = remember {
val editors = Editors()
CodeViewer(
editors = editors,
fileTree = FileTree(HomeFolder, editors),
settings = Settings()
)
}
WithoutSelection {
MaterialTheme(
colors = AppTheme.colors.material
) {
PlatformTheme {
Surface {
CodeViewerView(codeViewer)
}
}
}
}
}

64
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Fonts.kt

@ -0,0 +1,64 @@
package org.jetbrains.codeviewer.ui.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.fontFamily
import org.jetbrains.codeviewer.platform.font
object Fonts {
@Composable
fun jetbrainsMono() = fontFamily(
font(
"JetBrains Mono",
"jetbrainsmono_regular",
FontWeight.Normal,
FontStyle.Normal
),
font(
"JetBrains Mono",
"jetbrainsmono_italic",
FontWeight.Normal,
FontStyle.Italic
),
font(
"JetBrains Mono",
"jetbrainsmono_bold",
FontWeight.Bold,
FontStyle.Normal
),
font(
"JetBrains Mono",
"jetbrainsmono_bold_italic",
FontWeight.Bold,
FontStyle.Italic
),
font(
"JetBrains Mono",
"jetbrainsmono_extrabold",
FontWeight.ExtraBold,
FontStyle.Normal
),
font(
"JetBrains Mono",
"jetbrainsmono_extrabold_italic",
FontWeight.ExtraBold,
FontStyle.Italic
),
font(
"JetBrains Mono",
"jetbrainsmono_medium",
FontWeight.Medium,
FontStyle.Normal
),
font(
"JetBrains Mono",
"jetbrainsmono_medium_italic",
FontWeight.Medium,
FontStyle.Italic
)
)
}

11
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Settings.kt

@ -0,0 +1,11 @@
package org.jetbrains.codeviewer.ui.common
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.sp
class Settings {
var fontSize by mutableStateOf(13.sp)
val maxLineSymbols = 120
}

32
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Theme.kt

@ -0,0 +1,32 @@
package org.jetbrains.codeviewer.ui.common
import androidx.compose.material.darkColors
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
object AppTheme {
val colors: Colors = Colors()
val code: Code = Code()
class Colors(
val backgroundDark: Color = Color(0xFF2B2B2B),
val backgroundMedium: Color = Color(0xFF3C3F41),
val backgroundLight: Color = Color(0xFF4E5254),
val material: androidx.compose.material.Colors = darkColors(
background = backgroundDark,
surface = backgroundMedium,
primary = Color.White
),
)
class Code(
val simple: SpanStyle = SpanStyle(Color(0xFFA9B7C6)),
val value: SpanStyle = SpanStyle(Color(0xFF6897BB)),
val keyword: SpanStyle = SpanStyle(Color(0xFFCC7832)),
val punctuation: SpanStyle = SpanStyle(Color(0xFFA1C17E)),
val annotation: SpanStyle = SpanStyle(Color(0xFFBBB529)),
val comment: SpanStyle = SpanStyle(Color(0xFF808080))
)
}

58
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt

@ -0,0 +1,58 @@
package org.jetbrains.codeviewer.ui.editor
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.SingleSelection
import org.jetbrains.codeviewer.util.afterSet
class Editor(
val fileName: String,
val lines: suspend (backgroundScope: CoroutineScope) -> Lines,
) {
var close: (() -> Unit)? = null
lateinit var selection: SingleSelection
val isActive: Boolean
get() = selection.selected === this
fun activate() {
selection.selected = this
}
class Line(val number: Int, val content: Content)
interface Lines {
val lineNumberDigitCount: Int get() = size.toString().length
val size: Int
suspend fun get(index: Int): Line
}
class Content(val value: State<String>, val isCode: Boolean)
}
fun Editor(file: File) = Editor(
fileName = file.name
) { backgroundScope ->
val textLines = file.readLines(backgroundScope)
val indexToEditedText = mutableMapOf<Int, String>()
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
}
return Editor.Content(state, isCode)
}
object : Editor.Lines {
override val size get() = textLines.size
override suspend fun get(index: Int) = Editor.Line(
number = index + 1,
content = content(index)
)
}
}

34
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorEmptyView.kt

@ -0,0 +1,34 @@
package org.jetbrains.codeviewer.ui.editor
import androidx.compose.foundation.AmbientContentColor
import androidx.compose.foundation.Text
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Code
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun EditorEmptyView() = Box(Modifier.fillMaxSize()) {
Column(Modifier.align(Alignment.Center)) {
Icon(
Icons.Default.Code.copy(defaultWidth = 48.dp, defaultHeight = 48.dp),
tint = AmbientContentColor.current.copy(alpha = 0.60f),
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Text(
"To view file open it from the file tree",
color = AmbientContentColor.current.copy(alpha = 0.60f),
fontSize = 20.sp,
modifier = Modifier.align(Alignment.CenterHorizontally).padding(16.dp)
)
}
}

72
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorTabsView.kt

@ -0,0 +1,72 @@
package org.jetbrains.codeviewer.ui.editor
import androidx.compose.animation.animate
import androidx.compose.foundation.AmbientContentColor
import androidx.compose.foundation.ScrollableRow
import androidx.compose.foundation.Text
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.jetbrains.codeviewer.ui.common.AppTheme
@Composable
fun EditorTabsView(model: Editors) = ScrollableRow {
for (editor in model.editors) {
EditorTabView(editor)
}
}
@Composable
fun EditorTabView(model: Editor) = Surface(
color = animate(if (model.isActive) {
AppTheme.colors.backgroundDark
} else {
Color.Transparent
})
) {
Row(
Modifier
.clickable {
model.activate()
}
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
model.fileName,
color = AmbientContentColor.current,
fontSize = 12.sp,
modifier = Modifier.padding(horizontal = 4.dp)
)
val close = model.close
if (close != null) {
Icon(
Icons.Default.Close, tint = AmbientContentColor.current, modifier = Modifier
.size(24.dp)
.padding(4.dp)
.clickable {
close()
})
} else {
Box(
modifier = Modifier
.size(24.dp, 24.dp)
.padding(4.dp)
)
}
}
}

191
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorView.kt

@ -0,0 +1,191 @@
package org.jetbrains.codeviewer.ui.editor
import androidx.compose.foundation.AmbientContentColor
import androidx.compose.foundation.Text
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawOpacity
import androidx.compose.ui.platform.DensityAmbient
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.annotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import org.jetbrains.codeviewer.platform.SelectionContainer
import org.jetbrains.codeviewer.platform.VerticalScrollbar
import org.jetbrains.codeviewer.platform.WithoutSelection
import org.jetbrains.codeviewer.ui.common.AppTheme
import org.jetbrains.codeviewer.ui.common.Fonts
import org.jetbrains.codeviewer.ui.common.Settings
import org.jetbrains.codeviewer.util.LazyColumnFor
import org.jetbrains.codeviewer.util.loadable
import org.jetbrains.codeviewer.util.loadableScoped
import org.jetbrains.codeviewer.util.withoutWidthConstraints
import kotlin.text.Regex.Companion.fromLiteral
@Composable
fun EditorView(model: Editor, settings: Settings) = key(model) {
with (DensityAmbient.current) {
SelectionContainer {
Surface(
Modifier.fillMaxSize(),
color = AppTheme.colors.backgroundDark,
) {
val lines by loadableScoped(model.lines)
if (lines != null) {
Box {
Lines(lines!!, settings)
Box(
Modifier
.offset(
x = settings.fontSize.toDp() * 0.5f * settings.maxLineSymbols
)
.width(1.dp)
.fillMaxHeight()
.background(AppTheme.colors.backgroundLight)
)
}
} else {
CircularProgressIndicator(
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
}
}
}
@Composable
private fun Lines(lines: Editor.Lines, settings: Settings) = with(DensityAmbient.current) {
val maxNum = remember(lines.lineNumberDigitCount) {
(1..lines.lineNumberDigitCount).joinToString(separator = "") { "9" }
}
Box(Modifier.fillMaxSize()) {
val scrollState = rememberLazyListState()
val lineHeight = settings.fontSize.toDp() * 1.6f
LazyColumnFor(
lines.size,
modifier = Modifier.fillMaxSize(),
state = scrollState,
itemContent = { index ->
val line: Editor.Line? by loadable { lines.get(index) }
Box(Modifier.height(lineHeight)) {
if (line != null) {
Line(Modifier.align(Alignment.CenterStart), maxNum, line!!, settings)
}
}
}
)
VerticalScrollbar(
Modifier.align(Alignment.CenterEnd),
scrollState,
lines.size,
lineHeight
)
}
}
// Поддержка русского языка
// دعم اللغة العربية
// 中文支持
@Composable
private fun Line(modifier: Modifier, maxNum: String, line: Editor.Line, settings: Settings) {
Row(modifier = modifier) {
WithoutSelection {
Box {
LineNumber(maxNum, Modifier.drawOpacity(0f), settings)
LineNumber(line.number.toString(), Modifier.align(Alignment.CenterEnd), settings)
}
}
LineContent(
line.content,
modifier = Modifier
.weight(1f)
.withoutWidthConstraints()
.padding(start = 28.dp, end = 12.dp),
settings = settings
)
}
}
@Composable
private fun LineNumber(number: String, modifier: Modifier, settings: Settings) = Text(
text = number,
fontSize = settings.fontSize,
fontFamily = Fonts.jetbrainsMono(),
color = AmbientContentColor.current.copy(alpha = 0.30f),
modifier = modifier.padding(start = 12.dp)
)
@Composable
private fun LineContent(content: Editor.Content, modifier: Modifier, settings: Settings) = Text(
text = if (content.isCode) {
codeString(content.value.value)
} else {
annotatedString {
withStyle(AppTheme.code.simple) {
append(content.value.value)
}
}
},
fontSize = settings.fontSize,
fontFamily = Fonts.jetbrainsMono(),
modifier = modifier,
softWrap = false
)
private fun codeString(str: String) = annotatedString {
withStyle(AppTheme.code.simple) {
append(str.replace("\t", " "))
addStyle(AppTheme.code.punctuation, ":")
addStyle(AppTheme.code.punctuation, "=")
addStyle(AppTheme.code.punctuation, "\"")
addStyle(AppTheme.code.punctuation, "[")
addStyle(AppTheme.code.punctuation, "]")
addStyle(AppTheme.code.punctuation, "{")
addStyle(AppTheme.code.punctuation, "}")
addStyle(AppTheme.code.punctuation, "(")
addStyle(AppTheme.code.punctuation, ")")
addStyle(AppTheme.code.punctuation, ",")
addStyle(AppTheme.code.keyword, "fun ")
addStyle(AppTheme.code.keyword, "val ")
addStyle(AppTheme.code.keyword, "var ")
addStyle(AppTheme.code.keyword, "private ")
addStyle(AppTheme.code.keyword, "internal ")
addStyle(AppTheme.code.keyword, "for ")
addStyle(AppTheme.code.keyword, "expect ")
addStyle(AppTheme.code.keyword, "actual ")
addStyle(AppTheme.code.keyword, "import ")
addStyle(AppTheme.code.keyword, "package ")
addStyle(AppTheme.code.value, "true")
addStyle(AppTheme.code.value, "false")
addStyle(AppTheme.code.value, Regex("[0-9]*"))
addStyle(AppTheme.code.annotation, Regex("^@[a-zA-Z_]*"))
addStyle(AppTheme.code.comment, Regex("^\\s*//.*"))
}
}
private fun AnnotatedString.Builder.addStyle(style: SpanStyle, regexp: String) {
addStyle(style, fromLiteral(regexp))
}
private fun AnnotatedString.Builder.addStyle(style: SpanStyle, regexp: Regex) {
for (result in regexp.findAll(toString())) {
addStyle(style, result.range.first, result.range.last + 1)
}
}

32
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editors.kt

@ -0,0 +1,32 @@
package org.jetbrains.codeviewer.ui.editor
import androidx.compose.runtime.mutableStateListOf
import org.jetbrains.codeviewer.platform.File
import org.jetbrains.codeviewer.util.SingleSelection
class Editors {
private val selection = SingleSelection()
var editors = mutableStateListOf<Editor>()
private set
val active: Editor? get() = selection.selected as Editor?
fun open(file: File) {
val editor = Editor(file)
editor.selection = selection
editor.close = {
close(editor)
}
editors.add(editor)
editor.activate()
}
private fun close(editor: Editor) {
val index = editors.indexOf(editor)
editors.remove(editor)
if (editor.isActive) {
selection.selected = editors.getOrNull(index.coerceAtMost(editors.lastIndex))
}
}
}

72
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/filetree/FileTree.kt

@ -0,0 +1,72 @@
package org.jetbrains.codeviewer.ui.filetree
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import org.jetbrains.codeviewer.platform.File
import org.jetbrains.codeviewer.ui.editor.Editors
class ExpandableFile(
val file: File,
val level: Int,
) {
var children: List<ExpandableFile> by mutableStateOf(emptyList())
val canExpand: Boolean get() = file.hasChildren
fun toggleExpanded() {
children = if (children.isEmpty()) {
file.children
.map { ExpandableFile(it, level + 1) }
.sortedWith(compareBy({ it.file.isDirectory }, { it.file.name }))
.sortedBy { !it.file.isDirectory }
} else {
emptyList()
}
}
}
class FileTree(root: File, private val editors: Editors) {
private val expandableRoot = ExpandableFile(root, 0).apply {
toggleExpanded()
}
val items: List<Item> get() = expandableRoot.toItems()
inner class Item constructor(
private val file: ExpandableFile
) {
val name: String get() = file.file.name
val level: Int get() = file.level
val type: ItemType
get() = if (file.file.isDirectory) {
ItemType.Folder(isExpanded = file.children.isNotEmpty(), canExpand = file.canExpand)
} else {
ItemType.File(ext = file.file.name.substringAfterLast(".").toLowerCase())
}
fun open() = when (type) {
is ItemType.Folder -> file.toggleExpanded()
is ItemType.File -> editors.open(file.file)
}
}
sealed class ItemType {
class Folder(val isExpanded: Boolean, val canExpand: Boolean) : ItemType()
class File(val ext: String) : ItemType()
}
private fun ExpandableFile.toItems(): List<Item> {
fun ExpandableFile.addTo(list: MutableList<Item>) {
list.add(Item(this))
for (child in children) {
child.addTo(list)
}
}
val list = mutableListOf<Item>()
addTo(list)
return list
}
}

128
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/filetree/FileTreeView.kt

@ -0,0 +1,128 @@
package org.jetbrains.codeviewer.ui.filetree
import androidx.compose.foundation.AmbientContentColor
import androidx.compose.foundation.Text
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumnFor
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.DensityAmbient
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.jetbrains.codeviewer.platform.VerticalScrollbar
import org.jetbrains.codeviewer.platform.pointerMoveFilter
import org.jetbrains.codeviewer.util.withoutWidthConstraints
@Composable
fun FileTreeViewTabView() = Surface {
Row(
Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Files",
color = AmbientContentColor.current.copy(alpha = 0.60f),
fontSize = 12.sp,
modifier = Modifier.padding(horizontal = 4.dp)
)
}
}
@Composable
fun FileTreeView(model: FileTree) = Surface(
modifier = Modifier.fillMaxSize()
) {
with(DensityAmbient.current) {
Box {
val scrollState = rememberLazyListState()
val fontSize = 14.sp
val lineHeight = fontSize.toDp() * 1.5f
LazyColumnFor(
model.items,
modifier = Modifier.fillMaxSize().withoutWidthConstraints(),
state = scrollState,
itemContent = { FileTreeItemView(fontSize, lineHeight, it) }
)
VerticalScrollbar(
Modifier.align(Alignment.CenterEnd),
scrollState,
model.items.size,
lineHeight
)
}
}
}
@Composable
private fun FileTreeItemView(fontSize: TextUnit, height: Dp, model: FileTree.Item) = Row(
modifier = Modifier
.wrapContentHeight()
.clickable { model.open() }
.padding(start = 24.dp * model.level)
.height(height)
.fillMaxWidth()
) {
val active = remember { mutableStateOf(false) }
FileItemIcon(Modifier.align(Alignment.CenterVertically), model)
Text(
text = model.name,
color = if (active.value) AmbientContentColor.current.copy(alpha = 0.60f) else AmbientContentColor.current,
modifier = Modifier
.align(Alignment.CenterVertically)
.clipToBounds()
.pointerMoveFilter(
onEnter = {
active.value = true
true
},
onExit = {
active.value = false
true
}
),
softWrap = true,
fontSize = fontSize,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
@Composable
private fun FileItemIcon(modifier: Modifier, model: FileTree.Item) = Box(modifier.size(24.dp).padding(4.dp)) {
when (val type = model.type) {
is FileTree.ItemType.Folder -> when {
!type.canExpand -> Unit
type.isExpanded -> Icon(Icons.Default.KeyboardArrowDown, tint = AmbientContentColor.current)
else -> Icon(Icons.Default.KeyboardArrowRight, tint = AmbientContentColor.current)
}
is FileTree.ItemType.File -> when (type.ext) {
"kt" -> Icon(Icons.Default.Code, tint = Color(0xFF3E86A0))
"xml" -> Icon(Icons.Default.Code, tint = Color(0xFFC19C5F))
"txt" -> Icon(Icons.Default.Description, tint = Color(0xFF87939A))
"md" -> Icon(Icons.Default.Description, tint = Color(0xFF87939A))
"gitignore" -> Icon(Icons.Default.BrokenImage, tint = Color(0xFF87939A))
"gradle" -> Icon(Icons.Default.Build, tint = Color(0xFF87939A))
"kts" -> Icon(Icons.Default.Build, tint = Color(0xFF3E86A0))
"properties" -> Icon(Icons.Default.Settings, tint = Color(0xFF62B543))
"bat" -> Icon(Icons.Default.Launch, tint = Color(0xFF87939A))
else -> Icon(Icons.Default.TextSnippet, tint = Color(0xFF87939A))
}
}
}

48
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/statusbar/StatusBar.kt

@ -0,0 +1,48 @@
package org.jetbrains.codeviewer.ui.statusbar
import androidx.compose.foundation.AmbientContentColor
import androidx.compose.foundation.Text
import androidx.compose.foundation.layout.*
import androidx.compose.material.Slider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Providers
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.DensityAmbient
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
import androidx.compose.ui.unit.sp
import org.jetbrains.codeviewer.ui.common.Settings
private val MinFontSize = 6.sp
private val MaxFontSize = 40.sp
@Composable
fun StatusBar(settings: Settings) = Box(
Modifier
.height(32.dp)
.fillMaxWidth()
.padding(4.dp)
) {
Row(Modifier.fillMaxHeight().align(Alignment.CenterEnd)) {
Text(
text = "Text size",
modifier = Modifier.align(Alignment.CenterVertically),
color = AmbientContentColor.current.copy(alpha = 0.60f),
fontSize = 12.sp
)
Spacer(Modifier.width(8.dp))
Providers(DensityAmbient provides DensityAmbient.current.scale(0.5f)) {
Slider(
(settings.fontSize - MinFontSize) / (MaxFontSize - MinFontSize),
onValueChange = { settings.fontSize = lerp(MinFontSize, MaxFontSize, it) },
modifier = Modifier.width(240.dp).align(Alignment.CenterVertically)
)
}
}
}
private fun Density.scale(scale: Float) = Density(density * scale, fontScale * scale)

11
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/LayoutModifiers.kt

@ -0,0 +1,11 @@
package org.jetbrains.codeviewer.util
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout
fun Modifier.withoutWidthConstraints() = layout { measurable, constraints ->
val placeable = measurable.measure(constraints.copy(maxWidth = Int.MAX_VALUE))
layout(constraints.maxWidth, placeable.height) {
placeable.place(0, 0)
}
}

33
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/LazyColumnFor.kt

@ -0,0 +1,33 @@
package org.jetbrains.codeviewer.util
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumnForIndexed
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun LazyColumnFor(
size: Int,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
horizontalGravity: Alignment.Horizontal = Alignment.Start,
itemContent: @Composable LazyItemScope.(index: Int) -> Unit
) = LazyColumnForIndexed(
UnitList(size),
modifier,
state,
contentPadding,
horizontalGravity,
) { index, _ ->
itemContent(index)
}
private class UnitList(override val size: Int) : AbstractList<Unit>() {
override fun get(index: Int) = Unit
}

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

@ -0,0 +1,25 @@
package org.jetbrains.codeviewer.util
import androidx.compose.runtime.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@Composable
fun <T : Any> loadable(load: suspend () -> T): MutableState<T?> {
return loadableScoped { load() }
}
@Composable
fun <T : Any> loadableScoped(load: suspend CoroutineScope.() -> T): MutableState<T?> {
val state: MutableState<T?> = remember { mutableStateOf(null) }
LaunchedTask {
try {
state.value = load()
} catch (e: CancellationException) {
// ignore
} catch (e: Exception) {
e.printStackTrace()
}
}
return state
}

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

@ -0,0 +1,9 @@
package org.jetbrains.codeviewer.util
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class SingleSelection {
var selected: Any? by mutableStateOf(null)
}

14
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/State.kt

@ -0,0 +1,14 @@
package org.jetbrains.codeviewer.util
import androidx.compose.runtime.MutableState
fun <T> MutableState<T>.afterSet(
action: (T) -> Unit
) = object : MutableState<T> by this {
override var value: T
get() = this@afterSet.value
set(value) {
this@afterSet.value = value
action(value)
}
}

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

@ -0,0 +1,6 @@
package org.jetbrains.codeviewer.util
interface TextLines {
val size: Int
suspend fun get(index: Int): String
}

91
examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/VerticalSplittable.kt

@ -0,0 +1,91 @@
package org.jetbrains.codeviewer.util
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Layout
import androidx.compose.ui.Modifier
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.jetbrains.codeviewer.platform.cursorForHorizontalResize
import org.jetbrains.codeviewer.ui.common.AppTheme
@Composable
fun VerticalSplittable(
modifier: Modifier,
splitterState: SplitterState,
onResize: (delta: Dp) -> Unit,
children: @Composable () -> Unit
) = Layout({
children()
VerticalSplitter(splitterState, onResize)
}, modifier, measureBlock = { measurables, constraints ->
require(measurables.size == 3)
val firstPlaceable = measurables[0].measure(constraints.copy(minWidth = 0))
val secondWidth = constraints.maxWidth - firstPlaceable.width
val secondPlaceable = measurables[1].measure(
Constraints(
minWidth = secondWidth,
maxWidth = secondWidth,
minHeight = constraints.maxHeight,
maxHeight = constraints.maxHeight
)
)
val splitterPlaceable = measurables[2].measure(constraints)
layout(constraints.maxWidth, constraints.maxHeight) {
firstPlaceable.place(0, 0)
secondPlaceable.place(firstPlaceable.width, 0)
splitterPlaceable.place(firstPlaceable.width, 0)
}
})
class SplitterState {
var isResizing by mutableStateOf(false)
var isResizeEnabled by mutableStateOf(true)
}
@Composable
fun VerticalSplitter(
splitterState: SplitterState,
onResize: (delta: Dp) -> Unit,
color: Color = AppTheme.colors.backgroundDark
) = Box {
Box(
Modifier
.width(8.dp)
.fillMaxHeight()
.run {
if (splitterState.isResizeEnabled) {
this.
draggable(
Orientation.Horizontal,
startDragImmediately = true,
onDragStarted = { splitterState.isResizing = true },
onDragStopped = { splitterState.isResizing = false }
) {
onResize(it.toDp())
}
.cursorForHorizontalResize()
} else {
this
}
}
)
Box(
Modifier
.width(1.dp)
.fillMaxHeight()
.background(color)
)
}

BIN
examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_bold.ttf

Binary file not shown.

BIN
examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_bold_italic.ttf

Binary file not shown.

BIN
examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_extrabold.ttf

Binary file not shown.

BIN
examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_extrabold_italic.ttf

Binary file not shown.

BIN
examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_italic.ttf

Binary file not shown.

BIN
examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_medium.ttf

Binary file not shown.

BIN
examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_medium_italic.ttf

Binary file not shown.

BIN
examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_regular.ttf

Binary file not shown.

5
examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/File.kt

@ -0,0 +1,5 @@
@file:Suppress("NewApi")
package org.jetbrains.codeviewer.platform
actual val HomeFolder: File get() = java.io.File(System.getProperty("user.home")).toProjectFile()

33
examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt

@ -0,0 +1,33 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.desktop.AppWindowAmbient
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerMoveFilter
import java.awt.Cursor
actual fun Modifier.pointerMoveFilter(
onEnter: () -> Boolean,
onExit: () -> Boolean,
onMove: (Offset) -> Boolean
): Modifier = this.pointerMoveFilter(onEnter = onEnter, onExit = onExit, onMove = onMove)
actual fun Modifier.cursorForHorizontalResize(): Modifier = composed {
var isHover by remember { mutableStateOf(false) }
if (isHover) {
AppWindowAmbient.current!!.window.cursor = Cursor(Cursor.E_RESIZE_CURSOR)
} else {
AppWindowAmbient.current!!.window.cursor = Cursor.getDefaultCursor()
}
pointerMoveFilter(
onEnter = { isHover = true; true },
onExit = { isHover = false; true }
)
}

25
examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt

@ -0,0 +1,25 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageAsset
import androidx.compose.ui.graphics.asImageAsset
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.skija.Image
import java.io.InputStream
import java.net.URL
@Composable
actual fun imageResource(res: String) = androidx.compose.ui.res.imageResource("drawable/$res.png")
actual suspend fun imageFromUrl(url: String): ImageAsset = withContext(Dispatchers.IO) {
val bytes = URL(url).openStream().buffered().use(InputStream::readBytes)
Image.makeFromEncoded(bytes).asImageAsset()
}
@Composable
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font =
androidx.compose.ui.text.platform.font(name, "font/$res.ttf", weight, style)

48
examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt

@ -0,0 +1,48 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.LazyScrollbarAdapter
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.DensityAmbient
import androidx.compose.ui.unit.Dp
@Composable
actual fun VerticalScrollbar(
modifier: Modifier,
scrollState: ScrollState
) = androidx.compose.foundation.VerticalScrollbar(
modifier,
adapter = rememberScrollbarAdapter(scrollState)
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
actual fun VerticalScrollbar(
modifier: Modifier,
scrollState: LazyListState,
itemCount: Int,
averageItemSize: Dp
) = androidx.compose.foundation.VerticalScrollbar(
modifier,
adapter = rememberScrollbarAdapterFixed(scrollState, itemCount, averageItemSize)
)
// TODO/migrateToMaster should be fixed in androidx-master-dev
@Composable
fun rememberScrollbarAdapterFixed(
scrollState: LazyListState,
itemCount: Int,
averageItemSize: Dp
): LazyScrollbarAdapter {
val density = DensityAmbient.current
return remember(density, scrollState, itemCount, averageItemSize) {
with(density) {
LazyScrollbarAdapter(scrollState, itemCount, averageItemSize.toPx())
}
}
}

26
examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt

@ -0,0 +1,26 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.DesktopSelectionContainer
import androidx.compose.ui.selection.Selection
@Composable
actual fun SelectionContainer(children: @Composable () -> Unit) {
val selection = remember { mutableStateOf<Selection?>(null) }
DesktopSelectionContainer(
selection = selection.value,
onSelectionChange = { selection.value = it },
children = children
)
}
@Composable
actual fun WithoutSelection(children: @Composable () -> Unit) {
androidx.compose.ui.selection.SelectionContainer(
selection = null,
onSelectionChange = {},
children = children
)
}

7
examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt

@ -0,0 +1,7 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.desktop.DesktopTheme
import androidx.compose.runtime.Composable
@Composable
actual fun PlatformTheme(content: @Composable () -> Unit) = DesktopTheme(content = content)

145
examples/codeviewer/common/src/jvmMain/kotlin/org/jetbrains/codeviewer/platform/JvmFile.kt

@ -0,0 +1,145 @@
package org.jetbrains.codeviewer.platform
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlinx.coroutines.*
import org.jetbrains.codeviewer.util.TextLines
import java.io.FileInputStream
import java.io.IOException
import java.io.RandomAccessFile
import java.nio.channels.FileChannel
fun java.io.File.toProjectFile(): File = object : File {
override val name: String get() = this@toProjectFile.name
override val isDirectory: Boolean get() = this@toProjectFile.isDirectory
override val children: List<File>
get() = this@toProjectFile
.listFiles()
.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()
var size by mutableStateOf(0)
val refreshJob = backgroundScope.launch {
delay(100)
size = linePositions.size
while (true) {
delay(1000)
size = linePositions.size
}
}
backgroundScope.launch {
readLinePositions(linePositions)
refreshJob.cancel()
size = linePositions.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>"
}
}
}
}
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun java.io.File.readLinePositions(list: IntList) = withContext(Dispatchers.IO) {
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
try {
FileInputStream(this@readLinePositions).use {
val channel = it.channel
val ib = channel.map(
FileChannel.MapMode.READ_ONLY, 0, channel.size()
)
while (ib.hasRemaining()) {
val byte = ib.get()
if (isBeginOfLine) {
list.add(position.toInt())
}
isBeginOfLine = byte.toChar() == '\n'
position++
}
}
} catch (e: IOException) {
e.printStackTrace()
list.clear(1)
list.add(0)
}
list.compact()
}
/**
* Compact version of List<Int> (without unboxing Int and using IntArray under the hood)
*/
private class IntList(initialCapacity: Int = 16) {
@Volatile
private var array = IntArray(initialCapacity)
@Volatile
var size: Int = 0
private set
fun clear(capacity: Int) {
array = IntArray(capacity)
size = 0
}
fun add(value: Int) {
if (size == array.size) {
doubleCapacity()
}
array[size++] = value
}
operator fun get(index: Int) = array[index]
private fun doubleCapacity() {
val newArray = IntArray(array.size * 2 + 1)
System.arraycopy(array, 0, newArray, 0, size)
array = newArray
}
fun compact() {
array = array.copyOfRange(0, size)
}
}

27
examples/codeviewer/desktop/build.gradle.kts

@ -0,0 +1,27 @@
import org.jetbrains.compose.compose
plugins {
kotlin("multiplatform") // kotlin("jvm") doesn't work well in IDEA/AndroidStudio (https://github.com/JetBrains/compose-jb/issues/22)
id("org.jetbrains.compose")
java
application
}
kotlin {
jvm {
withJava()
}
sourceSets {
named("jvmMain") {
dependencies {
implementation(compose.desktop.all)
implementation(project(":common"))
}
}
}
}
application {
mainClassName = "org.jetbrains.codeviewer.MainKt"
}

24
examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt

@ -0,0 +1,24 @@
package org.jetbrains.codeviewer
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.ExperimentalLayout
import androidx.compose.ui.unit.IntSize
import org.jetbrains.codeviewer.ui.MainView
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
@OptIn(ExperimentalLayout::class)
fun main() = 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)
}

BIN
examples/codeviewer/desktop/src/jvmMain/resources/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

21
examples/codeviewer/gradle.properties

@ -0,0 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official

BIN
examples/codeviewer/gradle/wrapper/gradle-wrapper.jar vendored

Binary file not shown.

5
examples/codeviewer/gradle/wrapper/gradle-wrapper.properties vendored

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

183
examples/codeviewer/gradlew vendored

@ -0,0 +1,183 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

100
examples/codeviewer/gradlew.bat vendored

@ -0,0 +1,100 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
examples/codeviewer/settings.gradle.kts

@ -0,0 +1 @@
include(":common", ":android", ":desktop")
Loading…
Cancel
Save