diff --git a/editor/build.gradle b/editor/build.gradle index 5d2913bf..b767fc47 100644 --- a/editor/build.gradle +++ b/editor/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation platform("org.kordamp.ikonli:ikonli-bom:${ikonliVersion}") implementation 'org.kordamp.ikonli:ikonli-javafx' implementation 'org.kordamp.ikonli:ikonli-fontawesome-pack' + implementation "org.fxmisc.richtext:richtextfx:${richtextfxVersion}" // Spring implementation 'org.springframework.boot:spring-boot-starter' diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/component/CodeEditor.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/component/CodeEditor.kt new file mode 100644 index 00000000..cc40846d --- /dev/null +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/component/CodeEditor.kt @@ -0,0 +1,61 @@ +package com.bartlomiejpluta.base.editor.code.component + +import com.bartlomiejpluta.base.editor.code.highlighting.JavaSyntaxHighlighter +import com.bartlomiejpluta.base.editor.code.stylesheet.HighlightingStylesheet +import javafx.beans.property.Property +import javafx.concurrent.Task +import javafx.scene.layout.StackPane +import org.fxmisc.flowless.VirtualizedScrollPane +import org.fxmisc.richtext.CodeArea +import org.fxmisc.richtext.LineNumberFactory +import org.fxmisc.richtext.model.StyleSpans +import java.time.Duration +import java.util.* +import java.util.concurrent.Executors + + +class CodeEditor(val codeProperty: Property) : StackPane() { + private val editor = CodeArea() + private val executor = Executors.newSingleThreadExecutor() + private val cleanupWhenDone = editor.multiPlainChanges() + + private val highlighting = JavaSyntaxHighlighter() + + init { + editor.paragraphGraphicFactory = LineNumberFactory.get(editor) + editor.replaceText(0, 0, codeProperty.value) + applyHighlighting(highlighting.highlight(editor.text)) + + cleanupWhenDone + .successionEnds(Duration.ofMillis(500)) + .supplyTask(this::computeHighlightingAsync) + .awaitLatest(editor.multiPlainChanges()) + .filterMap { + when { + it.isSuccess -> Optional.of(it.get()) + else -> Optional.empty() + } + } + .subscribe(this::applyHighlighting) + + + children += VirtualizedScrollPane(editor) + } + + private fun computeHighlightingAsync(): Task>> { + val code = editor.text + + val task = object : Task>>() { + override fun call() = highlighting.highlight(code) + } + + executor.execute(task) + return task + } + + private fun applyHighlighting(highlighting: StyleSpans>) { + editor.setStyleSpans(0, highlighting) + } + + override fun getUserAgentStylesheet() = HighlightingStylesheet().base64URL.toExternalForm() +} \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/highlighting/JavaSyntaxHighlighter.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/highlighting/JavaSyntaxHighlighter.kt new file mode 100644 index 00000000..66c2bdd3 --- /dev/null +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/highlighting/JavaSyntaxHighlighter.kt @@ -0,0 +1,66 @@ +package com.bartlomiejpluta.base.editor.code.highlighting + +import org.fxmisc.richtext.model.StyleSpans +import org.fxmisc.richtext.model.StyleSpansBuilder + +class JavaSyntaxHighlighter : SyntaxHighlighter { + override fun highlight(code: String): StyleSpans> = StyleSpansBuilder>().let { + val lastKeywordEnd = PATTERN.findAll(code).fold(0) { lastKeywordEnd, result -> + val styleClass = when { + result.groups["KEYWORD"] != null -> "keyword" + result.groups["PAREN"] != null -> "paren" + result.groups["BRACE"] != null -> "brace" + result.groups["BRACKET"] != null -> "bracket" + result.groups["SEMICOLON"] != null -> "semicolon" + result.groups["STRING"] != null -> "string" + result.groups["COMMENT"] != null -> "comment" + else -> throw IllegalStateException("Unsupported regex group") + } + + it.add(emptyList(), result.range.first - lastKeywordEnd) + it.add(listOf(styleClass), result.range.last - result.range.first + 1) + + result.range.last + 1 + } + + it.add(emptyList(), code.length - lastKeywordEnd) + + it.create() + } + + companion object { + private val KEYWORDS = arrayOf( + "abstract", "assert", "boolean", "break", "byte", + "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", + "enum", "extends", "final", "finally", "float", + "for", "goto", "if", "implements", "import", + "instanceof", "int", "interface", "long", "native", + "new", "package", "private", "protected", "public", + "return", "short", "static", "strictfp", "super", + "switch", "synchronized", "this", "throw", "throws", + "transient", "try", "void", "volatile", "while" + ) + + private val KEYWORD_PATTERN = "\\b(" + KEYWORDS.joinToString("|") + ")\\b" + private val PAREN_PATTERN = "\\(|\\)" + private val BRACE_PATTERN = "\\{|\\}" + private val BRACKET_PATTERN = "\\[|\\]" + private val SEMICOLON_PATTERN = "\\;" + private val STRING_PATTERN = "\"([^\"\\\\]|\\\\.)*\"" + private val COMMENT_PATTERN = """ + //[^ + ]*|/\*(.|\R)*?\*/ + """.trimIndent() + + private val PATTERN = ( + "(?" + KEYWORD_PATTERN + ")" + + "|(?" + PAREN_PATTERN + ")" + + "|(?" + BRACE_PATTERN + ")" + + "|(?" + BRACKET_PATTERN + ")" + + "|(?" + SEMICOLON_PATTERN + ")" + + "|(?" + STRING_PATTERN + ")" + + "|(?" + COMMENT_PATTERN + ")" + ).toRegex() + } +} \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/highlighting/SyntaxHighlighter.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/highlighting/SyntaxHighlighter.kt new file mode 100644 index 00000000..d26cb286 --- /dev/null +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/highlighting/SyntaxHighlighter.kt @@ -0,0 +1,7 @@ +package com.bartlomiejpluta.base.editor.code.highlighting + +import org.fxmisc.richtext.model.StyleSpans + +interface SyntaxHighlighter { + fun highlight(code: String): StyleSpans> +} \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/stylesheet/HighlightingStylesheet.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/stylesheet/HighlightingStylesheet.kt new file mode 100644 index 00000000..094ff699 --- /dev/null +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/stylesheet/HighlightingStylesheet.kt @@ -0,0 +1,56 @@ +package com.bartlomiejpluta.base.editor.code.stylesheet + +import javafx.scene.text.FontWeight +import tornadofx.* + +class HighlightingStylesheet : Stylesheet() { + companion object { + val keyword by cssclass() + val semicolon by cssclass() + val paren by cssclass() + val bracket by cssclass() + val brace by cssclass() + val string by cssclass() + val comment by cssclass() + val paragraphBox by cssclass() + val hasCaret by csspseudoclass() + } + + init { + keyword { + fill = c("purple") + fontWeight = FontWeight.BOLD + } + + semicolon { + fontWeight = FontWeight.BOLD + } + + paren { + fill = c("firebrick") + fontWeight = FontWeight.BOLD + } + + bracket { + fill = c("darkgreen") + fontWeight = FontWeight.BOLD + } + + brace { + fill = c("teal") + fontWeight = FontWeight.BOLD + } + + string { + fill = c("blue") + } + + comment { + fill = c("cadetblue") + } + + paragraphBox and hasCaret { + backgroundColor = multi(c("#f2f9fc")) + } + } +} \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CodeEditorFragment.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CodeEditorFragment.kt index 07bff5aa..5a3a8385 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CodeEditorFragment.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CodeEditorFragment.kt @@ -1,9 +1,9 @@ package com.bartlomiejpluta.base.editor.code.view import tornadofx.Fragment -import tornadofx.hbox class CodeEditorFragment : Fragment() { + private val editorView = find() - override val root = hbox {} + override val root = editorView.root } \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CodeEditorView.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CodeEditorView.kt index f81858ae..4afc4f57 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CodeEditorView.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CodeEditorView.kt @@ -1,9 +1,15 @@ package com.bartlomiejpluta.base.editor.code.view +import com.bartlomiejpluta.base.editor.code.component.CodeEditor +import com.bartlomiejpluta.base.editor.code.viewmodel.CodeVM import tornadofx.View -import tornadofx.hbox +import tornadofx.borderpane class CodeEditorView : View() { + private val codeVM = find() + private val editor = CodeEditor(codeVM.codeProperty) - override val root = hbox {} + override val root = borderpane { + center = editor + } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 92b081bc..8cad45bc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,5 @@ guavaVersion=29.0-jre tornadoFxVersion=2.0.0-SNAPSHOT ikonliVersion=12.2.0 protobufPluginVersion=0.8.14 -protobufVersion=3.14.0 \ No newline at end of file +protobufVersion=3.14.0 +richtextfxVersion=0.10.5 \ No newline at end of file