package org.jetbrains.codeviewer.ui.editor import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width 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.MaterialTheme 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 = MaterialTheme.colors.background, ) { 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.codeGuide) ) } } 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 ) @Composable 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*//.*")) // Keeps copied lines separated and fixes crash during selection: // https://partnerissuetracker.corp.google.com/issues/199919707 append("\n") } } 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) } }