Browse Source
* Move experimental/examples to examples * Change version to 1.4.0-rc01pull/2977/head
Nikita Lipsky
2 years ago
committed by
GitHub
938 changed files with 1484 additions and 13809 deletions
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
@ -1,21 +0,0 @@ |
|||||||
<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> |
|
@ -1,26 +0,0 @@ |
|||||||
plugins { |
|
||||||
id("com.android.application") |
|
||||||
kotlin("android") |
|
||||||
id("org.jetbrains.compose") |
|
||||||
} |
|
||||||
|
|
||||||
android { |
|
||||||
compileSdk = 33 |
|
||||||
|
|
||||||
defaultConfig { |
|
||||||
minSdk = 26 |
|
||||||
targetSdk = 33 |
|
||||||
versionCode = 1 |
|
||||||
versionName = "1.0" |
|
||||||
} |
|
||||||
|
|
||||||
compileOptions { |
|
||||||
sourceCompatibility = JavaVersion.VERSION_11 |
|
||||||
targetCompatibility = JavaVersion.VERSION_11 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
dependencies { |
|
||||||
implementation(project(":common")) |
|
||||||
implementation("androidx.activity:activity-compose:1.5.0") |
|
||||||
} |
|
@ -1,32 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer |
|
||||||
|
|
||||||
import android.os.Bundle |
|
||||||
import androidx.activity.compose.setContent |
|
||||||
import androidx.appcompat.app.AppCompatActivity |
|
||||||
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) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,50 +0,0 @@ |
|||||||
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.5.1") |
|
||||||
api("androidx.core:core-ktx:1.8.0") |
|
||||||
} |
|
||||||
} |
|
||||||
named("desktopMain") { |
|
||||||
kotlin.srcDirs("src/jvmMain/kotlin") |
|
||||||
dependencies { |
|
||||||
api(compose.desktop.common) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
android { |
|
||||||
compileSdk = 33 |
|
||||||
|
|
||||||
defaultConfig { |
|
||||||
minSdk = 26 |
|
||||||
targetSdk = 33 |
|
||||||
} |
|
||||||
|
|
||||||
sourceSets { |
|
||||||
named("main") { |
|
||||||
manifest.srcFile("src/androidMain/AndroidManifest.xml") |
|
||||||
res.srcDirs("src/androidMain/res", "src/commonMain/resources") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,9 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.platform |
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import androidx.compose.ui.text.font.Font |
|
||||||
import androidx.compose.ui.text.font.FontStyle |
|
||||||
import androidx.compose.ui.text.font.FontWeight |
|
||||||
|
|
||||||
@Composable |
|
||||||
expect fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font |
|
@ -1,19 +0,0 @@ |
|||||||
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 |
|
||||||
) |
|
@ -1,107 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.ui |
|
||||||
|
|
||||||
import androidx.compose.animation.core.Spring.StiffnessLow |
|
||||||
import androidx.compose.animation.core.SpringSpec |
|
||||||
import androidx.compose.animation.core.animateDpAsState |
|
||||||
import androidx.compose.animation.core.animateFloatAsState |
|
||||||
import androidx.compose.foundation.clickable |
|
||||||
import androidx.compose.foundation.layout.* |
|
||||||
import androidx.compose.material.Icon |
|
||||||
import androidx.compose.material.LocalContentColor |
|
||||||
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.graphics.graphicsLayer |
|
||||||
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 { |
|
||||||
animateDpAsState( |
|
||||||
if (panelState.isExpanded) panelState.expandedSize else panelState.collapsedSize, |
|
||||||
SpringSpec(stiffness = StiffnessLow) |
|
||||||
).value |
|
||||||
} |
|
||||||
|
|
||||||
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 by animateFloatAsState(if (state.isExpanded) 1f else 0f, SpringSpec(stiffness = StiffnessLow)) |
|
||||||
|
|
||||||
Box(modifier) { |
|
||||||
Box(Modifier.fillMaxSize().graphicsLayer(alpha = alpha)) { |
|
||||||
content() |
|
||||||
} |
|
||||||
|
|
||||||
Icon( |
|
||||||
if (state.isExpanded) Icons.Default.ArrowBack else Icons.Default.ArrowForward, |
|
||||||
contentDescription = if (state.isExpanded) "Collapse" else "Expand", |
|
||||||
tint = LocalContentColor.current, |
|
||||||
modifier = Modifier |
|
||||||
.padding(top = 4.dp) |
|
||||||
.width(24.dp) |
|
||||||
.clickable { |
|
||||||
state.isExpanded = !state.isExpanded |
|
||||||
} |
|
||||||
.padding(4.dp) |
|
||||||
.align(Alignment.TopEnd) |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
@ -1,35 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.ui |
|
||||||
|
|
||||||
import androidx.compose.foundation.text.selection.DisableSelection |
|
||||||
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.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() |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
DisableSelection { |
|
||||||
MaterialTheme( |
|
||||||
colors = AppTheme.colors.material |
|
||||||
) { |
|
||||||
Surface { |
|
||||||
CodeViewerView(codeViewer) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,64 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.ui.common |
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import androidx.compose.ui.text.font.FontFamily |
|
||||||
import androidx.compose.ui.text.font.FontStyle |
|
||||||
import androidx.compose.ui.text.font.FontWeight |
|
||||||
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 |
|
||||||
) |
|
||||||
) |
|
||||||
} |
|
@ -1,35 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.ui.editor |
|
||||||
|
|
||||||
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.LocalContentColor |
|
||||||
import androidx.compose.material.Text |
|
||||||
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, |
|
||||||
contentDescription = null, |
|
||||||
tint = LocalContentColor.current.copy(alpha = 0.60f), |
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally) |
|
||||||
) |
|
||||||
|
|
||||||
Text( |
|
||||||
"To view file open it from the file tree", |
|
||||||
color = LocalContentColor.current.copy(alpha = 0.60f), |
|
||||||
fontSize = 20.sp, |
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally).padding(16.dp) |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
@ -1,78 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.ui.editor |
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable |
|
||||||
import androidx.compose.foundation.horizontalScroll |
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource |
|
||||||
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.foundation.rememberScrollState |
|
||||||
import androidx.compose.material.Icon |
|
||||||
import androidx.compose.material.LocalContentColor |
|
||||||
import androidx.compose.material.Surface |
|
||||||
import androidx.compose.material.Text |
|
||||||
import androidx.compose.material.icons.Icons |
|
||||||
import androidx.compose.material.icons.filled.Close |
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import androidx.compose.runtime.remember |
|
||||||
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) = Row(Modifier.horizontalScroll(rememberScrollState())) { |
|
||||||
for (editor in model.editors) { |
|
||||||
EditorTabView(editor) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Composable |
|
||||||
fun EditorTabView(model: Editor) = Surface( |
|
||||||
color = if (model.isActive) { |
|
||||||
AppTheme.colors.backgroundDark |
|
||||||
} else { |
|
||||||
Color.Transparent |
|
||||||
} |
|
||||||
) { |
|
||||||
Row( |
|
||||||
Modifier |
|
||||||
.clickable(remember(::MutableInteractionSource), indication = null) { |
|
||||||
model.activate() |
|
||||||
} |
|
||||||
.padding(4.dp), |
|
||||||
verticalAlignment = Alignment.CenterVertically |
|
||||||
) { |
|
||||||
Text( |
|
||||||
model.fileName, |
|
||||||
color = LocalContentColor.current, |
|
||||||
fontSize = 12.sp, |
|
||||||
modifier = Modifier.padding(horizontal = 4.dp) |
|
||||||
) |
|
||||||
|
|
||||||
val close = model.close |
|
||||||
|
|
||||||
if (close != null) { |
|
||||||
Icon( |
|
||||||
Icons.Default.Close, |
|
||||||
tint = LocalContentColor.current, |
|
||||||
contentDescription = "Close", |
|
||||||
modifier = Modifier |
|
||||||
.size(24.dp) |
|
||||||
.padding(4.dp) |
|
||||||
.clickable { |
|
||||||
close() |
|
||||||
} |
|
||||||
) |
|
||||||
} else { |
|
||||||
Box( |
|
||||||
modifier = Modifier |
|
||||||
.size(24.dp, 24.dp) |
|
||||||
.padding(4.dp) |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,186 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.ui.editor |
|
||||||
|
|
||||||
import androidx.compose.foundation.background |
|
||||||
import androidx.compose.foundation.layout.* |
|
||||||
import androidx.compose.foundation.lazy.LazyColumn |
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState |
|
||||||
import androidx.compose.foundation.text.selection.DisableSelection |
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer |
|
||||||
import androidx.compose.material.CircularProgressIndicator |
|
||||||
import androidx.compose.material.LocalContentColor |
|
||||||
import androidx.compose.material.Surface |
|
||||||
import androidx.compose.material.Text |
|
||||||
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.alpha |
|
||||||
import androidx.compose.ui.platform.LocalDensity |
|
||||||
import androidx.compose.ui.text.AnnotatedString |
|
||||||
import androidx.compose.ui.text.SpanStyle |
|
||||||
import androidx.compose.ui.text.buildAnnotatedString |
|
||||||
import androidx.compose.ui.text.withStyle |
|
||||||
import androidx.compose.ui.unit.dp |
|
||||||
import org.jetbrains.codeviewer.platform.VerticalScrollbar |
|
||||||
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.loadableScoped |
|
||||||
import org.jetbrains.codeviewer.util.withoutWidthConstraints |
|
||||||
import kotlin.text.Regex.Companion.fromLiteral |
|
||||||
|
|
||||||
@Composable |
|
||||||
fun EditorView(model: Editor, settings: Settings) = key(model) { |
|
||||||
with (LocalDensity.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(LocalDensity.current) { |
|
||||||
val maxNum = remember(lines.lineNumberDigitCount) { |
|
||||||
(1..lines.lineNumberDigitCount).joinToString(separator = "") { "9" } |
|
||||||
} |
|
||||||
|
|
||||||
Box(Modifier.fillMaxSize()) { |
|
||||||
val scrollState = rememberLazyListState() |
|
||||||
|
|
||||||
LazyColumn( |
|
||||||
modifier = Modifier.fillMaxSize(), |
|
||||||
state = scrollState |
|
||||||
) { |
|
||||||
items(lines.size) { index -> |
|
||||||
Box(Modifier.height(settings.fontSize.toDp() * 1.6f)) { |
|
||||||
Line(Modifier.align(Alignment.CenterStart), maxNum, lines[index], settings) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
VerticalScrollbar( |
|
||||||
Modifier.align(Alignment.CenterEnd), |
|
||||||
scrollState |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Поддержка русского языка |
|
||||||
// دعم اللغة العربية |
|
||||||
// 中文支持 |
|
||||||
@Composable |
|
||||||
private fun Line(modifier: Modifier, maxNum: String, line: Editor.Line, settings: Settings) { |
|
||||||
Row(modifier = modifier) { |
|
||||||
DisableSelection { |
|
||||||
Box { |
|
||||||
LineNumber(maxNum, Modifier.alpha(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 = LocalContentColor.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 { |
|
||||||
buildAnnotatedString { |
|
||||||
withStyle(AppTheme.code.simple) { |
|
||||||
append(content.value.value) |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
fontSize = settings.fontSize, |
|
||||||
fontFamily = Fonts.jetbrainsMono(), |
|
||||||
maxLines = 1, |
|
||||||
modifier = modifier, |
|
||||||
softWrap = false |
|
||||||
) |
|
||||||
|
|
||||||
private fun codeString(str: String) = buildAnnotatedString { |
|
||||||
withStyle(AppTheme.code.simple) { |
|
||||||
val strFormatted = str.replace("\t", " ") |
|
||||||
append(strFormatted) |
|
||||||
addStyle(AppTheme.code.punctuation, strFormatted, ":") |
|
||||||
addStyle(AppTheme.code.punctuation, strFormatted, "=") |
|
||||||
addStyle(AppTheme.code.punctuation, strFormatted, "\"") |
|
||||||
addStyle(AppTheme.code.punctuation, strFormatted, "[") |
|
||||||
addStyle(AppTheme.code.punctuation, strFormatted, "]") |
|
||||||
addStyle(AppTheme.code.punctuation, strFormatted, "{") |
|
||||||
addStyle(AppTheme.code.punctuation, strFormatted, "}") |
|
||||||
addStyle(AppTheme.code.punctuation, strFormatted, "(") |
|
||||||
addStyle(AppTheme.code.punctuation, strFormatted, ")") |
|
||||||
addStyle(AppTheme.code.punctuation, strFormatted, ",") |
|
||||||
addStyle(AppTheme.code.keyword, strFormatted, "fun ") |
|
||||||
addStyle(AppTheme.code.keyword, strFormatted, "val ") |
|
||||||
addStyle(AppTheme.code.keyword, strFormatted, "var ") |
|
||||||
addStyle(AppTheme.code.keyword, strFormatted, "private ") |
|
||||||
addStyle(AppTheme.code.keyword, strFormatted, "internal ") |
|
||||||
addStyle(AppTheme.code.keyword, strFormatted, "for ") |
|
||||||
addStyle(AppTheme.code.keyword, strFormatted, "expect ") |
|
||||||
addStyle(AppTheme.code.keyword, strFormatted, "actual ") |
|
||||||
addStyle(AppTheme.code.keyword, strFormatted, "import ") |
|
||||||
addStyle(AppTheme.code.keyword, strFormatted, "package ") |
|
||||||
addStyle(AppTheme.code.value, strFormatted, "true") |
|
||||||
addStyle(AppTheme.code.value, strFormatted, "false") |
|
||||||
addStyle(AppTheme.code.value, strFormatted, Regex("[0-9]+")) |
|
||||||
addStyle(AppTheme.code.annotation, strFormatted, Regex("^@[a-zA-Z_]*")) |
|
||||||
addStyle(AppTheme.code.comment, strFormatted, Regex("^\\s*//.*")) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun AnnotatedString.Builder.addStyle(style: SpanStyle, text: String, regexp: String) { |
|
||||||
addStyle(style, text, fromLiteral(regexp)) |
|
||||||
} |
|
||||||
|
|
||||||
private fun AnnotatedString.Builder.addStyle(style: SpanStyle, text: String, regexp: Regex) { |
|
||||||
for (result in regexp.findAll(text)) { |
|
||||||
addStyle(style, result.range.first, result.range.last + 1) |
|
||||||
} |
|
||||||
} |
|
@ -1,125 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.ui.filetree |
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable |
|
||||||
import androidx.compose.foundation.hoverable |
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource |
|
||||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState |
|
||||||
import androidx.compose.foundation.layout.* |
|
||||||
import androidx.compose.foundation.lazy.LazyColumn |
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState |
|
||||||
import androidx.compose.material.Icon |
|
||||||
import androidx.compose.material.LocalContentColor |
|
||||||
import androidx.compose.material.Surface |
|
||||||
import androidx.compose.material.Text |
|
||||||
import androidx.compose.material.icons.Icons |
|
||||||
import androidx.compose.material.icons.filled.* |
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import androidx.compose.runtime.getValue |
|
||||||
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.LocalDensity |
|
||||||
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.util.withoutWidthConstraints |
|
||||||
|
|
||||||
@Composable |
|
||||||
fun FileTreeViewTabView() = Surface { |
|
||||||
Row( |
|
||||||
Modifier.padding(8.dp), |
|
||||||
verticalAlignment = Alignment.CenterVertically |
|
||||||
) { |
|
||||||
Text( |
|
||||||
"Files", |
|
||||||
color = LocalContentColor.current.copy(alpha = 0.60f), |
|
||||||
fontSize = 12.sp, |
|
||||||
modifier = Modifier.padding(horizontal = 4.dp) |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Composable |
|
||||||
fun FileTreeView(model: FileTree) = Surface( |
|
||||||
modifier = Modifier.fillMaxSize() |
|
||||||
) { |
|
||||||
with(LocalDensity.current) { |
|
||||||
Box { |
|
||||||
val scrollState = rememberLazyListState() |
|
||||||
|
|
||||||
LazyColumn( |
|
||||||
modifier = Modifier.fillMaxSize().withoutWidthConstraints(), |
|
||||||
state = scrollState |
|
||||||
) { |
|
||||||
items(model.items.size) { |
|
||||||
FileTreeItemView(14.sp, 14.sp.toDp() * 1.5f, model.items[it]) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
VerticalScrollbar( |
|
||||||
Modifier.align(Alignment.CenterEnd), |
|
||||||
scrollState |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@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 interactionSource = remember { MutableInteractionSource() } |
|
||||||
val active by interactionSource.collectIsHoveredAsState() |
|
||||||
|
|
||||||
FileItemIcon(Modifier.align(Alignment.CenterVertically), model) |
|
||||||
Text( |
|
||||||
text = model.name, |
|
||||||
color = if (active) LocalContentColor.current.copy(alpha = 0.60f) else LocalContentColor.current, |
|
||||||
modifier = Modifier |
|
||||||
.align(Alignment.CenterVertically) |
|
||||||
.clipToBounds() |
|
||||||
.hoverable(interactionSource), |
|
||||||
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, contentDescription = null, tint = LocalContentColor.current |
|
||||||
) |
|
||||||
else -> Icon( |
|
||||||
Icons.Default.KeyboardArrowRight, contentDescription = null, tint = LocalContentColor.current |
|
||||||
) |
|
||||||
} |
|
||||||
is FileTree.ItemType.File -> when (type.ext) { |
|
||||||
"kt" -> Icon(Icons.Default.Code, contentDescription = null, tint = Color(0xFF3E86A0)) |
|
||||||
"xml" -> Icon(Icons.Default.Code, contentDescription = null, tint = Color(0xFFC19C5F)) |
|
||||||
"txt" -> Icon(Icons.Default.Description, contentDescription = null, tint = Color(0xFF87939A)) |
|
||||||
"md" -> Icon(Icons.Default.Description, contentDescription = null, tint = Color(0xFF87939A)) |
|
||||||
"gitignore" -> Icon(Icons.Default.BrokenImage, contentDescription = null, tint = Color(0xFF87939A)) |
|
||||||
"gradle" -> Icon(Icons.Default.Build, contentDescription = null, tint = Color(0xFF87939A)) |
|
||||||
"kts" -> Icon(Icons.Default.Build, contentDescription = null, tint = Color(0xFF3E86A0)) |
|
||||||
"properties" -> Icon(Icons.Default.Settings, contentDescription = null, tint = Color(0xFF62B543)) |
|
||||||
"bat" -> Icon(Icons.Default.Launch, contentDescription = null, tint = Color(0xFF87939A)) |
|
||||||
else -> Icon(Icons.Default.TextSnippet, contentDescription = null, tint = Color(0xFF87939A)) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,47 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.ui.statusbar |
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.* |
|
||||||
import androidx.compose.material.LocalContentColor |
|
||||||
import androidx.compose.material.Slider |
|
||||||
import androidx.compose.material.Text |
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import androidx.compose.runtime.CompositionLocalProvider |
|
||||||
import androidx.compose.ui.Alignment |
|
||||||
import androidx.compose.ui.Modifier |
|
||||||
import androidx.compose.ui.platform.LocalDensity |
|
||||||
import androidx.compose.ui.unit.* |
|
||||||
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 = LocalContentColor.current.copy(alpha = 0.60f), |
|
||||||
fontSize = 12.sp |
|
||||||
) |
|
||||||
|
|
||||||
Spacer(Modifier.width(8.dp)) |
|
||||||
|
|
||||||
CompositionLocalProvider(LocalDensity provides LocalDensity.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) |
|
||||||
private operator fun TextUnit.minus(other: TextUnit) = (value - other.value).sp |
|
||||||
private operator fun TextUnit.div(other: TextUnit) = value / other.value |
|
@ -1,27 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.util |
|
||||||
|
|
||||||
import androidx.compose.runtime.* |
|
||||||
import kotlinx.coroutines.CancellationException |
|
||||||
import kotlinx.coroutines.CoroutineScope |
|
||||||
|
|
||||||
@Composable |
|
||||||
fun <T : Any> loadable(load: () -> T): MutableState<T?> { |
|
||||||
return loadableScoped { load() } |
|
||||||
} |
|
||||||
|
|
||||||
private val loadingKey = Any() |
|
||||||
|
|
||||||
@Composable |
|
||||||
fun <T : Any> loadableScoped(load: CoroutineScope.() -> T): MutableState<T?> { |
|
||||||
val state: MutableState<T?> = remember { mutableStateOf(null) } |
|
||||||
LaunchedEffect(loadingKey) { |
|
||||||
try { |
|
||||||
state.value = load() |
|
||||||
} catch (e: CancellationException) { |
|
||||||
// ignore |
|
||||||
} catch (e: Exception) { |
|
||||||
e.printStackTrace() |
|
||||||
} |
|
||||||
} |
|
||||||
return state |
|
||||||
} |
|
@ -1,95 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.util |
|
||||||
|
|
||||||
import androidx.compose.foundation.background |
|
||||||
import androidx.compose.foundation.gestures.Orientation |
|
||||||
import androidx.compose.foundation.gestures.draggable |
|
||||||
import androidx.compose.foundation.gestures.rememberDraggableState |
|
||||||
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.Modifier |
|
||||||
import androidx.compose.ui.graphics.Color |
|
||||||
import androidx.compose.ui.layout.Layout |
|
||||||
import androidx.compose.ui.platform.LocalDensity |
|
||||||
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, measurePolicy = { 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 { |
|
||||||
val density = LocalDensity.current |
|
||||||
Box( |
|
||||||
Modifier |
|
||||||
.width(8.dp) |
|
||||||
.fillMaxHeight() |
|
||||||
.run { |
|
||||||
if (splitterState.isResizeEnabled) { |
|
||||||
this.draggable( |
|
||||||
state = rememberDraggableState { |
|
||||||
with(density) { |
|
||||||
onResize(it.toDp()) |
|
||||||
} |
|
||||||
}, |
|
||||||
orientation = Orientation.Horizontal, |
|
||||||
startDragImmediately = true, |
|
||||||
onDragStarted = { splitterState.isResizing = true }, |
|
||||||
onDragStopped = { splitterState.isResizing = false } |
|
||||||
).cursorForHorizontalResize() |
|
||||||
} else { |
|
||||||
this |
|
||||||
} |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
Box( |
|
||||||
Modifier |
|
||||||
.width(1.dp) |
|
||||||
.fillMaxHeight() |
|
||||||
.background(color) |
|
||||||
) |
|
||||||
} |
|
@ -1,10 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer.platform |
|
||||||
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi |
|
||||||
import androidx.compose.ui.Modifier |
|
||||||
import androidx.compose.ui.input.pointer.PointerIcon |
|
||||||
import androidx.compose.ui.input.pointer.pointerHoverIcon |
|
||||||
import java.awt.Cursor |
|
||||||
|
|
||||||
actual fun Modifier.cursorForHorizontalResize(): Modifier = |
|
||||||
this.pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))) |
|
@ -1,177 +0,0 @@ |
|||||||
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.FilenameFilter |
|
||||||
import java.io.IOException |
|
||||||
import java.io.RandomAccessFile |
|
||||||
import java.nio.ByteBuffer |
|
||||||
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 |
|
||||||
|
|
||||||
override val isDirectory: Boolean |
|
||||||
get() = this@toProjectFile.isDirectory |
|
||||||
|
|
||||||
override val children: List<File> |
|
||||||
get() = this@toProjectFile |
|
||||||
.listFiles(FilenameFilter { _, name -> !name.startsWith(".")}) |
|
||||||
.orEmpty() |
|
||||||
.map { it.toProjectFile() } |
|
||||||
|
|
||||||
private val numberOfFiles |
|
||||||
get() = listFiles()?.size ?: 0 |
|
||||||
|
|
||||||
override val hasChildren: Boolean |
|
||||||
get() = isDirectory && numberOfFiles > 0 |
|
||||||
|
|
||||||
|
|
||||||
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) |
|
||||||
|
|
||||||
// In case of big files, update number of lines periodically |
|
||||||
val refreshJob = scope.launch { |
|
||||||
delay(100) |
|
||||||
size = lineStartPositions.size |
|
||||||
while (isActive) { |
|
||||||
delay(1000) |
|
||||||
size = lineStartPositions.size |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Find indexes where lines starts in background |
|
||||||
scope.launch(Dispatchers.IO) { |
|
||||||
readLinePositions(lineStartPositions) |
|
||||||
refreshJob.cancel() |
|
||||||
size = lineStartPositions.size |
|
||||||
} |
|
||||||
|
|
||||||
return object : TextLines { |
|
||||||
override val size get() = size |
|
||||||
|
|
||||||
override fun get(index: Int): String { |
|
||||||
val position = lineRange(index) |
|
||||||
val slice = byteBuffer.slice(position.first, position.last - position.first) |
|
||||||
return StandardCharsets.UTF_8.decode(slice).toString() |
|
||||||
} |
|
||||||
|
|
||||||
private fun lineRange(index: Int): IntRange { |
|
||||||
val startPosition = lineStartPositions[index] |
|
||||||
val nextLineIndex = index + 1 |
|
||||||
var endPosition = if (nextLineIndex < size) lineStartPositions[nextLineIndex] else byteBufferSize |
|
||||||
|
|
||||||
// Remove line endings from the range |
|
||||||
while (endPosition > startPosition) { |
|
||||||
val lastSymbol = byteBuffer[endPosition - 1] |
|
||||||
when (lastSymbol.toInt().toChar()) { |
|
||||||
'\n', '\r' -> endPosition-- |
|
||||||
else -> break |
|
||||||
} |
|
||||||
} |
|
||||||
return startPosition..endPosition |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Backport slice from JDK 13 |
|
||||||
private fun ByteBuffer.slice(index: Int, length: Int): ByteBuffer { |
|
||||||
position(index) |
|
||||||
return slice().limit(length) |
|
||||||
} |
|
||||||
|
|
||||||
private fun java.io.File.readLinePositions(starts: IntList) { |
|
||||||
require(length() <= Int.MAX_VALUE) { |
|
||||||
"Files with size over ${Int.MAX_VALUE} aren't supported" |
|
||||||
} |
|
||||||
|
|
||||||
val averageLineLength = 200 |
|
||||||
starts.clear(length().toInt() / averageLineLength) |
|
||||||
|
|
||||||
try { |
|
||||||
for (i in readLinePositions()) { |
|
||||||
starts.add(i) |
|
||||||
} |
|
||||||
} catch (e: IOException) { |
|
||||||
e.printStackTrace() |
|
||||||
starts.clear(1) |
|
||||||
starts.add(0) |
|
||||||
} |
|
||||||
|
|
||||||
starts.compact() |
|
||||||
} |
|
||||||
|
|
||||||
private fun java.io.File.readLinePositions() = sequence { |
|
||||||
require(length() <= Int.MAX_VALUE) { |
|
||||||
"Files with size over ${Int.MAX_VALUE} aren't supported" |
|
||||||
} |
|
||||||
readBuffer { |
|
||||||
yield(position()) |
|
||||||
while (hasRemaining()) { |
|
||||||
val byte = get() |
|
||||||
if (byte.isChar('\n')) { |
|
||||||
yield(position()) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private inline fun java.io.File.readBuffer(block: ByteBuffer.() -> Unit) { |
|
||||||
FileInputStream(this).use { stream -> |
|
||||||
stream.channel.use { channel -> |
|
||||||
channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()).block() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun Byte.isChar(char: Char) = toInt().toChar() == char |
|
||||||
|
|
||||||
/** |
|
||||||
* Compact version of List<Int> (without unboxing Int and using IntArray under the hood) |
|
||||||
*/ |
|
||||||
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) |
|
||||||
} |
|
||||||
} |
|
@ -1,41 +0,0 @@ |
|||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat |
|
||||||
|
|
||||||
plugins { |
|
||||||
kotlin("multiplatform") // kotlin("jvm") doesn't work well in IDEA/AndroidStudio (https://github.com/JetBrains/compose-jb/issues/22) |
|
||||||
id("org.jetbrains.compose") |
|
||||||
} |
|
||||||
|
|
||||||
kotlin { |
|
||||||
jvm {} |
|
||||||
sourceSets { |
|
||||||
named("jvmMain") { |
|
||||||
dependencies { |
|
||||||
implementation(compose.desktop.currentOs) |
|
||||||
implementation(project(":common")) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
compose.desktop { |
|
||||||
application { |
|
||||||
mainClass = "org.jetbrains.codeviewer.MainKt" |
|
||||||
|
|
||||||
nativeDistributions { |
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) |
|
||||||
packageName = "ComposeCodeViewer" |
|
||||||
packageVersion = "1.0.0" |
|
||||||
|
|
||||||
windows { |
|
||||||
menu = true |
|
||||||
// see https://wixtoolset.org/documentation/manual/v3/howtos/general/generate_guids.html |
|
||||||
upgradeUuid = "AF792DA6-2EA3-495A-95E5-C3C6CBCB9948" |
|
||||||
} |
|
||||||
|
|
||||||
macOS { |
|
||||||
// Use -Pcompose.desktop.mac.sign=true to sign and notarize. |
|
||||||
bundleID = "com.jetbrains.compose.codeviewer" |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,17 +0,0 @@ |
|||||||
package org.jetbrains.codeviewer |
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.painter.BitmapPainter |
|
||||||
import androidx.compose.ui.res.loadImageBitmap |
|
||||||
import androidx.compose.ui.res.useResource |
|
||||||
import androidx.compose.ui.unit.dp |
|
||||||
import androidx.compose.ui.window.WindowState |
|
||||||
import androidx.compose.ui.window.singleWindowApplication |
|
||||||
import org.jetbrains.codeviewer.ui.MainView |
|
||||||
|
|
||||||
fun main() = singleWindowApplication( |
|
||||||
title = "Code Viewer", |
|
||||||
state = WindowState(width = 1280.dp, height = 768.dp), |
|
||||||
icon = BitmapPainter(useResource("ic_launcher.png", ::loadImageBitmap)), |
|
||||||
) { |
|
||||||
MainView() |
|
||||||
} |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
@ -1,24 +1,16 @@ |
|||||||
# 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 |
kotlin.code.style=official |
||||||
kotlin.version=1.8.10 |
xcodeproj=./iosApp |
||||||
|
kotlin.native.cocoapods.generate.wrapper=true |
||||||
|
android.useAndroidX=true |
||||||
|
org.gradle.jvmargs=-Xmx3g |
||||||
|
org.jetbrains.compose.experimental.jscanvas.enabled=true |
||||||
|
org.jetbrains.compose.experimental.macos.enabled=true |
||||||
|
org.jetbrains.compose.experimental.uikit.enabled=true |
||||||
|
kotlin.native.cacheKind=none |
||||||
|
kotlin.native.useEmbeddableCompilerJar=true |
||||||
|
kotlin.mpp.androidSourceSetLayoutVersion=2 |
||||||
|
# Enable kotlin/native experimental memory model |
||||||
|
kotlin.native.binary.memoryModel=experimental |
||||||
|
kotlin.version=1.8.0 |
||||||
agp.version=7.1.3 |
agp.version=7.1.3 |
||||||
compose.version=1.3.1 |
compose.version=1.4.0-rc01 |
||||||
|
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue