├── .gitignore
├── .run
├── All Tests.run.xml
├── Run.run.xml
└── shadowJar.run.xml
├── LICENSE
├── ReadMe.md
├── app
├── build.gradle.kts
├── compiler
│ └── cli
│ │ └── cli-common
│ │ └── resources
│ │ └── META-INF
│ │ └── extensions
│ │ └── compiler.xml
├── main
│ └── me
│ │ └── darthorimar
│ │ └── rekot
│ │ ├── analysis
│ │ ├── CellAnalyzer.kt
│ │ ├── CellError.kt
│ │ ├── CellErrorComputer.kt
│ │ ├── CellErrors.kt
│ │ ├── CompiledCell.kt
│ │ ├── CompiledCellStorage.kt
│ │ ├── CompilerErrorInterceptor.kt
│ │ ├── SafeAnalysisRunner.kt
│ │ ├── SourceCell.kt
│ │ └── di.kt
│ │ ├── app
│ │ ├── App.kt
│ │ ├── AppComponent.kt
│ │ ├── AppState.kt
│ │ └── di.kt
│ │ ├── args
│ │ ├── ArgsCommand.kt
│ │ ├── ArgsCommandsParser.kt
│ │ └── ArgsSecondaryCommandExecutor.kt
│ │ ├── cells
│ │ ├── Cell.kt
│ │ ├── CellId.kt
│ │ ├── Cells.kt
│ │ └── di.kt
│ │ ├── completion
│ │ ├── Completion.kt
│ │ ├── CompletionItem.kt
│ │ ├── CompletionItemFactory.kt
│ │ ├── CompletionItemSorter.kt
│ │ ├── CompletionItemsCollector.kt
│ │ ├── CompletionPopup.kt
│ │ ├── CompletionPopupFactory.kt
│ │ ├── CompletionPopupRenderer.kt
│ │ ├── CompletionSession.kt
│ │ ├── completionUtils.kt
│ │ └── di.kt
│ │ ├── cursor
│ │ ├── CursorModifier.kt
│ │ └── CursorModifierImpl.kt
│ │ ├── editor
│ │ ├── Editor.kt
│ │ ├── FancyEditorCommands.kt
│ │ ├── di.kt
│ │ ├── handlers.kt
│ │ ├── renderer
│ │ │ └── CellViewRenderer.kt
│ │ └── view
│ │ │ ├── CellViewBuilder.kt
│ │ │ ├── CodeViewGenerator.kt
│ │ │ ├── EditorView.kt
│ │ │ ├── EditorViewProvider.kt
│ │ │ ├── ErrorViewGenerator.kt
│ │ │ ├── ResultViewGenerator.kt
│ │ │ ├── SoutViewGenerator.kt
│ │ │ └── di.kt
│ │ ├── errors
│ │ ├── CellErrorProvider.kt
│ │ └── di.kt
│ │ ├── events
│ │ ├── Event.kt
│ │ ├── EventListener.kt
│ │ ├── EventQueue.kt
│ │ ├── KeyboardEventProcessor.kt
│ │ └── di.kt
│ │ ├── execution
│ │ ├── CellExecutionState.kt
│ │ ├── CellExecutionStateProvider.kt
│ │ ├── CellExecutor.kt
│ │ ├── CellsClassLoader.kt
│ │ ├── CompiledFile.kt
│ │ ├── ConsoleInterceptor.kt
│ │ ├── ExecutingThread.kt
│ │ ├── ExecutionResult.kt
│ │ ├── ExecutorValueRenderer.kt
│ │ └── di.kt
│ │ ├── help
│ │ ├── HelpRenderer.kt
│ │ ├── HelpWindow.kt
│ │ └── di.kt
│ │ ├── logging
│ │ ├── AppThrowableRenderer.kt
│ │ └── logging.kt
│ │ ├── main.kt
│ │ ├── projectStructure
│ │ ├── BinariesDeclarationProviderFactory.kt
│ │ ├── Builtins.kt
│ │ ├── CellScriptDefinitionProvider.kt
│ │ ├── EssentialLibrary.kt
│ │ ├── KaCellScriptModule.kt
│ │ ├── ProjectDeclarationFactoryImpl.kt
│ │ ├── ProjectEssentialLibraries.kt
│ │ ├── ProjectStructure.kt
│ │ ├── ProjectStructureInitiator.kt
│ │ ├── ProjectStructureProviderImpl.kt
│ │ ├── di.kt
│ │ └── projectStructureInternalUtils.kt
│ │ ├── psi
│ │ ├── CellPsiUtils.kt
│ │ ├── di.kt
│ │ └── psiUtills.kt
│ │ ├── screen
│ │ ├── HackyMacBugFix.kt
│ │ ├── KeyboardInputPoller.kt
│ │ ├── ScreenController.kt
│ │ ├── ScreenControllerLanternaImpl.kt
│ │ └── di.kt
│ │ ├── style
│ │ ├── Color.kt
│ │ ├── SimpleStyledLineRenderer.kt
│ │ ├── Style.kt
│ │ ├── StyleModifier.kt
│ │ ├── StyleRenderer.kt
│ │ ├── StyledLine.kt
│ │ ├── StyledText.kt
│ │ ├── Styles.kt
│ │ └── di.kt
│ │ ├── updates
│ │ └── UpdatesChecker.kt
│ │ └── util
│ │ ├── Scroller.kt
│ │ ├── collectionUtils.kt
│ │ ├── functions.kt
│ │ └── result.kt
└── test
│ └── me
│ └── darthorimar
│ └── rekot
│ ├── ProjectConfig.kt
│ ├── cases
│ ├── CompletionTest.kt
│ ├── EditorTest.kt
│ └── ExecutorTest.kt
│ ├── infra
│ ├── AppTest.kt
│ └── CellExecuting.kt
│ └── mocks
│ ├── TestConfigFactory.kt
│ ├── TestScreenController.kt
│ └── di.kt
├── build.gradle.kts
├── config
├── build.gradle.kts
└── main
│ └── me
│ └── darthorimar
│ └── rekot
│ └── config
│ ├── AppConfig.kt
│ ├── ConfigFactory.kt
│ ├── appConsts.kt
│ ├── persistent
│ ├── DefaultConfigFactory.kt
│ ├── PersistentConfig.kt
│ └── PersistentConfigFactory.kt
│ └── stdlib
│ └── KotlinStdLibDownloader.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── images
├── completion.png
├── errors.png
├── multi_cell.png
└── multi_line.png
├── install.sh
├── logo.svg
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build
3 | !gradle/wrapper/gradle-wrapper.jar
4 |
5 | .idea
6 | .kotlin
7 | tmp
8 |
9 |
10 | app/proguard
11 |
12 | .DS_Store
--------------------------------------------------------------------------------
/.run/All Tests.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
16 |
17 |
18 | false
19 | true
20 | false
21 | true
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.run/Run.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.run/shadowJar.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/ReadMe.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ReKot
5 |
6 | ##### Kotlin REPL with an IDE-like experience in your terminal
7 |
8 | [](https://github.com/darthorimar/rekot/releases/latest)
9 |
10 | ## ⚙️ Installation
11 |
12 | ### Homebrew (macOS)
13 |
14 | ```bash
15 | brew install darthorimar/tap/rekot
16 | ```
17 |
18 | ### Manual Installation (macOS/Linux)
19 | _Make sure you have the JDK installed. JDK 17 is recommended._
20 |
21 | Paste this into the terminal:
22 |
23 | ```bash
24 | bash <(curl -s https://raw.githubusercontent.com/darthorimar/rekot/master/install.sh)
25 | ```
26 |
27 | Or if you prefer wget:
28 |
29 | ```bash
30 | bash <(wget -qO- https://raw.githubusercontent.com/darthorimar/rekot/master/install.sh)
31 | ```
32 |
33 | ## ✨ Features
34 |
35 | ### Multiline Code Editing
36 |
37 | A full-fledged multiline code editor with code highlighting
38 |
39 |
40 | ### Multiple Cells
41 |
42 | With results that can be reused between the cells
43 |
44 |
45 | ### Code Completion
46 |
47 |
48 |
49 | ### In-editor Code Highlighting
50 |
51 |
52 |
53 | ## 🧪 Compatibility
54 | * Tested on _macOS Sequoia 15.2_ on _iTerm2_ and _Terminal.app_
55 | * Not tested on _Linux_ or _Windows_
56 |
57 | ## ⚠️ Known Problems
58 |
59 | On macOS Sequoia version < 15.4.1, some text may be printed on the terminal like:
60 | ```
61 | 2025-01-28 23:39:24.855 java[2091:30776] +[IMKClient subclass]: chose IMKClient_Modern
62 | 2025-01-28 23:39:24.855 java[2091:30776] +[IMKInputSession subclass]: chose IMKInputSession_Modern
63 | ```
64 |
65 | See https://discussions.apple.com/thread/255761734
66 |
67 | As a workaround:
68 | - On Mac systems, ReKot occasionally fully refreshes the screen at some interval.
69 | - You can press `Ctrl+R` to manually refresh the screen.
70 |
71 | Starting from macOS 15.4.1, this issue seems to be fixed.
72 | So the hack with automatic screen refreshing is disabled by default.
73 | Can be enabled back by setting `hackyMacFix=true` in the `config.properties` file.
74 |
75 | ## 🛠️ Building/Developing ReKot
76 |
77 | Currently, ReKot depends on the [Kotlin Analysis API](https://kotlin.github.io/analysis-api) with a few patches on top. These patches are in my fork of the Kotlin repository, in the branch `rekot`: https://github.com/darthorimar/kotlin/tree/rekot.
78 |
79 | To start developing/building ReKot:
80 |
81 | 1. Clone the Kotlin on the branch `rekot` repository from https://github.com/darthorimar/kotlin/tree/rekot.
82 | 2. Run `./gradlew installIdeArtifacts -Ppublish.ide.plugin.dependencies=true` in the cloned repository. This will install the Analysis API to your Maven Local.
83 | 3. Now you can start working in the ReKot repository, and it can be imported into IntelliJ IDEA.
84 | 4. Gradle tasks:
85 | - `:app:buildProd` - This will create a release (a shadow jar) in `app/build/libs/rekot-VERSION.jar`.
86 | - `:app:run` - Run the app in the Swing-based terminal emulator, which sometimes can look quite blurry, but it's useful for debugging.
87 |
--------------------------------------------------------------------------------
/app/compiler/cli/cli-common/resources/META-INF/extensions/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 | org.jetbrains.kotlin
3 |
4 |
5 |
8 |
11 |
14 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/analysis/CellAnalyzer.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.analysis
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.cells.Cell
5 | import me.darthorimar.rekot.cursor.Cursor
6 | import me.darthorimar.rekot.projectStructure.KaCellScriptModule
7 | import me.darthorimar.rekot.projectStructure.ProjectStructure
8 | import org.jetbrains.kotlin.psi.KtFile
9 | import org.jetbrains.kotlin.psi.KtPsiFactory
10 | import org.jetbrains.kotlin.util.suffixIfNot
11 | import org.koin.core.component.inject
12 | import java.util.concurrent.ConcurrentHashMap
13 |
14 | class CellAnalyzer : AppComponent {
15 | private val projectStructure: ProjectStructure by inject()
16 | private val compiledCellStorage: CompiledCellStorage by inject()
17 |
18 | private val contexts = ConcurrentHashMap()
19 |
20 | fun getAllCells() = buildList { addAll(contexts.values) }
21 |
22 | fun inAnalysisContext(
23 | cell: Cell,
24 | text: String = cell.text,
25 | cursor: Cursor? = null,
26 | kind: CellContextKind = CellContextKind.Analysis,
27 | action: CellAnalysisContext.() -> R,
28 | ): R {
29 | return inAnalysisContext(cell.id.toString(), text, cursor, kind, action)
30 | }
31 |
32 | fun inAnalysisContext(
33 | tag: String,
34 | text: String,
35 | cursor: Cursor?,
36 | kind: CellContextKind = CellContextKind.Analysis,
37 | action: CellAnalysisContext.() -> R,
38 | ): R {
39 | val context = createContext(tag, text, cursor, kind)
40 | val id = ID()
41 | contexts[id] = context
42 | try {
43 | return action(context)
44 | } finally {
45 | contexts.remove(id)
46 | }
47 | }
48 |
49 | private fun createContext(tag: String, text: String, cursor: Cursor?, kind: CellContextKind): CellAnalysisContext {
50 | val fileName =
51 | when (kind) {
52 | is CellContextKind.Analysis -> "CellForAnalysis$tag"
53 | is CellContextKind.Execution -> "CellForExecution${kind.executionNumber}"
54 | }
55 | val ktFile = createKtFile(text, fileName)
56 | val kaModule =
57 | createKaCellScriptModule(
58 | ktFile,
59 | resultVariableIndex = (kind as? CellContextKind.Execution)?.resultVariableIndex,
60 | )
61 | return CellAnalysisContext(ktFile, kaModule, cursor)
62 | }
63 |
64 | private fun createKaCellScriptModule(ktFile: KtFile, resultVariableIndex: Int?): KaCellScriptModule {
65 | val allCompiledCells = compiledCellStorage.allCompiledCells()
66 | val binaryDependencies = buildList {
67 | addAll(projectStructure.essentialLibraries.kaModules)
68 | allCompiledCells.mapTo(this) { it.compiledLibraryModule }
69 | add(projectStructure.builtins.kaModule)
70 | }
71 | return KaCellScriptModule(
72 | ktFile,
73 | projectStructure.kotlinCoreProjectEnvironment.project,
74 | allCompiledCells.map { it.executionResult.scriptInstance },
75 | binaryDependencies,
76 | imports = allCompiledCells.flatMap { it.classifiersDeclared },
77 | resultVariableName = resultVariableIndex?.let { "res$it" },
78 | )
79 | .also { projectStructure.projectStructureProvider.setModule(ktFile, it) }
80 | }
81 |
82 | private fun createKtFile(text: String, fileName: String): KtFile {
83 | val factory = KtPsiFactory(projectStructure.project, eventSystemEnabled = true)
84 | return factory.createFile(fileName.suffixIfNot(".kts"), text)
85 | }
86 | }
87 |
88 | sealed interface CellContextKind {
89 | data object Analysis : CellContextKind
90 |
91 | data class Execution(val executionNumber: Int, val resultVariableIndex: Int) : CellContextKind
92 | }
93 |
94 | private class ID()
95 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/analysis/CellError.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.analysis
2 |
3 | class CellError(val lineNumber: Int, val colStart: Int, val colEnd: Int, val message: String)
4 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/analysis/CellErrorComputer.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.analysis
2 |
3 | import com.intellij.openapi.util.TextRange
4 | import com.intellij.psi.PsiErrorElement
5 | import me.darthorimar.rekot.app.AppComponent
6 | import org.jetbrains.kotlin.analysis.api.KaSession
7 | import org.jetbrains.kotlin.analysis.api.components.KaDiagnosticCheckerFilter
8 | import org.jetbrains.kotlin.analysis.api.diagnostics.KaDiagnosticWithPsi
9 | import org.jetbrains.kotlin.analysis.api.diagnostics.KaSeverity
10 | import org.jetbrains.kotlin.psi.KtFile
11 | import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
12 |
13 | class CellErrorComputer : AppComponent {
14 |
15 | context(CellAnalysisContext)
16 | fun computeErrors(): CellErrors {
17 | val errors = buildList {
18 | addAll(computeSyntaxErrors(ktFile))
19 | addAll(computeSemanticErrors(ktFile) ?: return CellErrors.EMPTY)
20 | }
21 | return CellErrors(errors.groupBy { it.lineNumber })
22 | }
23 |
24 | private fun computeSyntaxErrors(ktFile: KtFile): List {
25 | val syntaxErrors = ktFile.collectDescendantsOfType()
26 | return syntaxErrors.map { error -> error.toCellError(ktFile) }
27 | }
28 |
29 | /*
30 | * Returns null in a case of error during computation
31 | */
32 | context(CellAnalysisContext)
33 | private fun computeSemanticErrors(ktFile: KtFile): List? {
34 | return analyze {
35 | ktFile.collectDiagnostics(KaDiagnosticCheckerFilter.ONLY_COMMON_CHECKERS).mapNotNull { error ->
36 | toCellError(error, ktFile)
37 | }
38 | }
39 | }
40 |
41 | private fun PsiErrorElement.toCellError(ktFile: KtFile): CellError {
42 | val range = textRange.toEditorRange(ktFile)
43 | return CellError(
44 | lineNumber = range.lineNumber,
45 | colStart = range.colStart,
46 | colEnd = range.colEnd,
47 | message = errorDescription,
48 | )
49 | }
50 |
51 | context(KaSession)
52 | private fun toCellError(error: KaDiagnosticWithPsi<*>, ktFile: KtFile): CellError? {
53 | if (error.severity != KaSeverity.ERROR) return null
54 | val range = error.textRanges.firstOrNull()?.toEditorRange(ktFile) ?: error.psi.textRange.toEditorRange(ktFile)
55 | return CellError(
56 | lineNumber = range.lineNumber,
57 | colStart = range.colStart,
58 | colEnd = range.colEnd,
59 | message = error.defaultMessage,
60 | )
61 | }
62 |
63 | private fun TextRange.toEditorRange(psiFile: KtFile): Range {
64 | val document = psiFile.viewProvider.document
65 | val startLineNumber = document.getLineNumber(startOffset)
66 | val endLineNumber = document.getLineNumber(endOffset)
67 |
68 | return Range(
69 | lineNumber = startLineNumber,
70 | colStart = startOffset - document.getLineStartOffset(startLineNumber),
71 | colEnd =
72 | if (startLineNumber == endLineNumber) endOffset - document.getLineStartOffset(startLineNumber)
73 | else document.getLineEndOffset(startLineNumber),
74 | )
75 | }
76 |
77 | private class Range(val lineNumber: Int, val colStart: Int, val colEnd: Int)
78 | }
79 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/analysis/CellErrors.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.analysis
2 |
3 | class CellErrors(private val errorPerLine: Map>) {
4 | fun byLine(line: Int): List = errorPerLine[line] ?: emptyList()
5 |
6 | val allErrors: List
7 | get() = errorPerLine.values.flatten()
8 |
9 | companion object {
10 | val EMPTY = CellErrors(emptyMap())
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/analysis/CompiledCell.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.analysis
2 |
3 | import me.darthorimar.rekot.execution.ExecutionResult
4 | import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderFactory
5 | import org.jetbrains.kotlin.analysis.api.projectStructure.KaLibraryModule
6 |
7 | class CompiledCell(
8 | val compiledLibraryModule: KaLibraryModule,
9 | val compiledCellProviderFactory: KotlinDeclarationProviderFactory,
10 | val executionResult: ExecutionResult,
11 | val classifiersDeclared: List,
12 | )
13 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/analysis/CompiledCellStorage.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.analysis
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.app.SubscriptionContext
5 | import me.darthorimar.rekot.app.subscribe
6 | import me.darthorimar.rekot.cells.Cell
7 | import me.darthorimar.rekot.cells.CellId
8 | import me.darthorimar.rekot.cells.Cells
9 | import me.darthorimar.rekot.events.Event
10 | import me.darthorimar.rekot.execution.CellExecutionState
11 | import me.darthorimar.rekot.execution.ExecutionResult
12 | import me.darthorimar.rekot.projectStructure.ProjectStructure
13 | import me.darthorimar.rekot.projectStructure.createLibraryModule
14 | import me.darthorimar.rekot.psi.CellPsiUtils
15 | import org.jetbrains.kotlin.analysis.api.standalone.base.declarations.KotlinStandaloneDeclarationProviderFactory
16 | import org.jetbrains.kotlin.psi.KtClassLikeDeclaration
17 | import org.koin.core.component.inject
18 |
19 | class CompiledCellStorage : AppComponent {
20 | private val projectStructure: ProjectStructure by inject()
21 | private val compiledCells = mutableListOf()
22 | private val psiFactory by inject()
23 | private val cells: Cells by inject()
24 |
25 | context(SubscriptionContext)
26 | override fun performSubscriptions() {
27 | subscribe { event ->
28 | val state = event.state as? CellExecutionState.Executed ?: return@subscribe
29 | updaterCompiledAnalyzableCell(event.cellId, state.result)
30 | }
31 | }
32 |
33 | private fun updaterCompiledAnalyzableCell(cellId: CellId, executedCell: ExecutionResult) {
34 | val kaLibraryModule =
35 | createLibraryModule(
36 | listOf(executedCell.classRoot),
37 | projectStructure.kotlinCoreProjectEnvironment,
38 | "CompiledCell#${cellId}, $executedCell",
39 | isSdk = false,
40 | )
41 | for (virtualFile in kaLibraryModule.virtualFiles) {
42 | projectStructure.projectStructureProvider.setModule(virtualFile, kaLibraryModule)
43 | }
44 |
45 | val compiledCellProviderFactory =
46 | KotlinStandaloneDeclarationProviderFactory(
47 | projectStructure.kotlinCoreProjectEnvironment.project,
48 | projectStructure.kotlinCoreProjectEnvironment.environment,
49 | sourceKtFiles = emptyList(),
50 | binaryRoots = kaLibraryModule.virtualFiles,
51 | shouldBuildStubsForBinaryLibraries = true,
52 | skipBuiltins = true,
53 | )
54 |
55 | compiledCells +=
56 | CompiledCell(
57 | kaLibraryModule,
58 | compiledCellProviderFactory,
59 | executedCell,
60 | classifiersDeclared = getAllDefinedClassifiersImportableNames(cells.getCell(cellId), executedCell),
61 | )
62 | }
63 |
64 | private fun getAllDefinedClassifiersImportableNames(cell: Cell, executedCell: ExecutionResult): List {
65 | val result = mutableListOf()
66 | val file = psiFactory.createKtFile(cell, "Cell for Fq Names${cell.id}")
67 | for (declaration in file.script!!.declarations) {
68 | if (declaration is KtClassLikeDeclaration) {
69 | result += executedCell.scriptClassFqName + "." + declaration.name
70 | }
71 | }
72 | return result
73 | }
74 |
75 | fun allCompiledCells(): List {
76 | return compiledCells.reversed()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/analysis/CompilerErrorInterceptor.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.analysis
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.logging.error
5 | import me.darthorimar.rekot.logging.logger
6 |
7 | private val loggerLoggerErrorInterceptor = logger()
8 |
9 | interface CompilerErrorInterceptor : AppComponent {
10 | fun intercept(error: Throwable)
11 | }
12 |
13 | class LoggingErrorInterceptor : CompilerErrorInterceptor {
14 | override fun intercept(error: Throwable) {
15 | loggerLoggerErrorInterceptor.error("Compiler error", error)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/analysis/SafeAnalysisRunner.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.analysis
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import org.jetbrains.kotlin.util.PrivateForInline
5 | import org.koin.core.component.inject
6 |
7 | class SafeAnalysisRunner : AppComponent {
8 | @PrivateForInline val interceptor: CompilerErrorInterceptor by inject()
9 |
10 | @OptIn(PrivateForInline::class)
11 | inline fun runSafely(defaultValue: R? = null, action: () -> R): R? {
12 | // sometimes Analysis API may fail during analysis and this may break the whole App
13 | return runCatching { action() }.onFailure { interceptor.intercept(it) }.getOrElse { defaultValue }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/analysis/SourceCell.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.analysis
2 |
3 | import com.intellij.psi.PsiElement
4 | import me.darthorimar.rekot.cursor.Cursor
5 | import me.darthorimar.rekot.projectStructure.KaCellScriptModule
6 | import me.darthorimar.rekot.psi.document
7 | import org.jetbrains.kotlin.analysis.api.KaSession
8 | import org.jetbrains.kotlin.analysis.api.analyze
9 | import org.jetbrains.kotlin.psi.KtFile
10 | import org.jetbrains.kotlin.util.PrivateForInline
11 | import org.koin.core.component.KoinComponent
12 | import org.koin.core.component.inject
13 |
14 | @OptIn(PrivateForInline::class)
15 | class CellAnalysisContext(val ktFile: KtFile, val cellKtModule: KaCellScriptModule, val analysisCursor: Cursor?) :
16 | KoinComponent {
17 | @PrivateForInline val safeAnalysisRunner: SafeAnalysisRunner by inject()
18 | @PrivateForInline val cellErrorComputer: CellErrorComputer by inject()
19 |
20 | val errors by lazy { cellErrorComputer.computeErrors() }
21 |
22 | fun getPsiElementAtCursor(): PsiElement? {
23 | val cursor = analysisCursor ?: error("Cursor is not available in this context")
24 | val offset = ktFile.document.getLineStartOffset(cursor.row) + cursor.column
25 | return ktFile.findElementAt(offset - 1)
26 | }
27 |
28 | fun PsiElement.getLineNumber(): Int {
29 | return ktFile.document.getLineNumber(textRange.startOffset)
30 | }
31 |
32 | /** Returns null in a case of exception during computation */
33 | inline fun analyze(action: KaSession.() -> R): R? {
34 | return safeAnalysisRunner.runSafely { analyze(ktFile) { action() } }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/analysis/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.analysis
2 |
3 | import org.koin.dsl.module
4 |
5 | val analysisModule = module {
6 | single { CellErrorComputer() }
7 | single { CellAnalyzer() }
8 | single { CompiledCellStorage() }
9 | single { SafeAnalysisRunner() }
10 | }
11 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/app/App.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.app
2 |
3 | import me.darthorimar.rekot.cells.Cells
4 | import me.darthorimar.rekot.completion.CompletionPopupRenderer
5 | import me.darthorimar.rekot.editor.Editor
6 | import me.darthorimar.rekot.editor.renderer.CellViewRenderer
7 | import me.darthorimar.rekot.events.Event
8 | import me.darthorimar.rekot.events.EventQueue
9 | import me.darthorimar.rekot.help.HelpRenderer
10 | import me.darthorimar.rekot.logging.error
11 | import me.darthorimar.rekot.logging.logger
12 | import me.darthorimar.rekot.screen.KeyboardInputPoller
13 | import me.darthorimar.rekot.screen.ScreenController
14 | import org.koin.core.component.KoinComponent
15 | import org.koin.core.component.inject
16 |
17 | private val logger = logger()
18 |
19 | class App : KoinComponent {
20 | private val cells: Cells by inject()
21 | private val editor: Editor by inject()
22 | private val screenController: ScreenController by inject()
23 | private val queue: EventQueue by inject()
24 | private val keyboardInputPoller: KeyboardInputPoller by inject()
25 |
26 | private val cellViewRenderer: CellViewRenderer by inject()
27 | private val completionPopupRenderer: CompletionPopupRenderer by inject()
28 | private val helpRenderer: HelpRenderer by inject()
29 |
30 | private val appState: AppState by inject()
31 |
32 | fun runApp() {
33 | try {
34 | doRun()
35 | } catch (e: Exception) {
36 | logger.error("Error during app execution", e)
37 | throw e
38 | }
39 | }
40 |
41 | private fun doRun() {
42 | queue.fire(Event.AppStarted)
43 | editor.navigateToCell(cells.newCell())
44 |
45 | keyboardInputPoller.startPolling()
46 |
47 | val appState = appState
48 | while (appState.active) {
49 | queue.processAllBlocking()
50 | render()
51 | }
52 | }
53 |
54 | private fun render() {
55 | cellViewRenderer.render()
56 | completionPopupRenderer.render()
57 | helpRenderer.render()
58 | screenController.refresh()
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/app/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.app
2 |
3 | import me.darthorimar.rekot.events.Event
4 | import me.darthorimar.rekot.events.EventListener
5 | import me.darthorimar.rekot.events.EventQueue
6 | import org.koin.core.annotation.KoinInternalApi
7 | import org.koin.core.component.KoinComponent
8 | import org.koin.core.component.get
9 | import org.koin.core.context.GlobalContext
10 | import org.koin.core.definition.Kind
11 | import kotlin.reflect.full.isSubclassOf
12 |
13 | interface AppComponent : KoinComponent {
14 | context(SubscriptionContext)
15 | fun performSubscriptions() {}
16 |
17 | fun fireEvent(event: Event) {
18 | get().fire(event)
19 | }
20 |
21 | companion object {
22 | fun performSubscriptions() {
23 | val context = object : SubscriptionContext {}
24 | for (subscriptable in allComponents()) {
25 | with(context) { subscriptable.performSubscriptions() }
26 | }
27 | }
28 |
29 | @OptIn(KoinInternalApi::class)
30 | private fun allComponents(): List {
31 | val koin = GlobalContext.get()
32 | return koin.instanceRegistry.instances
33 | .map { it.value.beanDefinition }
34 | .filter { it.kind == Kind.Singleton }
35 | .filter { it.primaryType.isSubclassOf(AppComponent::class) }
36 | .map { koin.get(clazz = it.primaryType, qualifier = null, parameters = null) }
37 | }
38 | }
39 | }
40 |
41 | interface SubscriptionContext : KoinComponent
42 |
43 | inline fun SubscriptionContext.subscribe(listener: EventListener) {
44 | get().subscribe(listener)
45 | }
46 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/app/AppState.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.app
2 |
3 | import me.darthorimar.rekot.events.Event
4 |
5 | class AppState : AppComponent {
6 | private var _active: Boolean = true
7 |
8 | context(SubscriptionContext)
9 | override fun performSubscriptions() {
10 | subscribe { _active = false }
11 | }
12 |
13 | val active: Boolean
14 | get() = _active
15 | }
16 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/app/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.app
2 |
3 | import me.darthorimar.rekot.analysis.analysisModule
4 | import me.darthorimar.rekot.cells.cellsModule
5 | import me.darthorimar.rekot.completion.completionModule
6 | import me.darthorimar.rekot.editor.editorModule
7 | import me.darthorimar.rekot.errors.errorsModule
8 | import me.darthorimar.rekot.events.eventModule
9 | import me.darthorimar.rekot.execution.executionModule
10 | import me.darthorimar.rekot.help.helpModule
11 | import me.darthorimar.rekot.projectStructure.projectStructureModule
12 | import me.darthorimar.rekot.psi.psiModule
13 | import me.darthorimar.rekot.style.styleModule
14 | import org.koin.dsl.module
15 |
16 | val appModule = module {
17 | includes(
18 | eventModule,
19 | completionModule,
20 | psiModule,
21 | cellsModule,
22 | editorModule,
23 | analysisModule,
24 | styleModule,
25 | projectStructureModule,
26 | executionModule,
27 | errorsModule,
28 | helpModule,
29 | )
30 | single { AppState() }
31 | }
32 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/args/ArgsCommand.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.args
2 |
3 | @Suppress("ClassName")
4 | sealed interface ArgsCommand {
5 | sealed interface Secondary : ArgsCommand {
6 | data object VERSION : Secondary
7 |
8 | data object PRINT_CONFIG : Secondary
9 |
10 | data object HELP : Secondary
11 | }
12 |
13 | data object RUN_APP : ArgsCommand
14 | }
15 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/args/ArgsCommandsParser.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.args
2 |
3 | import me.darthorimar.rekot.config.APP_NAME
4 |
5 | object ArgsCommandsParser {
6 | fun parse(args: Array): ArgsCommand {
7 | return when (args.size) {
8 | 0 -> ArgsCommand.RUN_APP
9 | 1 -> when (val arg = args.single()) {
10 | "--version", "-version", "version" -> ArgsCommand.Secondary.VERSION
11 | "--help", "-help", "help" -> ArgsCommand.Secondary.HELP
12 | "--app-dir" -> ArgsCommand.Secondary.PRINT_CONFIG
13 | else -> error("Unknown arguments `$arg`, use `--help` for supported commands")
14 | }
15 | else -> error("Too many arguments, use `--help` for supported commands")
16 | }
17 | }
18 |
19 | fun help(): String {
20 | return """
21 | | --version - print $APP_NAME version
22 | | --app-dir - print directory with the $APP_NAME configuration
23 | | --help - print this message
24 | """.trimMargin()
25 | }
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/args/ArgsSecondaryCommandExecutor.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.args
2 |
3 | import me.darthorimar.rekot.config.APP_VERSION
4 | import me.darthorimar.rekot.config.ConfigFactory
5 | import kotlin.io.path.absolutePathString
6 |
7 | object ArgsSecondaryCommandExecutor {
8 | fun execute(command: ArgsCommand.Secondary) {
9 | when (command) {
10 | ArgsCommand.Secondary.HELP -> {
11 | println(ArgsCommandsParser.help())
12 | }
13 | ArgsCommand.Secondary.PRINT_CONFIG -> {
14 | println(ConfigFactory.getDefaultAppDirectory().absolutePathString())
15 | }
16 | ArgsCommand.Secondary.VERSION -> {
17 | println(APP_VERSION)
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/cells/CellId.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.cells
2 |
3 | class CellId(private val tag: Int) : Comparable {
4 | val isInitialCell: Boolean
5 | get() = tag == 1
6 |
7 | override fun compareTo(other: CellId): Int {
8 | return tag.compareTo(other.tag)
9 | }
10 |
11 | override fun toString(): String {
12 | return tag.toString()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/cells/Cells.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.cells
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.events.Event
5 |
6 | class Cells : AppComponent {
7 | private val _cells = mutableListOf()
8 | private val _cellsById = mutableMapOf()
9 |
10 | private var freeCellId = 1
11 |
12 | val cells: List
13 | get() = _cells
14 |
15 | fun newCell(): Cell {
16 | val newCellId = CellId(freeCellId)
17 | val cell = Cell(newCellId)
18 | freeCellId++
19 | _cells += cell
20 | _cellsById[newCellId] = cell
21 | fireEvent(Event.CellTextChanged(newCellId))
22 | return cell
23 | }
24 |
25 | fun getCell(cellId: CellId): Cell {
26 | return _cellsById.getValue(cellId)
27 | }
28 |
29 | fun previousCell(cell: Cell): Cell {
30 | require(cells.size > 1)
31 | val prevCellIndex =
32 | when (val index = _cells.indexOf(cell)) {
33 | 0 -> 1
34 | else -> index - 1
35 | }
36 | return _cells[prevCellIndex]
37 | }
38 |
39 | fun deleteCell(cell: Cell) {
40 | _cells.remove(cell)
41 | _cellsById.remove(cell.id)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/cells/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.cells
2 |
3 | import org.koin.dsl.module
4 |
5 | val cellsModule = module { single { Cells() } }
6 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/completion/Completion.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.completion
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.app.SubscriptionContext
5 | import me.darthorimar.rekot.app.subscribe
6 | import me.darthorimar.rekot.cells.CellId
7 | import me.darthorimar.rekot.cells.Cells
8 | import me.darthorimar.rekot.events.Event
9 | import org.koin.core.component.inject
10 | import java.util.concurrent.Executors
11 |
12 | class Completion : AppComponent {
13 | private val popupFactory: CompletionPopupFactory by inject()
14 | private val cells: Cells by inject()
15 |
16 | private val executor = Executors.newSingleThreadExecutor()
17 | private var _popup: CompletionPopup? = null
18 | private var currentSession: CompletionSession? = null
19 |
20 | context(SubscriptionContext)
21 | override fun performSubscriptions() {
22 | subscribe { e -> }
23 |
24 | subscribe { showPopup(it.popup) }
25 | subscribe { e ->
26 | currentSession?.stop()
27 |
28 | if (shouldShowCompletionPopupAfterTyping(e.char)) {
29 | val popup = _popup
30 | if (popup != null) {
31 | if (!popup.addPrefix(e.char)) {
32 | closePopup()
33 | }
34 | } else {
35 | scheduleShowingCompletionPopup(e.cellId)
36 | }
37 | } else {
38 | closePopup()
39 | }
40 | }
41 | }
42 |
43 | private fun scheduleShowingCompletionPopup(cellId: CellId) {
44 | val cell = cells.getCell(cellId)
45 | val cellText = cell.text
46 | val cellIndex = cell.id
47 | val cellCursor = cell.cursor
48 |
49 | val newSession = CompletionSession()
50 | currentSession = newSession
51 |
52 | executor.execute {
53 | val popup =
54 | try {
55 | popupFactory.createPopup(cellIndex, cellText, cellCursor, newSession) ?: return@execute
56 | } catch (e: InterruptedCompletionException) {
57 | return@execute
58 | }
59 | fireEvent(Event.ShowPopup(popup))
60 | }
61 | }
62 |
63 | private fun shouldShowCompletionPopupAfterTyping(char: Char): Boolean {
64 | return char.isLetterOrDigit() || char == '.'
65 | }
66 |
67 | val popup: CompletionPopup
68 | get() = _popup ?: error("Popup is not shown")
69 |
70 | val popupShown: Boolean
71 | get() = _popup != null
72 |
73 | fun choseItem() {
74 | ensurePopupShown()
75 | popup.choseItem()
76 | closePopup()
77 | }
78 |
79 | fun choseItemOnDot(): Boolean {
80 | ensurePopupShown()
81 | if (popup.choseItemOnDot()) {
82 | closePopup()
83 | return true
84 | }
85 | return false
86 | }
87 |
88 | fun closePopup() {
89 | _popup = null
90 | }
91 |
92 | fun showPopup(popup: CompletionPopup) {
93 | _popup = popup
94 | }
95 |
96 | fun up() {
97 | ensurePopupShown()
98 | popup.up()
99 | }
100 |
101 | fun down() {
102 | ensurePopupShown()
103 | popup.down()
104 | }
105 |
106 | private fun ensurePopupShown() {
107 | check(_popup != null) { "Popup is not shown" }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/completion/CompletionItem.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.completion
2 |
3 | import me.darthorimar.rekot.completion.CompletionItem.Declaration
4 | import me.darthorimar.rekot.completion.CompletionItem.Keyword
5 | import org.jetbrains.kotlin.analysis.api.symbols.KaSymbolLocation
6 | import org.jetbrains.kotlin.lexer.KtSingleValueToken
7 |
8 | sealed interface CompletionItem {
9 | val show: String
10 | val insert: String
11 | val tag: CompletionItemTag
12 | val moveCaret: Int
13 | val name: String
14 |
15 | data class Declaration(
16 | override val show: String,
17 | override val insert: String,
18 | override val name: String,
19 | override val tag: CompletionItemTag,
20 | override val moveCaret: Int = 0,
21 | val import: Boolean = false,
22 | val location: KaSymbolLocation,
23 | val fqName: String?,
24 | ) : CompletionItem {
25 | override fun toString(): String {
26 | return """|{
27 | | show: $show,
28 | | insert: $insert,
29 | | tag: $tag,
30 | | moveCaret: $moveCaret,
31 | | import: $import,
32 | | location: $location,
33 | | fqName: $fqName
34 | |}"""
35 | .trimMargin()
36 | }
37 | }
38 |
39 | data class Keyword(
40 | override val show: String,
41 | override val insert: String = show,
42 | override val name: String,
43 | override val moveCaret: Int = 0,
44 | ) : CompletionItem {
45 | override val tag: CompletionItemTag
46 | get() = CompletionItemTag.KEYWORD
47 |
48 | override fun toString(): String {
49 | return """|{
50 | | show: $show,
51 | | insert: $insert,
52 | | moveCaret: $moveCaret,
53 | | tag: $tag
54 | |}"""
55 | .trimIndent()
56 | }
57 | }
58 | }
59 |
60 | class KeywordItemBuilder {
61 | lateinit var textToShow: String
62 | lateinit var textToInsert: String
63 | var moveCaret: Int = 0
64 | lateinit var name: String
65 |
66 | fun with(completionItem: Keyword) {
67 | textToShow = completionItem.show
68 | textToInsert = completionItem.insert
69 | moveCaret = completionItem.moveCaret
70 | name = completionItem.name
71 | }
72 |
73 | fun withSpace() {
74 | textToInsert += " "
75 | }
76 |
77 | fun build() = Keyword(textToShow, textToInsert, name, moveCaret)
78 | }
79 |
80 | class DeclarationCompletionItemBuilder {
81 | lateinit var show: String
82 | var insert: String? = null
83 | lateinit var tag: CompletionItemTag
84 | var moveCaret: Int = 0
85 | var import: Boolean = false
86 | lateinit var location: KaSymbolLocation
87 | var fqName: String? = null
88 | var name: String? = null
89 |
90 | fun withImport() {
91 | import = true
92 | }
93 |
94 | fun with(completionItem: Declaration) {
95 | show = completionItem.show
96 | insert = completionItem.insert
97 | tag = completionItem.tag
98 | moveCaret = completionItem.moveCaret
99 | import = completionItem.import
100 | location = completionItem.location
101 | fqName = completionItem.fqName
102 | name = completionItem.name
103 | }
104 |
105 | fun build() =
106 | Declaration(
107 | show = show,
108 | insert = insert ?: show,
109 | name = name ?: fqName?.substringAfterLast(".") ?: error("No name provided"),
110 | tag = tag,
111 | moveCaret = moveCaret,
112 | import = import,
113 | location = location,
114 | fqName = fqName,
115 | )
116 | }
117 |
118 | fun declarationCompletionItem(block: DeclarationCompletionItemBuilder.() -> Unit): Declaration {
119 | val builder = DeclarationCompletionItemBuilder().apply(block)
120 | return builder.build()
121 | }
122 |
123 | fun keywordCompletionItem(keyword: KtSingleValueToken, block: KeywordItemBuilder.() -> Unit = {}): Keyword {
124 | return keywordCompletionItem {
125 | textToShow = keyword.value
126 | textToInsert = keyword.value
127 | name = keyword.value
128 | apply(block)
129 | }
130 | }
131 |
132 | fun keywordCompletionItem(block: KeywordItemBuilder.() -> Unit = {}): Keyword {
133 | val builder = KeywordItemBuilder().apply { apply(block) }
134 | return builder.build()
135 | }
136 |
137 | enum class CompletionItemTag(val text: String) {
138 | FUNCTION("ⓕ"),
139 | PROPERTY("ⓟ"),
140 | CLASS("ⓒ"),
141 | LOCAL_VARIABLE("ⓥ"),
142 | KEYWORD("ⓚ"),
143 | }
144 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/completion/CompletionItemFactory.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.completion
2 |
3 | import me.darthorimar.rekot.analysis.SafeAnalysisRunner
4 | import me.darthorimar.rekot.app.AppComponent
5 | import org.jetbrains.kotlin.analysis.api.KaSession
6 | import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource
7 | import org.jetbrains.kotlin.analysis.api.signatures.KaCallableSignature
8 | import org.jetbrains.kotlin.analysis.api.signatures.KaFunctionSignature
9 | import org.jetbrains.kotlin.analysis.api.signatures.KaVariableSignature
10 | import org.jetbrains.kotlin.analysis.api.symbols.*
11 | import org.jetbrains.kotlin.builtins.functions.FunctionTypeKind
12 | import org.jetbrains.kotlin.types.Variance
13 | import org.koin.core.component.inject
14 |
15 | class CompletionItemFactory : AppComponent {
16 | private val safeAnalysisRunner: SafeAnalysisRunner by inject()
17 |
18 | context(KaSession)
19 | fun createCompletionItem(symbol: KaDeclarationSymbol): CompletionItem.Declaration? {
20 | return safeAnalysisRunner.runSafely {
21 | createCompletionItem(textToShow = renderSymbol(symbol) ?: return null, symbol = symbol)
22 | }
23 | }
24 |
25 | context(KaSession)
26 | fun createCompletionItem(signature: KaCallableSignature<*>): CompletionItem.Declaration? {
27 | val symbol = signature.symbol
28 | return safeAnalysisRunner.runSafely {
29 | createCompletionItem(textToShow = renderSignature(signature) ?: return null, symbol = symbol)
30 | }
31 | }
32 |
33 | context(KaSession)
34 | private fun createCompletionItem(textToShow: String, symbol: KaDeclarationSymbol): CompletionItem.Declaration? {
35 | if (textToShow.contains(COMPLETION_FAKE_IDENTIFIER)) return null
36 | val (textToInsert, offset) = getInsertionText(symbol) ?: return null
37 | return declarationCompletionItem {
38 | this.show = textToShow
39 | this.insert = textToInsert
40 | tag = getCompletionItemTag(symbol)
41 | moveCaret = offset
42 | location = symbol.location
43 | fqName = symbol.importableFqName?.asString()
44 | name = symbol.name?.asString() ?: ""
45 | }
46 | }
47 |
48 | private fun getCompletionItemTag(symbol: KaDeclarationSymbol): CompletionItemTag {
49 | return when (symbol) {
50 | is KaFunctionSymbol -> CompletionItemTag.FUNCTION
51 | is KaPropertySymbol -> CompletionItemTag.PROPERTY
52 | is KaClassLikeSymbol -> CompletionItemTag.CLASS
53 | else -> CompletionItemTag.LOCAL_VARIABLE
54 | }
55 | }
56 |
57 | context(KaSession)
58 | private fun getInsertionText(declaration: KaDeclarationSymbol): Pair? {
59 | return when {
60 | declaration is KaFunctionSymbol -> {
61 | val name = declaration.name?.asString() ?: return null
62 | when {
63 | declaration.hasSingleFunctionTypeParameter() -> "$name { }" to -3
64 |
65 | declaration.valueParameters.isNotEmpty() -> "$name()" to -1
66 | else -> "$name()" to 0
67 | }
68 | }
69 |
70 | else -> declaration.name?.asString()?.let { it to 0 }
71 | }
72 | }
73 |
74 | context(KaSession)
75 | private fun KaFunctionSymbol.hasSingleFunctionTypeParameter(): Boolean {
76 | val singleParameter = valueParameters.singleOrNull() ?: return false
77 | val kind = singleParameter.returnType.functionTypeKind ?: return false
78 | return kind == FunctionTypeKind.Function || kind == FunctionTypeKind.SuspendFunction
79 | }
80 |
81 | context(KaSession)
82 | private fun renderSymbol(symbol: KaSymbol): String? =
83 | when (symbol) {
84 | is KaFunctionSymbol -> renderSignature(symbol.asSignature())
85 | is KaVariableSymbol -> renderSignature(symbol.asSignature())
86 | is KaClassLikeSymbol -> renderClassLikeSymbol(symbol)
87 |
88 | else -> null
89 | }
90 |
91 | context(KaSession)
92 | private fun renderClassLikeSymbol(symbol: KaClassLikeSymbol): String? {
93 | val name = symbol.name?.asString() ?: return null
94 | val typeParameters = symbol.typeParameters
95 | val typeParametersText =
96 | if (typeParameters.isNotEmpty()) {
97 | typeParameters.joinToString(prefix = "<", postfix = ">") { it.name.asString() }
98 | } else ""
99 | return "$name$typeParametersText"
100 | }
101 |
102 | context(KaSession)
103 | private fun renderSignature(symbol: KaCallableSignature<*>): String? =
104 | when (symbol) {
105 | is KaFunctionSignature<*> ->
106 | buildString {
107 | append(symbol.symbol.name?.asString() ?: return null)
108 | append("(")
109 | symbol.valueParameters.forEachIndexed { i, p ->
110 | append(p.name.asString())
111 | append(": ")
112 | append(p.returnType.render(KaTypeRendererForSource.WITH_SHORT_NAMES, Variance.IN_VARIANCE))
113 | if (i != symbol.valueParameters.lastIndex) {
114 | append(", ")
115 | }
116 | }
117 | append("): ")
118 | append(symbol.returnType.render(KaTypeRendererForSource.WITH_SHORT_NAMES, Variance.OUT_VARIANCE))
119 | }
120 |
121 | is KaVariableSignature<*> ->
122 | buildString {
123 | append(symbol.name.asString())
124 | append(": ")
125 | append(symbol.returnType.render(KaTypeRendererForSource.WITH_SHORT_NAMES, Variance.OUT_VARIANCE))
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/completion/CompletionItemSorter.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.completion
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import org.apache.commons.text.similarity.FuzzyScore
5 | import java.util.*
6 |
7 | class CompletionItemSorter : AppComponent {
8 | fun sort(items: List, prefix: String): List {
9 | val fuzzy = FuzzyScore(Locale.US)
10 | return items.sortedByDescending { item ->
11 | val name = item.name
12 | when {
13 | name == prefix -> 100
14 | name.startsWith(prefix) -> 99
15 | name.startsWith(prefix, ignoreCase = true) -> 98
16 | name.contains(prefix) -> 97
17 | name.contains(prefix, ignoreCase = true) -> 96
18 | name.startsWith("res") -> 95
19 | else -> fuzzy.fuzzyScore(name, prefix)
20 | }
21 | }
22 | }
23 |
24 | private val declarationComparator =
25 | compareBy {
26 | // stdlib declarations first
27 | it.fqName?.startsWith("kotlin.") == true
28 | }
29 | }
30 |
31 | private inline fun comparatorFor(comparator: Comparator): Comparator = Comparator { a, b ->
32 | if (a is S && b is S) {
33 | comparator.compare(a, b)
34 | } else 0
35 | }
36 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/completion/CompletionItemsCollector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("CONTEXT_RECEIVERS_DEPRECATED")
2 |
3 | package me.darthorimar.rekot.completion
4 |
5 | import me.darthorimar.rekot.analysis.SafeAnalysisRunner
6 | import org.jetbrains.kotlin.analysis.api.KaSession
7 | import org.jetbrains.kotlin.analysis.api.components.KaCompletionExtensionCandidateChecker
8 | import org.jetbrains.kotlin.analysis.api.components.KaExtensionApplicabilityResult
9 | import org.jetbrains.kotlin.analysis.api.components.KaUseSiteVisibilityChecker
10 | import org.jetbrains.kotlin.analysis.api.signatures.KaCallableSignature
11 | import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
12 | import org.jetbrains.kotlin.analysis.api.symbols.KaDeclarationSymbol
13 | import org.jetbrains.kotlin.analysis.api.symbols.name
14 | import org.jetbrains.kotlin.lexer.KtSingleValueToken
15 | import org.jetbrains.kotlin.name.Name
16 | import org.jetbrains.kotlin.psi.KtDeclaration
17 | import org.koin.core.component.KoinComponent
18 | import org.koin.core.component.inject
19 | import kotlin.contracts.contract
20 |
21 | class CompletionItemsCollector(
22 | private val applicabilityChecker: KaCompletionExtensionCandidateChecker?,
23 | private val visibilityChecker: KaUseSiteVisibilityChecker,
24 | val nameFilter: (Name) -> Boolean,
25 | private val session: CompletionSession,
26 | ) : KoinComponent {
27 | private val factory: CompletionItemFactory by inject()
28 | private val safeAnalysisRunner: SafeAnalysisRunner by inject()
29 |
30 | private val items = mutableListOf()
31 | private val symbols = mutableSetOf()
32 |
33 | context(KaSession)
34 | fun add(declaration: KtDeclaration?, modify: (DeclarationCompletionItemBuilder.() -> Unit)? = null) {
35 | add(declaration?.symbol, modify)
36 | }
37 |
38 | context(KaSession)
39 | @JvmName("addDeclarations")
40 | fun add(declarations: Iterable, modify: (DeclarationCompletionItemBuilder.() -> Unit)? = null) {
41 | declarations.forEach { add(it, modify) }
42 | }
43 |
44 | context(KaSession)
45 | fun add(symbol: KaDeclarationSymbol?, modify: (DeclarationCompletionItemBuilder.() -> Unit)? = null) {
46 | when (symbol) {
47 | null -> {}
48 | in symbols -> {}
49 |
50 | is KaCallableSymbol -> {
51 | val substituted = symbol.asApplicableSignature()
52 | if (substituted != null) {
53 | add(substituted, modify)
54 | } else if (!symbol.isExtension) {
55 | _add(symbol, modify)
56 | }
57 | }
58 |
59 | else -> {
60 | _add(symbol, modify)
61 | }
62 | }
63 | }
64 |
65 | context(KaSession)
66 | @JvmName("addSymbols")
67 | fun add(symbols: Sequence, modify: (DeclarationCompletionItemBuilder.() -> Unit)? = null) {
68 | symbols.forEach { add(it, modify) }
69 | }
70 |
71 | context(KaSession)
72 | fun add(signature: KaCallableSignature<*>?, modify: (DeclarationCompletionItemBuilder.() -> Unit)? = null) {
73 | _add(signature, modify)
74 | }
75 |
76 | context(KaSession)
77 | @JvmName("addSignatures")
78 | fun add(symbols: Sequence>, modify: (DeclarationCompletionItemBuilder.() -> Unit)? = null) {
79 | symbols.forEach { add(it, modify) }
80 | }
81 |
82 | context(KaSession)
83 | fun add(item: KtSingleValueToken, modify: (KeywordItemBuilder.() -> Unit)? = null) {
84 | session.interruptIfCancelled()
85 | val name = Name.identifier(item.value)
86 | if (!nameFilter(name)) return
87 | val element = keywordCompletionItem(item)
88 |
89 | items +=
90 | if (modify == null) element
91 | else
92 | keywordCompletionItem() {
93 | with(element)
94 | modify()
95 | }
96 | }
97 |
98 | context(KaSession)
99 | fun add(items: Iterable, modify: (KeywordItemBuilder.() -> Unit)? = null) {
100 | items.forEach { add(it, modify) }
101 | }
102 |
103 | context(KaSession)
104 | @Suppress("FunctionName")
105 | private fun _add(symbol: KaDeclarationSymbol?, modify: (DeclarationCompletionItemBuilder.() -> Unit)?) {
106 | session.interruptIfCancelled()
107 | if (!acceptsSymbol(symbol)) return
108 | val item = factory.createCompletionItem(symbol) ?: return
109 | symbols += symbol
110 | items +=
111 | if (modify == null) item
112 | else {
113 | declarationCompletionItem {
114 | with(item)
115 | modify()
116 | }
117 | }
118 | }
119 |
120 | context(KaSession)
121 | @Suppress("FunctionName")
122 | private fun _add(signature: KaCallableSignature<*>?, modify: (DeclarationCompletionItemBuilder.() -> Unit)?) {
123 | session.interruptIfCancelled()
124 | if (!acceptsSymbol(signature?.symbol)) return
125 |
126 | val item = factory.createCompletionItem(signature) ?: return
127 | symbols += signature.symbol
128 | items +=
129 | if (modify == null) item
130 | else
131 | declarationCompletionItem {
132 | with(item)
133 | modify()
134 | }
135 | }
136 |
137 | context(KaSession)
138 | private fun acceptsSymbol(symbol: KaDeclarationSymbol?): Boolean {
139 | contract { returns(true) implies (symbol != null) }
140 | if (symbol == null) return false
141 | if (symbol in symbols) return false
142 | if (!visibilityChecker.isVisible(symbol)) {
143 | return false
144 | }
145 | if (symbol.name?.asString()?.contains(COMPLETION_FAKE_IDENTIFIER) == true) return false
146 | return true
147 | }
148 |
149 | context(KaSession)
150 | private fun KaCallableSymbol.asApplicableSignature(): KaCallableSignature? {
151 | val checker = applicabilityChecker ?: return asSignature()
152 | return safeAnalysisRunner.runSafely {
153 | when (val applicability = checker.computeApplicability(this@asApplicableSignature)) {
154 | is KaExtensionApplicabilityResult.Applicable -> substitute(applicability.substitutor)
155 | else -> null
156 | }
157 | }
158 | }
159 |
160 | fun build(): Collection {
161 | return items
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/completion/CompletionPopup.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.completion
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.cells.CellId
5 | import me.darthorimar.rekot.cells.Cells
6 | import me.darthorimar.rekot.editor.Editor
7 | import me.darthorimar.rekot.editor.FancyEditorCommands
8 | import org.koin.core.component.inject
9 |
10 | class CompletionPopup(val cellId: CellId, elements: List, prefix: String) : AppComponent {
11 | private val editor: Editor by inject()
12 | private val cells: Cells by inject()
13 | private val fancyEditorCommands: FancyEditorCommands by inject()
14 | private val sorter: CompletionItemSorter by inject()
15 |
16 | private var _selectedIndex = 0
17 | private var _prefix = prefix
18 | private var _elements = elements.let { sorter.sort(it, _prefix) }
19 |
20 | val prefix: String
21 | get() = _prefix
22 |
23 | val elements: List
24 | get() = _elements
25 |
26 | val selectedIndex: Int
27 | get() = _selectedIndex
28 |
29 | fun up() {
30 | if (_selectedIndex > 0) {
31 | _selectedIndex--
32 | }
33 | }
34 |
35 | fun down() {
36 | if (_selectedIndex < elements.lastIndex) {
37 | _selectedIndex++
38 | }
39 | }
40 |
41 | fun choseItemOnDot(): Boolean {
42 | val element = selectedItem()
43 | if (prefix == element.name) {
44 | choseItem()
45 | return true
46 | }
47 | return false
48 | }
49 |
50 | fun choseItem() {
51 | val element = selectedItem()
52 | repeat(prefix.length) { editor.backspace() }
53 | editor.type(element.insert)
54 | (element as? CompletionItem.Declaration)?.let { declaration ->
55 | if (declaration.import) {
56 | fancyEditorCommands.insertImport(cells.getCell(cellId), declaration.fqName!!)
57 | }
58 | }
59 | editor.offsetColumn(element.moveCaret)
60 | }
61 |
62 | private fun selectedItem(): CompletionItem = elements[_selectedIndex]
63 |
64 | fun addPrefix(char: Char): Boolean {
65 | _prefix += char
66 | if (_elements.isEmpty()) return false
67 | val selectedElement = _elements[_selectedIndex]
68 | _elements = _elements.filter { matchesPrefix(_prefix, it.name) }.let { sorter.sort(it, _prefix) }
69 |
70 | val newElementIndex = if (_selectedIndex == 0) 0 else _elements.indexOf(selectedElement)
71 | _selectedIndex = if (newElementIndex >= 0) newElementIndex else 0
72 | return elements.isNotEmpty()
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/completion/CompletionPopupRenderer.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.completion
2 |
3 | import com.googlecode.lanterna.TerminalPosition
4 | import com.googlecode.lanterna.TerminalSize
5 | import com.googlecode.lanterna.screen.Screen
6 | import me.darthorimar.rekot.app.AppComponent
7 | import me.darthorimar.rekot.cells.Cells
8 | import me.darthorimar.rekot.config.AppConfig
9 | import me.darthorimar.rekot.screen.ScreenController
10 | import me.darthorimar.rekot.style.Colors
11 | import me.darthorimar.rekot.style.StyleRenderer
12 | import me.darthorimar.rekot.style.Styles
13 | import me.darthorimar.rekot.style.styledText
14 | import me.darthorimar.rekot.util.Scroller
15 | import me.darthorimar.rekot.util.limit
16 | import org.koin.core.component.inject
17 |
18 | class CompletionPopupRenderer : AppComponent {
19 | private val appConfig: AppConfig by inject()
20 | private val cells: Cells by inject()
21 | private val screen: Screen by inject()
22 | private val completion: Completion by inject()
23 | private val renderer: StyleRenderer by inject()
24 | private val screenController: ScreenController by inject()
25 | private val styles: Styles by inject()
26 | private val colors: Colors by inject()
27 |
28 | private val scroller = Scroller(appConfig.completionPopupHeight)
29 |
30 | fun render() {
31 | if (!completion.popupShown) return
32 | val popup = completion.popup
33 | val elements = popup.elements
34 | if (elements.isEmpty()) return
35 | val width = computeWidth()
36 | val offset = scroller.scroll(popup.selectedIndex)
37 | val height = computeHeight()
38 | scroller.resize(height)
39 | val addDotsAtTheEnd = offset + height < elements.size
40 |
41 | val position =
42 | popup.computePosition(
43 | width = width,
44 | height = height,
45 | addDotsAtTheEnd = addDotsAtTheEnd,
46 | )
47 | val graphics =
48 | screen
49 | .newTextGraphics()
50 | .newTextGraphics(
51 | position,
52 | TerminalSize(
53 | /* columns= */ width,
54 | /* rows= */ height + if (addDotsAtTheEnd) 1 else 0,
55 | ),
56 | )
57 |
58 | val toShow = elements.limit(offset, height)
59 | val text = styledText {
60 | for ((i, completionElement) in toShow.withIndex()) {
61 | val selected = popup.selectedIndex == offset + i
62 | styledLine {
63 | from(styles.COMPLETION)
64 | if (selected) {
65 | with(styles.COMPLETION_SELECTED)
66 | }
67 | styled {
68 | foregroundColor(
69 | when (completionElement.tag) {
70 | CompletionItemTag.FUNCTION -> colors.FUNCTION
71 | CompletionItemTag.PROPERTY -> colors.PROPERTY
72 | CompletionItemTag.CLASS -> colors.CLASS
73 | CompletionItemTag.LOCAL_VARIABLE -> colors.LOCAL_VARIABLE
74 | CompletionItemTag.KEYWORD -> colors.KEYWORD
75 | })
76 | string(completionElement.tag.text)
77 | }
78 |
79 | string(" ")
80 | string(completionElement.show)
81 | }
82 | }
83 | if (addDotsAtTheEnd) {
84 | styledLine {
85 | from(styles.COMPLETION)
86 | foregroundColor(colors.COMMENT)
87 | string("...")
88 | }
89 | }
90 | }
91 | renderer.render(graphics, text)
92 | }
93 |
94 | private fun computeHeight(): Int {
95 | val raw = appConfig.completionPopupHeight
96 | return raw.coerceAtMost(screenController.screenSize.rows / 2)
97 | }
98 |
99 | private fun computeWidth(): Int {
100 | return (screen.terminalSize.columns * 3 / 4).coerceAtLeast(appConfig.completionPopupMinWidth)
101 | }
102 |
103 | private fun CompletionPopup.computePosition(width: Int, height: Int, addDotsAtTheEnd: Boolean): TerminalPosition {
104 | val cell = cells.getCell(cellId)
105 | val columnOffset = (cell.cursor.column - prefix.length).coerceAtLeast(0)
106 | val column =
107 | if (columnOffset + width <= screen.terminalSize.columns) columnOffset
108 | else screen.terminalSize.columns - width
109 |
110 | val cursorPosition = screenController.cursor
111 | val row =
112 | if (cursorPosition.row + 1 + height >= screen.terminalSize.rows) {
113 | cursorPosition.row - height - (if (addDotsAtTheEnd) 1 else 0)
114 | } else {
115 | cursorPosition.row + 1
116 | }
117 | return TerminalPosition(column, row)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/completion/CompletionSession.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.completion
2 |
3 | class CompletionSession {
4 | @Volatile private var running = true
5 |
6 | fun stop() {
7 | running = false
8 | }
9 |
10 | fun interruptIfCancelled() {
11 | if (!running) {
12 | throw InterruptedCompletionException()
13 | }
14 | }
15 | }
16 |
17 | class InterruptedCompletionException : Exception()
18 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/completion/completionUtils.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.completion
2 |
3 | const val COMPLETION_FAKE_IDENTIFIER = "RWwgUHN5IEtvbmdyb28g"
4 |
5 | fun matchesPrefix(prefix: String?, item: String): Boolean {
6 | if (prefix == null) return true
7 | return item.contains(prefix, ignoreCase = true)
8 | }
9 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/completion/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.completion
2 |
3 | import org.koin.dsl.module
4 |
5 | val completionModule = module {
6 | single { CompletionPopupFactory() }
7 | single { Completion() }
8 | single { CompletionItemFactory() }
9 | single { CompletionPopupRenderer() }
10 | single { CompletionItemSorter() }
11 | }
12 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/cursor/CursorModifier.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.cursor
2 |
3 | interface CursorModifier {
4 | var row: Int
5 | var column: Int
6 |
7 | fun resetToZero() {
8 | row = 0
9 | column = 0
10 | }
11 |
12 | fun asCursor(): Cursor
13 | }
14 |
15 | class Cursor(val row: Int, val column: Int) {
16 | fun asModifier(maxColumn: Int? = null): CursorModifier = CursorModifierImpl(row, column, maxColumn)
17 |
18 | fun modified(modifier: CursorModifier.() -> Unit): Cursor = asModifier().apply(modifier).asCursor()
19 |
20 | override fun toString(): String {
21 | return "($row, $column)"
22 | }
23 |
24 | companion object {
25 | fun zero() = Cursor(0, 0)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/cursor/CursorModifierImpl.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.cursor
2 |
3 | class CursorModifierImpl(
4 | row: Int,
5 | column: Int,
6 | private var maxColumn: Int? = null,
7 | private val onRowModification: (Int) -> Unit = {},
8 | ) : CursorModifier {
9 | override var column = column
10 | set(value) {
11 | field =
12 | when {
13 | maxColumn != null -> value.coerceAtMost(maxColumn!!)
14 | else -> value
15 | }.coerceAtLeast(0)
16 | }
17 |
18 | override var row = row
19 | set(value) {
20 | field = value.coerceAtLeast(0)
21 | column = column
22 | onRowModification(value)
23 | }
24 |
25 | override fun resetToZero() {
26 | row = 0
27 | column = 0
28 | }
29 |
30 | fun updateMaxColumn(newMaxColumn: Int) {
31 | maxColumn = newMaxColumn
32 | column = column
33 | }
34 |
35 | override fun asCursor(): Cursor = Cursor(row, column)
36 | }
37 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/Editor.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.app.SubscriptionContext
5 | import me.darthorimar.rekot.app.subscribe
6 | import me.darthorimar.rekot.cells.Cell
7 | import me.darthorimar.rekot.cells.CellModifier
8 | import me.darthorimar.rekot.cells.Cells
9 | import me.darthorimar.rekot.cursor.CursorModifierImpl
10 | import me.darthorimar.rekot.editor.view.EditorView
11 | import me.darthorimar.rekot.editor.view.EditorViewLine
12 | import me.darthorimar.rekot.editor.view.EditorViewProvider
13 | import me.darthorimar.rekot.events.Event
14 | import me.darthorimar.rekot.execution.CellExecutionState
15 | import me.darthorimar.rekot.execution.CellExecutionStateProvider
16 | import me.darthorimar.rekot.screen.ScreenController
17 | import me.darthorimar.rekot.util.Scroller
18 | import org.koin.core.component.inject
19 |
20 | class Editor : AppComponent {
21 | private val cells: Cells by inject()
22 | private val editorViewProvider: EditorViewProvider by inject()
23 | private val cellExecutionStateProvider: CellExecutionStateProvider by inject()
24 | private val screenController: ScreenController by inject()
25 |
26 | private val enterHandler = EnterHandler()
27 | private val typingHandler = TypingHandler()
28 |
29 | private val editorView: EditorView
30 | get() = editorViewProvider.view
31 |
32 | val focusedCell: Cell
33 | get() = currentLine.cell
34 |
35 | private val scroller = Scroller(screenController.screenSize.rows)
36 | val viewPosition: Int
37 | get() = scroller.viewPosition
38 |
39 | context(SubscriptionContext)
40 | override fun performSubscriptions() {
41 | subscribe { e ->
42 | scroller.resize(e.screenSize.rows)
43 | cursor.updateMaxColumn(e.screenSize.columns)
44 | }
45 | }
46 |
47 | val cursor =
48 | CursorModifierImpl(row = 1, column = 0, maxColumn = screenController.screenSize.columns) { scroller.scroll(it) }
49 |
50 | private fun modify(action: CellModifier.() -> Unit) {
51 | if (currentLine is EditorViewLine.CodeLine) {
52 | focusedCell.modify(action)
53 | }
54 | }
55 |
56 | private val currentLine: EditorViewLine
57 | get() = editorView.lines[cursor.row]
58 |
59 | fun down() {
60 | while (true) {
61 | if (cursor.row == editorView.lines.size - 1) return
62 | cursor.row++
63 | val line = currentLine
64 | if (line !is EditorViewLine.NonNavigatableLine) {
65 | break
66 | }
67 | }
68 | adjustColumn()
69 | }
70 |
71 | fun up() {
72 | while (true) {
73 | if (cursor.row == 0) return
74 | cursor.row--
75 | val line = currentLine
76 | if (line !is EditorViewLine.NonNavigatableLine) {
77 | break
78 | }
79 | }
80 | adjustColumn()
81 | }
82 |
83 | private fun adjustColumn() {
84 | when (currentLine) {
85 | is EditorViewLine.CodeLine -> {
86 | modify { adjustColumn() }
87 | }
88 |
89 | is EditorViewLine.NavigatableLine -> {
90 | cursor.column = 0
91 | }
92 |
93 | is EditorViewLine.NonNavigatableLine -> {
94 | error("NonNavigatableLine should not be navigated 🙃")
95 | }
96 | }
97 | }
98 |
99 | fun left() {
100 | modify { left() }
101 | }
102 |
103 | fun right() {
104 | modify { right() }
105 | }
106 |
107 | fun delete() {
108 | modify { delete() }
109 | }
110 |
111 | fun backspace() {
112 | modify { backspace() }
113 | }
114 |
115 | fun enter() {
116 | modify { enterHandler.enter(this) }
117 | }
118 |
119 | fun type(char: Char) {
120 | modify { typingHandler.type(char, this) }
121 | fireEvent(Event.AfterCharTyping(focusedCell.id, char))
122 | }
123 |
124 | fun type(string: String) {
125 | modify { insert(string) }
126 | }
127 |
128 | fun offsetColumn(offset: Int) {
129 | modify { offsetColumn(offset) }
130 | }
131 |
132 | fun clearCell() {
133 | modify { clear() }
134 | }
135 |
136 | fun navigateToCell(newCell: Cell) {
137 | cursor.row = editorView.codeLineNumberToOffset(newCell, 0)
138 | cursor.column = 0
139 | }
140 |
141 | fun deleteCell() {
142 | if (cellExecutionStateProvider.getCellExecutionState(focusedCell.id) is CellExecutionState.Executing) {
143 | fireEvent(Event.Error(focusedCell.id, "Can't delete cell while it's executing"))
144 | return
145 | }
146 | if (cells.cells.size == 1) {
147 | fireEvent(Event.Error(focusedCell.id, "Can't delete the only cell"))
148 | return
149 | }
150 | val previousCell = cells.previousCell(focusedCell)
151 | cells.deleteCell(focusedCell)
152 | navigateToCell(previousCell)
153 | }
154 |
155 | fun tab() {
156 | modify { tab() }
157 | }
158 |
159 | fun shiftTab() {
160 | modify { removeTab() }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/FancyEditorCommands.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor
2 |
3 | import me.darthorimar.rekot.analysis.CellAnalyzer
4 | import me.darthorimar.rekot.app.AppComponent
5 | import me.darthorimar.rekot.cells.Cell
6 | import org.koin.core.component.inject
7 |
8 | class FancyEditorCommands : AppComponent {
9 | private val analyzer: CellAnalyzer by inject()
10 |
11 | @Suppress("UnstableApiUsage")
12 | fun insertImport(cell: Cell, import: String) {
13 | val insertAfterLineIndex =
14 | analyzer.inAnalysisContext(cell) {
15 | val existingImports =
16 | ktFile.importDirectives
17 | .mapNotNull { it.importPath }
18 | .filterNot { it.isAllUnder }
19 | .mapTo(mutableSetOf()) { it.fqName.asString() }
20 | if (import in existingImports) return@inAnalysisContext null
21 | ktFile.importList?.imports?.lastOrNull()?.getLineNumber()?.let { it + 1 } ?: 0
22 | } ?: return
23 |
24 | cell.modify {
25 | insertNewLineFromStart("import $import", insertAfterLineIndex)
26 | if (insertAfterLineIndex == 0) {
27 | insertNewLineFromStart("", 1)
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor
2 |
3 | import me.darthorimar.rekot.editor.view.editorViewModule
4 | import org.koin.dsl.module
5 |
6 | val editorModule = module {
7 | includes(editorViewModule, editorViewModule)
8 | single { Editor() }
9 | single { FancyEditorCommands() }
10 | }
11 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/handlers.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor
2 |
3 | import com.intellij.psi.PsiElement
4 | import com.intellij.psi.PsiErrorElement
5 | import com.intellij.psi.util.nextLeafs
6 | import com.intellij.psi.util.parents
7 | import me.darthorimar.rekot.analysis.CellAnalyzer
8 | import me.darthorimar.rekot.app.AppComponent
9 | import me.darthorimar.rekot.cells.CellModifier
10 | import me.darthorimar.rekot.config.AppConfig
11 | import org.jetbrains.kotlin.lexer.KtTokens
12 | import org.jetbrains.kotlin.psi.KtBlockExpression
13 | import org.jetbrains.kotlin.psi.KtScript
14 | import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace
15 | import org.koin.core.component.inject
16 |
17 | class EnterHandler : AppComponent {
18 | private val appConfig: AppConfig by inject()
19 | private val cellAnalyzer: CellAnalyzer by inject()
20 |
21 | fun enter(modifier: CellModifier) =
22 | cellAnalyzer.inAnalysisContext(modifier.cell, cursor = modifier.cell.cursor) {
23 | with(modifier) {
24 | val currentLine = currentLine
25 | lines[cursor.row] = currentLine.substring(0, cursor.column)
26 | val elementAtCursor = getPsiElementAtCursor()
27 |
28 | val indentLevel =
29 | elementAtCursor?.let { indentLevel(it) * appConfig.tabSize }
30 | ?: currentLine.takeWhile { it == ' ' }.length
31 | if (elementAtCursor == null || !tryEnterBetweenPairBraces(elementAtCursor, currentLine, indentLevel)) {
32 | insertNewLine(currentLine.substring(cursor.column), indent = indentLevel)
33 | }
34 | down()
35 | cursor.column = indentLevel
36 | }
37 | }
38 |
39 | private fun CellModifier.tryEnterBetweenPairBraces(
40 | elementAtCursor: PsiElement,
41 | currentLine: String,
42 | indentLevel: Int,
43 | ): Boolean {
44 | val closingBrace = bracesTokenPairs[elementAtCursor.node.elementType]
45 | if (closingBrace != null &&
46 | elementAtCursor.getNextSiblingIgnoringWhitespace()?.node?.elementType == closingBrace) {
47 | insertNewLine("", indent = indentLevel)
48 | val leftover = currentLine.substring(cursor.column)
49 | if (leftover.isNotBlank()) {
50 | insertNewLine(leftover, indent = (indentLevel - appConfig.tabSize).coerceAtLeast(0), offset = 1)
51 | }
52 |
53 | return true
54 | }
55 | return false
56 | }
57 |
58 | private fun indentLevel(element: PsiElement) =
59 | element
60 | .parents(withSelf = true)
61 | .takeWhile { it !is KtBlockExpression || it.parent !is KtScript }
62 | .count { it is KtBlockExpression }
63 | }
64 |
65 | class TypingHandler : AppComponent {
66 | private val analyzer: CellAnalyzer by inject()
67 |
68 | fun type(char: Char, CellModifier: CellModifier) =
69 | with(CellModifier) {
70 | insert(char)
71 | analyzer.inAnalysisContext(CellModifier.cell, cursor = cell.cursor) {
72 | val elementAtCursor = getPsiElementAtCursor()
73 | if (elementAtCursor != null) {
74 | if (char in bracketPairs) {
75 | val closing = bracketPairs.getValue(char)
76 | if (expectsClosingBrace(elementAtCursor, closing)) {
77 | insert(closing, cursorOffset = 1)
78 | }
79 | }
80 | }
81 | cursor.column++
82 | }
83 | }
84 |
85 | private fun expectsClosingBrace(openingPsi: PsiElement, closing: Char) =
86 | openingPsi.nextLeafs.any { leaf ->
87 | if (leaf !is PsiErrorElement) return@any false
88 | val errorDescription = leaf.errorDescription
89 | // not the best way to check for this, but it's simple and works in the most cases :)
90 | errorDescription == "Expecting '${closing}'" ||
91 | errorDescription == "Expecting an expression" ||
92 | closing == '}' && errorDescription == "Missing '}"
93 | }
94 | }
95 |
96 | private val bracesTokenPairs =
97 | mapOf(
98 | KtTokens.LPAR to KtTokens.RPAR,
99 | KtTokens.LBRACE to KtTokens.RBRACE,
100 | KtTokens.LBRACKET to KtTokens.RBRACKET,
101 | KtTokens.OPEN_QUOTE to KtTokens.CLOSING_QUOTE,
102 | )
103 |
104 | private val bracketPairs = mapOf('(' to ')', '{' to '}', '[' to ']', '"' to '"')
105 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/renderer/CellViewRenderer.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor.renderer
2 |
3 | import com.googlecode.lanterna.TerminalPosition
4 | import com.googlecode.lanterna.screen.Screen
5 | import me.darthorimar.rekot.app.AppComponent
6 | import me.darthorimar.rekot.editor.Editor
7 | import me.darthorimar.rekot.editor.view.EditorViewProvider
8 | import me.darthorimar.rekot.style.StyleRenderer
9 | import me.darthorimar.rekot.style.Styles
10 | import me.darthorimar.rekot.style.styledText
11 | import org.koin.core.component.inject
12 |
13 | class CellViewRenderer(private val screen: Screen) : AppComponent {
14 | private val renderer: StyleRenderer by inject()
15 | private val editor: Editor by inject()
16 | private val editorViewProvider: EditorViewProvider by inject()
17 | private val styles: Styles by inject()
18 |
19 | fun render() {
20 | val view = editorViewProvider.view
21 | val text = styledText {
22 | with(view.lines.drop(editor.viewPosition).take(screen.terminalSize.rows).map { it.line })
23 | fillUp(styles.EMPTY, screen.terminalSize.rows)
24 | }
25 | renderer.render(screen.newTextGraphics(), text)
26 |
27 | screen.cursorPosition =
28 | TerminalPosition(/* column= */ editor.cursor.column, /* row= */ editor.cursor.row - editor.viewPosition)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/view/CellViewBuilder.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor.view
2 |
3 | import me.darthorimar.rekot.cells.Cell
4 | import me.darthorimar.rekot.editor.view.EditorViewProvider.Companion.GAP
5 | import me.darthorimar.rekot.style.*
6 | import org.koin.core.component.KoinComponent
7 | import org.koin.core.component.inject
8 |
9 | abstract class CellViewBuilder(private val cell: Cell) : KoinComponent {
10 | protected val styles: Styles by inject()
11 | protected val colors: Colors by inject()
12 |
13 | private val lines = mutableListOf()
14 |
15 | abstract fun styledLine(init: StyledLineBuilder.() -> Unit): StyledLine
16 |
17 | fun codeLine(builder: StyledLineBuilder.() -> Unit) {
18 | lines += EditorViewLine.CodeLine(styledLine(builder), cell)
19 | }
20 |
21 | fun navigatableLine(builder: StyledLineBuilder.() -> Unit) {
22 | lines += EditorViewLine.NavigatableLine(styledLine(builder), cell)
23 | }
24 |
25 | fun nonNavigatableLine(builder: StyledLineBuilder.() -> Unit) {
26 | lines += EditorViewLine.NonNavigatableLine(styledLine(builder), cell)
27 | }
28 |
29 | fun emptyLine() {
30 | nonNavigatableLine {
31 | from(styles.HEADER)
32 | fill(" ")
33 | }
34 | }
35 |
36 | fun header(text: String, modifier: StyleModifier = StyleModifier.EMPTY, extra: StyledLineBuilder.() -> Unit = {}) {
37 | nonNavigatableLine {
38 | from(styles.HEADER)
39 | with(modifier)
40 | gap(GAP)
41 | string(text)
42 | extra()
43 | }
44 | }
45 |
46 | fun codeLikeBlock(bgColor: Color, build: CellViewBuilder.() -> Unit) {
47 | lines += CellViewBuilderForCodeBlock(bgColor, cell).apply(build).build().lines
48 | }
49 |
50 | fun build(): CellView {
51 | return CellView(cell.id, lines)
52 | }
53 | }
54 |
55 | class CellViewBuilderImpl(cell: Cell) : CellViewBuilder(cell) {
56 | override fun styledLine(init: StyledLineBuilder.() -> Unit): StyledLine {
57 | return me.darthorimar.rekot.style.styledLine(init)
58 | }
59 | }
60 |
61 | private class CellViewBuilderForCodeBlock(private val bgColor: Color, cell: Cell) : CellViewBuilder(cell) {
62 | override fun styledLine(init: StyledLineBuilder.() -> Unit): StyledLine {
63 | return me.darthorimar.rekot.style.styledLine {
64 | foregroundColor(colors.CELL_BG)
65 | backgroundColor(bgColor)
66 | styled { init() }
67 | fill(" ")
68 | }
69 | }
70 | }
71 |
72 | inline fun cellView(cell: Cell, init: CellViewBuilderImpl.() -> Unit): CellView {
73 | return CellViewBuilderImpl(cell).apply(init).build()
74 | }
75 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/view/EditorView.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor.view
2 |
3 | import me.darthorimar.rekot.cells.Cell
4 | import me.darthorimar.rekot.cells.CellId
5 | import me.darthorimar.rekot.style.StyledLine
6 |
7 | class EditorView(private val cells: Map) {
8 | val lines: List = cells.values.flatMap { it.lines }
9 |
10 | private val offsets: Map| = buildMap {
11 | var offset = 0
12 | for ((cell, lines) in cells) {
13 | put(cell, offset)
14 | offset += lines.lines.size
15 | }
16 | }
17 |
18 | fun cellOffset(cell: Cell): Int {
19 | return offsets.getValue(cell)
20 | }
21 |
22 | fun view(cell: Cell): CellView {
23 | return cells.getValue(cell)
24 | }
25 |
26 | fun codeLineNumberToOffset(cell: Cell, codeLineNumber: Int): Int {
27 | return cellOffset(cell) + view(cell).codeLineNumberToOffset(codeLineNumber)
28 | }
29 |
30 | fun offsetToCodeLineNumber(cell: Cell, offset: Int): Int {
31 | return view(cell).offsetToCodeLineNumber(offset - cellOffset(cell))
32 | }
33 | }
34 |
35 | class CellView(private val cellId: CellId, val lines: List) {
36 | private val lineNumberToOffset = buildMap {
37 | var lineNumber = 0
38 | for ((i, line) in lines.withIndex()) {
39 | if (line is EditorViewLine.CodeLine) {
40 | put(lineNumber, i)
41 | lineNumber++
42 | }
43 | }
44 | }
45 |
46 | private val offsetToLineNumber = lineNumberToOffset.entries.associate { (k, v) -> v to k }
47 |
48 | fun codeLineNumberToOffset(lineNumber: Int): Int {
49 | return lineNumberToOffset[lineNumber]
50 | ?: error(
51 | "Can't find view line with for Cell#${cellId}, lineNumber: $lineNumber, available: ${lineNumberToOffset.entries}")
52 | }
53 |
54 | fun offsetToCodeLineNumber(offset: Int): Int {
55 | return offsetToLineNumber[offset]
56 | ?: error(
57 | "Can't find line number for Cell#$cellId, offset: $offset, available: ${offsetToLineNumber.entries}")
58 | }
59 | }
60 |
61 | sealed interface EditorViewLine {
62 | val line: StyledLine
63 | val cell: Cell
64 |
65 | data class CodeLine(override val line: StyledLine, override val cell: Cell) : EditorViewLine {
66 | override fun toString(): String {
67 | return " C $line"
68 | }
69 | }
70 |
71 | data class NavigatableLine(override val line: StyledLine, override val cell: Cell) : EditorViewLine {
72 | override fun toString(): String {
73 | return " M $line"
74 | }
75 | }
76 |
77 | data class NonNavigatableLine(override val line: StyledLine, override val cell: Cell) : EditorViewLine {
78 | override fun toString(): String {
79 | return "NN $line"
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/view/EditorViewProvider.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor.view
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.app.SubscriptionContext
5 | import me.darthorimar.rekot.app.subscribe
6 | import me.darthorimar.rekot.cells.Cell
7 | import me.darthorimar.rekot.cells.Cells
8 | import me.darthorimar.rekot.events.Event
9 | import me.darthorimar.rekot.screen.ScreenController
10 | import me.darthorimar.rekot.style.styleModifier
11 | import me.darthorimar.rekot.util.Scroller
12 | import org.koin.core.component.inject
13 |
14 | class EditorViewProvider : AppComponent {
15 | private val cells: Cells by inject()
16 | private val screenController: ScreenController by inject()
17 |
18 | private val codeViewGenerator: CodeViewGenerator by inject()
19 | private val soutViewGenerator: SoutViewGenerator by inject()
20 | private val resultViewGenerator: ResultViewGenerator by inject()
21 | private val errorViewGenerator: ErrorViewGenerator by inject()
22 |
23 | private val scroller = Scroller(screenController.screenSize.rows)
24 |
25 | context(SubscriptionContext)
26 | override fun performSubscriptions() {
27 | subscribe { e -> scroller.resize(e.screenSize.rows) }
28 | }
29 |
30 | val view: EditorView
31 | get() = compute()
32 |
33 | private fun compute(): EditorView {
34 | val cellViews = cells.cells.associateWith { cellView(it) { cell(it) } }
35 | return EditorView(cellViews)
36 | }
37 |
38 | private fun CellViewBuilder.cell(cell: Cell) {
39 | header("Cell#${cell.id}", modifier = styleModifier { bold() })
40 | with(codeViewGenerator) { code(cell) }
41 | with(errorViewGenerator) { error(cell) }
42 | with(resultViewGenerator) { result(cell) }
43 | with(soutViewGenerator) { sout(cell) }
44 | emptyLine()
45 | }
46 |
47 | companion object {
48 | const val GAP = 0
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/view/ErrorViewGenerator.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor.view
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.cells.Cell
5 | import me.darthorimar.rekot.errors.CellErrorProvider
6 | import me.darthorimar.rekot.style.Colors
7 | import org.koin.core.component.inject
8 |
9 | class ErrorViewGenerator : AppComponent {
10 | private val cellErrorProvider: CellErrorProvider by inject()
11 | private val colors: Colors by inject()
12 |
13 | fun CellViewBuilder.error(cell: Cell) {
14 | when (val error = cellErrorProvider.getCellError(cell.id)) {
15 | is String -> {
16 | header("Error")
17 | codeLikeBlock(colors.ERROR_BG) {
18 | for (line in error.lines()) {
19 | navigatableLine {
20 | foregroundColor(colors.DEFAULT)
21 | string(line)
22 | }
23 | }
24 | }
25 | }
26 | null -> {}
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/view/ResultViewGenerator.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor.view
2 |
3 | import me.darthorimar.rekot.analysis.CellAnalyzer
4 | import me.darthorimar.rekot.app.AppComponent
5 | import me.darthorimar.rekot.cells.Cell
6 | import me.darthorimar.rekot.cursor.Cursor
7 | import me.darthorimar.rekot.execution.CellExecutionState
8 | import me.darthorimar.rekot.execution.CellExecutionStateProvider
9 | import me.darthorimar.rekot.execution.ExecutionResult
10 | import me.darthorimar.rekot.execution.ExecutorValueRenderer
11 | import me.darthorimar.rekot.style.Colors
12 | import me.darthorimar.rekot.style.Styles
13 | import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource
14 | import org.jetbrains.kotlin.psi.KtNameReferenceExpression
15 | import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
16 | import org.jetbrains.kotlin.types.Variance
17 | import org.koin.core.component.inject
18 |
19 | class ResultViewGenerator : AppComponent {
20 | private val executionResultProvider: CellExecutionStateProvider by inject()
21 | private val cellAnalyzer: CellAnalyzer by inject()
22 | private val styles: Styles by inject()
23 | private val colors: Colors by inject()
24 |
25 | fun CellViewBuilder.result(cell: Cell) {
26 | when (val executionResult = executionResultProvider.getCellExecutionState(cell.id)) {
27 | is CellExecutionState.Error -> {}
28 | CellExecutionState.Executing -> {
29 | header("Executing...") {
30 | italic()
31 | string(" (Ctrl+B to cancel)")
32 | }
33 | }
34 |
35 | is CellExecutionState.Executed ->
36 | when (val result = executionResult.result) {
37 | is ExecutionResult.Result -> {
38 | header("Result")
39 | codeLikeBlock(colors.EDITOR_BG) {
40 | val valueRenderedLines = ExecutorValueRenderer.render(result.value).lines()
41 | navigatableLine {
42 | styled {
43 | from(styles.PROPERTY)
44 | string(result.resultVariableName)
45 | }
46 | styled {
47 | from(styles.CODE)
48 | val typeText = getResultVariableTypeRendered(result)
49 | if (typeText != null) {
50 | string(": $typeText")
51 | }
52 | }
53 | styled {
54 | from(styles.CODE)
55 | string(" = ")
56 | if (valueRenderedLines.size == 1) {
57 | string(valueRenderedLines.single())
58 | }
59 | }
60 | }
61 | if (valueRenderedLines.size > 1) {
62 | for (line in valueRenderedLines) {
63 | navigatableLine {
64 | styled {
65 | from(styles.CODE)
66 | gap((result.resultVariableName + " = ").length)
67 | string(line)
68 | }
69 | }
70 | }
71 | }
72 | }
73 | }
74 |
75 | is ExecutionResult.Void -> {
76 | if (result.sout == null) {
77 | header("Result")
78 | codeLikeBlock(colors.EDITOR_BG) {
79 | navigatableLine {
80 | from(styles.CODE)
81 | string("res = Unit")
82 | }
83 | }
84 | }
85 | }
86 | }
87 |
88 | null -> {}
89 | }
90 | }
91 |
92 | private fun getResultVariableTypeRendered(result: ExecutionResult.Result): String? {
93 | return cellAnalyzer.inAnalysisContext("res", result.resultVariableName, Cursor.zero()) {
94 | analyze {
95 | val reference = ktFile.collectDescendantsOfType().single()
96 | val type = reference.expressionType ?: return@inAnalysisContext null
97 | type.render(KaTypeRendererForSource.WITH_SHORT_NAMES, Variance.INVARIANT)
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/view/SoutViewGenerator.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor.view
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.cells.Cell
5 | import me.darthorimar.rekot.execution.CellExecutionState
6 | import me.darthorimar.rekot.execution.CellExecutionStateProvider
7 | import me.darthorimar.rekot.style.Colors
8 | import me.darthorimar.rekot.style.Styles
9 | import org.koin.core.component.inject
10 |
11 | class SoutViewGenerator : AppComponent {
12 | private val executionResultProvider: CellExecutionStateProvider by inject()
13 | private val styles: Styles by inject()
14 | private val colors: Colors by inject()
15 |
16 | fun CellViewBuilder.sout(cell: Cell) {
17 | val executionResult = executionResultProvider.getCellExecutionState(cell.id)
18 | if (executionResult !is CellExecutionState.Executed) return
19 | val sout = executionResult.result.sout ?: return
20 |
21 | header("Output")
22 | codeLikeBlock(colors.SOUT_BG) {
23 | for (soutLine in sout.split('\n').dropLastWhile { it.isEmpty() }) {
24 | navigatableLine {
25 | from(styles.SOUT)
26 | string(soutLine)
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/editor/view/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.editor.view
2 |
3 | import org.koin.dsl.module
4 |
5 | val editorViewModule = module {
6 | single { EditorViewProvider() }
7 | single { CodeViewGenerator() }
8 | single { ResultViewGenerator() }
9 | single { SoutViewGenerator() }
10 | single { ErrorViewGenerator() }
11 | }
12 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/errors/CellErrorProvider.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.errors
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.app.SubscriptionContext
5 | import me.darthorimar.rekot.app.subscribe
6 | import me.darthorimar.rekot.cells.CellId
7 | import me.darthorimar.rekot.events.Event
8 |
9 | class CellErrorProvider : AppComponent {
10 | private val errors = mutableMapOf()
11 |
12 | context(SubscriptionContext)
13 | override fun performSubscriptions() {
14 | subscribe { e -> errors[e.cellId] = e.message }
15 | subscribe { e -> clearError(e.cellId) }
16 | subscribe { e -> clearError(e.cellId) }
17 | }
18 |
19 | fun getCellError(cellId: CellId): String? {
20 | return errors[cellId]
21 | }
22 |
23 | private fun clearError(cellId: CellId) {
24 | errors.remove(cellId)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/errors/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.errors
2 |
3 | import org.koin.dsl.module
4 |
5 | val errorsModule = module { single { CellErrorProvider() } }
6 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/events/Event.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.events
2 |
3 | import me.darthorimar.rekot.cells.CellId
4 | import me.darthorimar.rekot.completion.CompletionPopup
5 | import me.darthorimar.rekot.execution.CellExecutionState
6 | import me.darthorimar.rekot.screen.ScreenSize
7 |
8 | sealed interface Event {
9 | sealed interface Keyboard : Event {
10 | sealed interface Typing : Keyboard {
11 | data class TextTyping(val text: String) : Typing
12 |
13 | data class CharTyping(val char: Char) : Typing
14 | }
15 |
16 | data object Tab : Keyboard
17 |
18 | data object ShiftTab : Keyboard
19 |
20 | data object Enter : Keyboard
21 |
22 | data object Backspace : Keyboard
23 |
24 | data object Delete : Keyboard
25 |
26 | data object NewCell : Keyboard
27 |
28 | data object DeleteCell : Keyboard
29 |
30 | data object ClearCell : Keyboard
31 |
32 | data object RefreshScreen : Keyboard
33 |
34 | data object StopExecution : Keyboard
35 |
36 | data object ShowHelp : Keyboard
37 |
38 | data object Escape : Keyboard
39 |
40 | data object ExecuteCell : Keyboard
41 |
42 | data class ArrowButton(val direction: Direction) : Keyboard {
43 | enum class Direction {
44 | UP,
45 | DOWN,
46 | LEFT,
47 | RIGHT,
48 | }
49 | }
50 | }
51 |
52 | data object AppStarted: Event
53 |
54 | data class AfterCharTyping(val cellId: CellId, val char: Char) : Event
55 |
56 | data object CloseApp : Event
57 |
58 | data class ShowPopup(val popup: CompletionPopup) : Event
59 |
60 | data class CellTextChanged(val cellId: CellId) : Event
61 |
62 | data class CellCleared(val cellId: CellId) : Event
63 |
64 | data class CellExecutionStateChanged(val cellId: CellId, val state: CellExecutionState) : Event
65 |
66 | data class TerminalResized(val screenSize: ScreenSize) : Event
67 |
68 | data class Error(val cellId: CellId, val message: String) : Event
69 | }
70 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/events/EventListener.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.events
2 |
3 | fun interface EventListener {
4 | fun onEvent(event: E)
5 | }
6 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/events/EventQueue.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.events
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.app.AppState
5 | import org.koin.core.component.inject
6 | import java.util.concurrent.LinkedBlockingQueue
7 | import java.util.concurrent.TimeUnit
8 | import kotlin.getValue
9 | import kotlin.reflect.KClass
10 |
11 | class EventQueue : AppComponent {
12 | private val queue = LinkedBlockingQueue()
13 |
14 | private val appState: AppState by inject()
15 |
16 | val byEventListeners = mutableListOf, EventListener<*>>>()
17 |
18 | fun fire(event: Event) {
19 | queue.offer(event)
20 | }
21 |
22 | fun processAllBlocking() {
23 | processFirstBlocking()
24 | processAllNonBlocking()
25 | }
26 |
27 | fun processAllNonBlocking() {
28 | do {
29 | val event = queue.poll() ?: break
30 | notifyListener(event)
31 | } while (true)
32 | }
33 |
34 | fun processFirstBlocking(): Event? {
35 | val appState = appState
36 | while (appState.active) {
37 | // timeout to listen for the app shutdown
38 | val event = queue.poll(POLL_TIMEOUT, TimeUnit.MILLISECONDS) ?: continue
39 | notifyListener(event)
40 | return event
41 | }
42 | return null
43 | }
44 |
45 | fun processFirstNonBlocking(): Event? {
46 | val event = queue.poll() ?: return null
47 | notifyListener(event)
48 | return event
49 | }
50 |
51 | private fun notifyListener(event: Event) {
52 | /*snapshot of the list to avoid CCE on subscribing inside the listener*/
53 | val listenersSnapshot = byEventListeners.toList()
54 | for ((eventKClass, listener) in listenersSnapshot) {
55 | if (eventKClass.isInstance(event)) {
56 | (listener as EventListener).onEvent(event)
57 | }
58 | }
59 | }
60 |
61 | inline fun subscribe(listener: EventListener) {
62 | byEventListeners.add(E::class to listener)
63 | }
64 |
65 | companion object {
66 | private const val POLL_TIMEOUT = 100L
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/events/KeyboardEventProcessor.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.events
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.app.SubscriptionContext
5 | import me.darthorimar.rekot.app.subscribe
6 | import me.darthorimar.rekot.cells.Cells
7 | import me.darthorimar.rekot.completion.Completion
8 | import me.darthorimar.rekot.editor.Editor
9 | import me.darthorimar.rekot.events.Event.Keyboard.Typing.CharTyping
10 | import me.darthorimar.rekot.execution.CellExecutor
11 | import me.darthorimar.rekot.help.HelpWindow
12 | import me.darthorimar.rekot.screen.ScreenController
13 | import org.koin.core.component.inject
14 |
15 | class KeyboardEventProcessor : AppComponent {
16 | private val editor: Editor by inject()
17 | private val cells: Cells by inject()
18 | private val screenController: ScreenController by inject()
19 | private val completion: Completion by inject()
20 | private val cellExecutor: CellExecutor by inject()
21 | private val helpWindow: HelpWindow by inject()
22 |
23 | context(SubscriptionContext)
24 | override fun performSubscriptions() {
25 | subscribe { event -> process(event) }
26 | }
27 |
28 | private fun process(event: Event.Keyboard) {
29 | when (event) {
30 | is CharTyping -> {
31 | if (completion.popupShown) {
32 | when (event.char) {
33 | '.' -> {
34 | if (completion.choseItemOnDot()) {
35 | fireEvent(CharTyping('.'))
36 | return
37 | }
38 | }
39 | }
40 | }
41 | editor.type(event.char)
42 | }
43 |
44 | is Event.Keyboard.Typing.TextTyping -> {
45 | editor.type(event.text)
46 | }
47 |
48 | Event.Keyboard.Backspace -> {
49 | editor.backspace()
50 | completion.closePopup()
51 | }
52 |
53 | Event.Keyboard.ClearCell -> {
54 | editor.clearCell()
55 | completion.closePopup()
56 | }
57 |
58 | Event.Keyboard.Delete -> {
59 | editor.delete()
60 | completion.closePopup()
61 | }
62 |
63 | Event.Keyboard.Enter -> {
64 | completionOrEditor({ completion.choseItem() }, { editor.enter() })
65 | }
66 |
67 | Event.Keyboard.Escape -> {
68 | helpWindow.close()
69 | completion.closePopup()
70 | }
71 |
72 | Event.Keyboard.ExecuteCell -> {
73 | cellExecutor.execute(editor.focusedCell)
74 | completion.closePopup()
75 | }
76 |
77 | Event.Keyboard.StopExecution -> {
78 | cellExecutor.stop(editor.focusedCell)
79 | completion.closePopup()
80 | }
81 |
82 | Event.Keyboard.NewCell -> {
83 | editor.navigateToCell(cells.newCell())
84 | completion.closePopup()
85 | }
86 |
87 | Event.Keyboard.DeleteCell -> {
88 | completion.closePopup()
89 | editor.deleteCell()
90 | }
91 |
92 | Event.Keyboard.RefreshScreen -> {
93 | screenController.fullRefresh()
94 | }
95 |
96 | Event.Keyboard.ShiftTab -> {
97 | editor.shiftTab()
98 | }
99 |
100 | Event.Keyboard.Tab -> {
101 | completionOrEditor({ completion.choseItem() }, { editor.tab() })
102 | }
103 |
104 | is Event.Keyboard.ArrowButton -> {
105 | when (event.direction) {
106 | Event.Keyboard.ArrowButton.Direction.UP -> {
107 | completionOrEditor({ completion.up() }, { editor.up() })
108 | }
109 | Event.Keyboard.ArrowButton.Direction.DOWN -> {
110 | completionOrEditor({ completion.down() }, { editor.down() })
111 | }
112 |
113 | Event.Keyboard.ArrowButton.Direction.LEFT -> {
114 | completion.closePopup()
115 | editor.left()
116 | }
117 | Event.Keyboard.ArrowButton.Direction.RIGHT -> {
118 | completion.closePopup()
119 | editor.right()
120 | }
121 | }
122 | }
123 |
124 | Event.Keyboard.ShowHelp -> {
125 | helpWindow.show()
126 | }
127 | }
128 | }
129 |
130 | private inline fun completionOrEditor(completion: () -> Unit = {}, editor: () -> Unit = {}) {
131 | if (this.completion.popupShown) {
132 | completion()
133 | } else {
134 | editor()
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/events/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.events
2 |
3 | import org.koin.dsl.module
4 |
5 | val eventModule = module {
6 | single { EventQueue() }
7 | single { KeyboardEventProcessor() }
8 | }
9 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/execution/CellExecutionState.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.execution
2 |
3 | sealed interface CellExecutionState {
4 | data class Executed(val result: ExecutionResult) : CellExecutionState
5 |
6 | data object Executing : CellExecutionState
7 |
8 | data class Error(val errorMessage: String) : CellExecutionState
9 | }
10 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/execution/CellExecutionStateProvider.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.execution
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.app.SubscriptionContext
5 | import me.darthorimar.rekot.app.subscribe
6 | import me.darthorimar.rekot.cells.CellId
7 | import me.darthorimar.rekot.events.Event
8 |
9 | class CellExecutionStateProvider : AppComponent {
10 | private val states = mutableMapOf()
11 |
12 | context(SubscriptionContext)
13 | override fun performSubscriptions() {
14 | subscribe { e ->
15 | states[e.cellId] = e.state
16 | if (e.state is CellExecutionState.Error) {
17 | fireEvent(Event.Error(e.cellId, e.state.errorMessage))
18 | }
19 | }
20 | subscribe { e -> states.remove(e.cellId) }
21 | }
22 |
23 | fun getCellExecutionState(cellId: CellId): CellExecutionState? {
24 | return states[cellId]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/execution/CellsClassLoader.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.execution
2 |
3 | import me.darthorimar.rekot.cells.CellId
4 |
5 | class CellsClassLoader(stdlibClassLoader: ClassLoader) : ClassLoader(stdlibClassLoader) {
6 | private val indexToCompiledFile = mutableMapOf>()
7 | private var map: Map = emptyMap()
8 |
9 | fun updateEntries(cellId: CellId, files: List) {
10 | indexToCompiledFile[cellId] = files
11 | map =
12 | indexToCompiledFile.entries
13 | .sortedBy { it.key }
14 | .asSequence()
15 | .flatMap { it.value }
16 | .filterIsInstance()
17 | .associateBy { it.fqName }
18 | }
19 |
20 | override fun findClass(name: String): Class<*> {
21 | val file = map[name] ?: return super.findClass(name)
22 | return defineClass(name, file.content, 0, file.content.size)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/execution/CompiledFile.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.execution
2 |
3 | sealed interface CompiledFile {
4 | val path: String
5 | val content: ByteArray
6 |
7 | class CompiledClass(val fqName: String, override val path: String, override val content: ByteArray) : CompiledFile
8 |
9 | class CompiledNonClassFile(override val path: String, override val content: ByteArray) : CompiledFile
10 | }
11 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/execution/ConsoleInterceptor.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.execution
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import java.io.ByteArrayOutputStream
5 | import java.io.InputStream
6 | import java.io.PrintStream
7 |
8 | class ConsoleInterceptor : AppComponent {
9 | fun intercept(block: () -> R): Pair {
10 | val buffer = ByteArrayOutputStream()
11 | val stream = PrintStream(buffer)
12 |
13 | val originalOut = System.out
14 | val originalIn = System.`in`
15 | System.setOut(stream)
16 | System.setIn(exceptionThrowingIn)
17 | val result =
18 | try {
19 | block()
20 | } finally {
21 | System.setIn(originalIn)
22 | System.setOut(originalOut)
23 | }
24 | val sout = buffer.toByteArray().decodeToString().takeUnless { it.isEmpty() }
25 | return result to sout
26 | }
27 | }
28 |
29 | private val exceptionThrowingIn =
30 | object : InputStream() {
31 | override fun read(): Int {
32 | throw ReadFromSystemInNotAllowedException()
33 | }
34 | }
35 |
36 | class ReadFromSystemInNotAllowedException : Exception(MESSAGE) {
37 | companion object {
38 | const val MESSAGE = "Reading from `System.in` is not allowed"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/execution/ExecutingThread.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.execution
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.cells.CellId
5 | import me.darthorimar.rekot.events.Event
6 | import java.util.concurrent.atomic.AtomicReference
7 | import kotlin.concurrent.thread
8 |
9 | class ExecutingThread : AppComponent {
10 | private var thread: Thread? = null
11 | private val _executingCell = AtomicReference(null)
12 |
13 | fun execute(cellId: CellId, task: () -> Unit) {
14 | if (!_executingCell.compareAndSet(null, cellId)) {
15 | error("Cannot add task, executing thread is busy")
16 | }
17 | thread = thread {
18 | try {
19 | task()
20 | } finally {
21 | _executingCell.set(null)
22 | }
23 | }
24 | }
25 |
26 | fun stop(cellId: CellId): Boolean {
27 | if (_executingCell.get() == null) return false
28 | val thread = thread ?: return false
29 | thread.interrupt()
30 | repeat(5) {
31 | if (!thread.isAlive) {
32 | _executingCell.set(null)
33 | return true
34 | }
35 | Thread.sleep(100)
36 | // wait for the thread to stop for 500ms
37 | }
38 | try {
39 | @Suppress("DEPRECATION") thread.stop()
40 | } catch (_: UnsupportedOperationException) {
41 | // starting from java 20, Thread.stop() is throwing UnsupportedOperationException and does not stop the
42 | // thread so we can do nothing here :(
43 | fireEvent(
44 | Event.Error(
45 | cellId,
46 | "You are running Java ${System.getProperty("java.version")}.\n" +
47 | "Fully stopping a thread that does not listen to interruptions is not supported in Java versions 20 and above.\n" +
48 | "So, we cannot stop the cell execution :(",
49 | ))
50 | return false
51 | }
52 | _executingCell.set(null)
53 | return true
54 | }
55 |
56 | val currentlyExecuting: CellId?
57 | get() = _executingCell.get()
58 | }
59 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/execution/ExecutionResult.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.execution
2 |
3 | import java.nio.file.Path
4 |
5 | sealed interface ExecutionResult {
6 | val sout: String?
7 | val scriptClassFqName: String
8 | val scriptInstance: Any
9 | val classRoot: Path
10 |
11 | class Result(
12 | val value: Any?,
13 | val resultVariableName: String,
14 | override val scriptClassFqName: String,
15 | override val sout: String?,
16 | override val scriptInstance: Any,
17 | override val classRoot: Path,
18 | ) : ExecutionResult
19 |
20 | class Void(
21 | override val sout: String?,
22 | override val scriptClassFqName: String,
23 | override val scriptInstance: Any,
24 | override val classRoot: Path,
25 | ) : ExecutionResult
26 | }
27 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/execution/ExecutorValueRenderer.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.execution
2 |
3 | import me.darthorimar.rekot.util.retry
4 | import java.nio.file.Path
5 |
6 | object ExecutorValueRenderer {
7 | fun render(value: Any?): String =
8 | runCatching {
9 | when (value) {
10 | is Array<*> -> value.joinToString(prefix = "[", postfix = "]") { render(it) }
11 | is IntArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
12 | is ByteArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
13 | is ShortArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
14 | is LongArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
15 | is FloatArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
16 | is DoubleArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
17 | is CharArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
18 | is BooleanArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
19 | is UIntArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
20 | is UByteArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
21 | is UShortArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
22 | is ULongArray -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
23 | is Set<*> -> value.joinToString(prefix = "{", postfix = "}") { render(it) }
24 | is List<*> -> value.joinToString(prefix = "[", postfix = "]") { render(it) }
25 | is Path -> value.toString() // Path is Iterable, so its check comes before Iterable
26 | is Collection<*> -> value.render()
27 | is Sequence<*> -> value.toString()
28 | is Map<*, *> -> value.entries.joinToString(prefix = "{", postfix = "}") { render(it) }
29 | is Map.Entry<*, *> -> "(${render(value.key)}, ${render(value.value)})"
30 | is CharSequence -> "\"$value\""
31 | is Char -> "'$value'"
32 | else -> value.toString()
33 | }
34 | }
35 | .retry { value.toString() }
36 | .getOrElse { value?.let { it::class.java }.toString() }
37 |
38 | private fun Collection<*>.render(prefix: String = this.shortClassName) = buildString {
39 | append(prefix)
40 | append("[")
41 | for ((i, e) in this@render.withIndex()) {
42 | if (i >= LIMIT) {
43 | append("...")
44 | break
45 | }
46 | append(render(e))
47 | append(", ")
48 | }
49 | append("]")
50 | }
51 |
52 | private val Any.shortClassName: String
53 | get() = this::class.simpleName ?: this::class.java.simpleName
54 |
55 | const val LIMIT = 10
56 | }
57 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/execution/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.execution
2 |
3 | import org.koin.dsl.module
4 |
5 | val executionModule = module {
6 | single { CellExecutionStateProvider() }
7 | single { CellExecutor() }
8 | single { ConsoleInterceptor() }
9 | }
10 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/help/HelpRenderer.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.help
2 |
3 | import com.googlecode.lanterna.screen.Screen
4 | import me.darthorimar.rekot.app.AppComponent
5 | import me.darthorimar.rekot.style.StyleRenderer
6 | import org.koin.core.component.inject
7 |
8 | class HelpRenderer : AppComponent {
9 | private val window: HelpWindow by inject()
10 | private val renderer: StyleRenderer by inject()
11 | private val screen: Screen by inject()
12 |
13 | fun render() {
14 | if (!window.shown) return
15 | val text = window.text()
16 | renderer.render(screen.newTextGraphics(), text)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/help/HelpWindow.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.help
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.config.APP_LOGO
5 | import me.darthorimar.rekot.config.APP_NAME
6 | import me.darthorimar.rekot.config.AppConfig
7 | import me.darthorimar.rekot.screen.ScreenController
8 | import me.darthorimar.rekot.style.StyledText
9 | import me.darthorimar.rekot.style.Styles
10 | import me.darthorimar.rekot.style.styledText
11 | import org.koin.core.component.inject
12 |
13 | class HelpWindow : AppComponent {
14 | private val styles: Styles by inject()
15 | private val screenController: ScreenController by inject()
16 |
17 | private val keybindings =
18 | listOf(
19 | "Ctrl+E" binding "Execute current cell",
20 | "Ctrl+B" binding "Stop current cell execution",
21 | "Ctrl+D" binding "Delete current cell",
22 | "Ctrl+L" binding "Clear current cell",
23 | "Ctrl+R" binding "Refresh screen",
24 | "Ctrl+C" binding "Exit $APP_NAME",
25 | )
26 |
27 | private var _shown: Boolean = false
28 |
29 | fun show() {
30 | _shown = true
31 | }
32 |
33 | fun close() {
34 | _shown = false
35 | }
36 |
37 | val shown: Boolean
38 | get() = _shown
39 |
40 | fun text(): StyledText {
41 | return styledText {
42 | if (screenController.screenSize.rows > APP_LOGO.lines().size + keybindings.size + 2) {
43 | for (line in APP_LOGO.lines()) {
44 | styledLine {
45 | from(styles.HELP)
46 | string(line)
47 | }
48 | }
49 | }
50 |
51 | styledLine { from(styles.HELP) }
52 |
53 | styledLine {
54 | from(styles.HELP)
55 | string("$APP_NAME keybindings")
56 | styled {
57 | italic()
58 | string(" ")
59 | string("(Press Esc to exit)")
60 | }
61 | }
62 |
63 | styledLine { from(styles.HELP) }
64 |
65 | for (keybinding in keybindings) {
66 | styledLine {
67 | from(styles.HELP)
68 | string("• ")
69 | styled {
70 | bold()
71 | string(keybinding.key)
72 | }
73 | string(" - ")
74 | string(keybinding.description)
75 | fill(" ")
76 | }
77 | }
78 | styledLine { from(styles.HELP) }
79 | fillUp(styles.HELP, screenController.screenSize.rows)
80 | }
81 | }
82 | }
83 |
84 | private data class Keybinding(
85 | val key: String,
86 | val description: String,
87 | )
88 |
89 | private infix fun String.binding(description: String): Keybinding {
90 | return Keybinding(this, description)
91 | }
92 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/help/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.help
2 |
3 | import org.koin.dsl.module
4 |
5 | val helpModule = module {
6 | single { HelpRenderer() }
7 | single { HelpWindow() }
8 | }
9 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/logging/AppThrowableRenderer.kt:
--------------------------------------------------------------------------------
1 | // package me.darthorimar.repl.logging
2 | //
3 | // import org.apache.log4j.DefaultThrowableRenderer
4 | // import org.apache.log4j.spi.ThrowableRenderer
5 | //
6 | // class AppThrowableRenderer : ThrowableRenderer {
7 | // private val delegate = DefaultThrowableRenderer()
8 | // override fun doRender(throwable: Throwable?): Array {
9 | // if (throwable == null) return emptyArray()
10 | //
11 | // throwable.stackTraceToString()
12 | // IllegalStateException("AAA", IllegalStateException("BBB")).stackTraceToString()
13 | // }
14 | // }
15 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/logging/logging.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.logging
2 |
3 | import me.darthorimar.rekot.config.APP_NAME_LOWERCASE
4 | import me.darthorimar.rekot.config.LOG_DIR_PROPERTY
5 | import java.nio.file.Path
6 | import java.nio.file.Paths
7 | import java.time.LocalDate
8 | import java.time.format.DateTimeFormatter
9 | import java.util.logging.FileHandler
10 | import java.util.logging.Level
11 | import java.util.logging.Logger
12 | import java.util.logging.SimpleFormatter
13 | import kotlin.io.path.absolutePathString
14 | import kotlin.io.path.div
15 |
16 | inline fun logger(): Logger {
17 | val logger = Logger.getLogger(C::class.java.getName())
18 | val handler = FileHandler(getLogFilePath().absolutePathString(), true).apply { formatter = SimpleFormatter() }
19 | logger.addHandler(handler)
20 | return logger
21 | }
22 |
23 | fun Logger.error(message: String, error: Throwable) {
24 | log(Level.SEVERE, message, error)
25 | }
26 |
27 | @PublishedApi
28 | internal fun getLogFilePath(): Path {
29 | val logsDir = Paths.get(System.getProperty(LOG_DIR_PROPERTY))
30 | val dateSuffix = LocalDate.now().format(logNameDateFormatter)
31 | return logsDir / "$APP_NAME_LOWERCASE-$dateSuffix.log"
32 | }
33 |
34 | private val logNameDateFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy")
35 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/main.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot
2 |
3 | import com.googlecode.lanterna.screen.Screen
4 | import com.googlecode.lanterna.screen.TerminalScreen
5 | import com.googlecode.lanterna.terminal.DefaultTerminalFactory
6 | import com.googlecode.lanterna.terminal.Terminal
7 | import me.darthorimar.rekot.analysis.CompilerErrorInterceptor
8 | import me.darthorimar.rekot.analysis.LoggingErrorInterceptor
9 | import me.darthorimar.rekot.app.App
10 | import me.darthorimar.rekot.app.AppComponent
11 | import me.darthorimar.rekot.app.appModule
12 | import me.darthorimar.rekot.args.ArgsCommand
13 | import me.darthorimar.rekot.args.ArgsCommandsParser
14 | import me.darthorimar.rekot.args.ArgsSecondaryCommandExecutor
15 | import me.darthorimar.rekot.config.AppConfig
16 | import me.darthorimar.rekot.config.ConfigFactory
17 | import me.darthorimar.rekot.screen.screenModuleFactory
18 | import me.darthorimar.rekot.updates.UpdatesChecker
19 | import org.koin.core.context.GlobalContext.startKoin
20 | import org.koin.core.context.loadKoinModules
21 | import org.koin.dsl.module
22 | import java.io.OutputStream
23 | import java.io.PrintStream
24 | import kotlin.system.exitProcess
25 | import me.darthorimar.rekot.logging.*
26 |
27 |
28 | fun main(args: Array) {
29 | when (val command = ArgsCommandsParser.parse(args)) {
30 | ArgsCommand.RUN_APP -> {
31 | runApp()
32 | }
33 | is ArgsCommand.Secondary -> {
34 | ArgsSecondaryCommandExecutor.execute(command)
35 | }
36 | }
37 | }
38 |
39 | private fun runApp() {
40 | val config = ConfigFactory.createConfig()
41 | config.init()
42 |
43 | val appConfigModule = module { single { config } }
44 |
45 | startKoin { modules(appConfigModule, appModule) }
46 |
47 | try {
48 | withoutErr {
49 | withScreenAndTerminal { screen, terminal ->
50 | loadKoinModules(screenModuleFactory(screen, terminal))
51 | loadKoinModules(productionAppModule)
52 | AppComponent.performSubscriptions()
53 | App().runApp()
54 | }
55 | }
56 | } catch (e: Throwable) {
57 | logger().error("Application error", e)
58 | throw e
59 | } finally {
60 | exitProcess(0)
61 | }
62 | }
63 |
64 | private val productionAppModule = module {
65 | single { LoggingErrorInterceptor() }
66 | single { UpdatesChecker() }
67 | }
68 |
69 | private fun withoutErr(block: () -> Unit) {
70 | val original = System.err
71 | System.setErr(voidStream)
72 | try {
73 | block()
74 | } finally {
75 | System.setErr(original)
76 | }
77 | }
78 |
79 | private fun withScreenAndTerminal(block: (Screen, Terminal) -> Unit) {
80 | var screen: Screen? = null
81 | var terminal: Terminal? = null
82 | try {
83 | terminal =
84 | DefaultTerminalFactory()
85 | // .setForceTextTerminal(true)
86 | .createTerminal()
87 | screen = TerminalScreen(terminal)
88 | screen.startScreen()
89 | block(screen, terminal)
90 | } finally {
91 | screen?.stopScreen()
92 | terminal?.close()
93 | }
94 | }
95 |
96 | private val voidStream =
97 | PrintStream(
98 | object : OutputStream() {
99 | override fun write(b: Int) {}
100 | })
101 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/projectStructure/BinariesDeclarationProviderFactory.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.projectStructure
2 |
3 | import com.intellij.openapi.vfs.VirtualFile
4 | import com.intellij.psi.search.GlobalSearchScope
5 | import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProvider
6 | import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderFactory
7 | import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule
8 | import org.jetbrains.kotlin.analysis.api.standalone.base.declarations.KotlinStandaloneDeclarationProviderFactory
9 | import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment
10 |
11 | class BinariesDeclarationProviderFactory(
12 | kotlinCoreProjectEnvironment: KotlinCoreProjectEnvironment,
13 | libraryBinaryFiles: List,
14 | ) : KotlinDeclarationProviderFactory {
15 | private val delegate =
16 | KotlinStandaloneDeclarationProviderFactory(
17 | kotlinCoreProjectEnvironment.project,
18 | kotlinCoreProjectEnvironment.environment,
19 | sourceKtFiles = emptyList(),
20 | binaryRoots = libraryBinaryFiles,
21 | shouldBuildStubsForBinaryLibraries = true,
22 | skipBuiltins = true,
23 | )
24 |
25 | override fun createDeclarationProvider(
26 | scope: GlobalSearchScope,
27 | contextualModule: KaModule?,
28 | ): KotlinDeclarationProvider {
29 | return delegate.createDeclarationProvider(scope, contextualModule)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/projectStructure/Builtins.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.projectStructure
2 |
3 | import org.jetbrains.kotlin.analysis.api.impl.base.projectStructure.KaBuiltinsModuleImpl
4 | import org.jetbrains.kotlin.analysis.api.standalone.base.declarations.KotlinStandaloneDeclarationProviderFactory
5 | import org.jetbrains.kotlin.analysis.decompiler.psi.BuiltinsVirtualFileProvider
6 | import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment
7 | import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
8 |
9 | class Builtins(kotlinCoreProjectEnvironment: KotlinCoreProjectEnvironment) {
10 | val symbolProvider =
11 | KotlinStandaloneDeclarationProviderFactory(
12 | kotlinCoreProjectEnvironment.project,
13 | kotlinCoreProjectEnvironment.environment,
14 | sourceKtFiles = emptyList(),
15 | binaryRoots = emptyList(),
16 | shouldBuildStubsForBinaryLibraries = true,
17 | skipBuiltins = false,
18 | )
19 |
20 | val kaModule = KaBuiltinsModuleImpl(JvmPlatforms.defaultJvmPlatform, kotlinCoreProjectEnvironment.project)
21 |
22 | init {
23 | BuiltinsVirtualFileProvider.getInstance().getBuiltinVirtualFiles().forEach { it.kaModule = kaModule }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/projectStructure/CellScriptDefinitionProvider.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.projectStructure
2 |
3 | import com.intellij.mock.MockProject
4 | import me.darthorimar.rekot.app.AppComponent
5 | import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinProjectStructureProvider
6 | import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
7 | import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition.FromConfigurations
8 | import org.jetbrains.kotlin.scripting.definitions.ScriptDefinitionProvider
9 | import org.jetbrains.kotlin.scripting.definitions.ScriptEvaluationConfigurationFromHostConfiguration
10 | import org.jetbrains.kotlin.scripting.resolve.KtFileScriptSource
11 | import kotlin.script.experimental.api.*
12 | import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
13 |
14 | class CellScriptDefinitionProvider(private val project: MockProject) : ScriptDefinitionProvider, AppComponent {
15 | override fun findDefinition(script: SourceCode): ScriptDefinition {
16 | val ktFile = (script as KtFileScriptSource).ktFile
17 | val kaCellScriptModule =
18 | KotlinProjectStructureProvider.getModule(project, ktFile, useSiteModule = null) as KaCellScriptModule
19 | return CellScriptDefinition(kaCellScriptModule, kaCellScriptModule.resultVariableName)
20 | }
21 |
22 | override fun getDefaultDefinition(): ScriptDefinition = error("Should not be called")
23 |
24 | override fun getKnownFilenameExtensions(): Sequence = sequenceOf("kts")
25 |
26 | override fun isScript(script: SourceCode): Boolean = true
27 | }
28 |
29 | private class CellScriptDefinition(
30 | private val scriptModule: KaCellScriptModule,
31 | private val resultPropertyName: String?,
32 | ) :
33 | FromConfigurations(
34 | defaultJvmScriptingHostConfiguration,
35 | ScriptCompilationConfiguration {
36 | implicitReceivers(*scriptModule.dependentScriptInstances.map { it::class }.toTypedArray())
37 | resultPropertyName?.let { resultField(it) }
38 | defaultImports(scriptModule.imports)
39 | },
40 | ScriptEvaluationConfigurationFromHostConfiguration(defaultJvmScriptingHostConfiguration),
41 | ) {
42 | override val isDefault = true
43 | }
44 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/projectStructure/EssentialLibrary.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.projectStructure
2 |
3 | import com.intellij.openapi.vfs.VirtualFile
4 | import org.jetbrains.kotlin.analysis.api.projectStructure.KaLibraryModule
5 | import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment
6 | import java.nio.file.Path
7 |
8 | class EssentialLibrary(val kaModule: KaLibraryModule, val files: List) {
9 | companion object {
10 | fun create(
11 | roots: List,
12 | kotlinCoreProjectEnvironment: KotlinCoreProjectEnvironment,
13 | name: String,
14 | isSdk: Boolean,
15 | ): EssentialLibrary {
16 | val allVirtualFiles = getVirtualFilesByRoots(roots, kotlinCoreProjectEnvironment)
17 | val kaLibraryModule =
18 | KaLibraryModuleImpl(allVirtualFiles, roots, isSdk, name, kotlinCoreProjectEnvironment.project)
19 | return EssentialLibrary(kaLibraryModule, allVirtualFiles)
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/projectStructure/KaCellScriptModule.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.projectStructure
2 |
3 | import com.intellij.openapi.project.Project
4 | import com.intellij.psi.search.GlobalSearchScope
5 | import org.jetbrains.kotlin.analysis.api.KaPlatformInterface
6 | import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule
7 | import org.jetbrains.kotlin.analysis.api.projectStructure.KaScriptModule
8 | import org.jetbrains.kotlin.config.LanguageVersionSettings
9 | import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
10 | import org.jetbrains.kotlin.platform.TargetPlatform
11 | import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
12 | import org.jetbrains.kotlin.psi.KtFile
13 |
14 | class KaCellScriptModule(
15 | override val file: KtFile,
16 | override val project: Project,
17 | val dependentScriptInstances: List,
18 | binaryDependencies: List,
19 | val imports: List,
20 | val resultVariableName: String?,
21 | ) : KaScriptModule {
22 | override val directRegularDependencies: List = binaryDependencies
23 |
24 | override val contentScope: GlobalSearchScope
25 | get() = GlobalSearchScope.fileScope(file)
26 |
27 | override val languageVersionSettings: LanguageVersionSettings
28 | get() = LanguageVersionSettingsImpl.DEFAULT
29 |
30 | override val targetPlatform: TargetPlatform
31 | get() = JvmPlatforms.defaultJvmPlatform
32 |
33 | override val transitiveDependsOnDependencies: List
34 | get() = emptyList()
35 |
36 | override val directDependsOnDependencies: List
37 | get() = emptyList()
38 |
39 | override val directFriendDependencies: List
40 | get() = emptyList()
41 |
42 | @KaPlatformInterface
43 | override val baseContentScope: GlobalSearchScope
44 | get() = contentScope
45 | }
46 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/projectStructure/ProjectDeclarationFactoryImpl.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.projectStructure
2 |
3 | import com.intellij.psi.search.GlobalSearchScope
4 | import me.darthorimar.rekot.analysis.CellAnalyzer
5 | import me.darthorimar.rekot.analysis.CompiledCellStorage
6 | import me.darthorimar.rekot.app.AppComponent
7 | import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinCompositeDeclarationProvider
8 | import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProvider
9 | import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderFactory
10 | import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinFileBasedDeclarationProvider
11 | import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule
12 | import org.koin.core.component.inject
13 |
14 | class ProjectDeclarationFactoryImpl() : KotlinDeclarationProviderFactory, AppComponent {
15 | private val cellAnalyzer: CellAnalyzer by inject()
16 | private val compiledCellStorage: CompiledCellStorage by inject()
17 | private val projectStructure: ProjectStructure by inject()
18 |
19 | private val essentialLibrariesProviderFactory by lazy {
20 | BinariesDeclarationProviderFactory(
21 | projectStructure.kotlinCoreProjectEnvironment,
22 | projectStructure.essentialLibraries.allVirtualFiles,
23 | )
24 | }
25 |
26 | override fun createDeclarationProvider(
27 | scope: GlobalSearchScope,
28 | contextualModule: KaModule?,
29 | ): KotlinDeclarationProvider {
30 | val providers = buildList {
31 | cellAnalyzer.getAllCells().mapNotNullTo(this) { analyzableCell ->
32 | if (analyzableCell.ktFile.virtualFile !in scope) return@mapNotNullTo null
33 | KotlinFileBasedDeclarationProvider(analyzableCell.ktFile)
34 | }
35 | compiledCellStorage.allCompiledCells().mapTo(this) {
36 | it.compiledCellProviderFactory.createDeclarationProvider(scope, contextualModule)
37 | }
38 | add(essentialLibrariesProviderFactory.createDeclarationProvider(scope, contextualModule))
39 | add(projectStructure.builtins.symbolProvider.createDeclarationProvider(scope, contextualModule))
40 | }
41 | return KotlinCompositeDeclarationProvider.create(providers)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/projectStructure/ProjectEssentialLibraries.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.projectStructure
2 |
3 | class ProjectEssentialLibraries(val stdlib: EssentialLibrary, val jdk: EssentialLibrary) {
4 | val kaModules
5 | get() = listOf(stdlib.kaModule, jdk.kaModule)
6 |
7 | val allVirtualFiles
8 | get() = stdlib.files + jdk.files
9 |
10 | val allLibraries
11 | get() = listOf(stdlib, jdk)
12 | }
13 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/projectStructure/ProjectStructure.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.projectStructure
2 |
3 | import com.intellij.openapi.Disposable
4 | import com.intellij.openapi.project.Project
5 | import com.intellij.openapi.util.Disposer
6 | import me.darthorimar.rekot.app.AppComponent
7 | import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment
8 |
9 | class ProjectStructure(
10 | val kotlinCoreProjectEnvironment: KotlinCoreProjectEnvironment,
11 | val essentialLibraries: ProjectEssentialLibraries,
12 | val builtins: Builtins,
13 | val projectStructureProvider: ProjectStructureProviderImpl,
14 | private val projectDisposable: Disposable,
15 | ) : AppComponent {
16 | val project: Project
17 | get() = kotlinCoreProjectEnvironment.project
18 |
19 | fun shutdown() {
20 | Disposer.dispose(projectDisposable)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/projectStructure/ProjectStructureProviderImpl.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.projectStructure
2 |
3 | import com.intellij.openapi.util.Key
4 | import com.intellij.openapi.vfs.VirtualFile
5 | import com.intellij.psi.PsiElement
6 | import com.intellij.psi.PsiFile
7 | import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinProjectStructureProvider
8 | import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule
9 |
10 | class ProjectStructureProviderImpl() : KotlinProjectStructureProvider {
11 | override fun getImplementingModules(module: KaModule): List {
12 | error("Should not be called for jvm code")
13 | }
14 |
15 | override fun getModule(element: PsiElement, useSiteModule: KaModule?): KaModule {
16 | val containingFile = element.containingFile
17 |
18 | val virtualFile: VirtualFile? = containingFile.virtualFile
19 | virtualFile?.kaModule?.let {
20 | return it
21 | }
22 |
23 | error("Module not found for $virtualFile, $containingFile, $element")
24 | }
25 |
26 | fun setModule(file: VirtualFile, module: KaModule) {
27 | file.kaModule = module
28 | }
29 |
30 | fun setModule(file: PsiFile, module: KaModule) {
31 | setModule(file.virtualFile, module)
32 | }
33 | }
34 |
35 | private val module_KEY: Key = Key.create("module_KEY")
36 | var VirtualFile.kaModule: KaModule?
37 | get() = getUserData(module_KEY)
38 | set(value) {
39 | putUserData(module_KEY, value)
40 | }
41 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/projectStructure/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.projectStructure
2 |
3 | import com.intellij.openapi.project.Project
4 | import org.koin.dsl.module
5 |
6 | val projectStructureModule = module {
7 | single { ProjectStructureInitiator.initiateProjectStructure(appConfig = get()) }
8 | factory { get().project }
9 | }
10 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/projectStructure/projectStructureInternalUtils.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.projectStructure
2 |
3 | import com.intellij.openapi.project.Project
4 | import com.intellij.openapi.vfs.VirtualFile
5 | import com.intellij.psi.search.GlobalSearchScope
6 | import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
7 | import org.jetbrains.kotlin.analysis.api.KaPlatformInterface
8 | import org.jetbrains.kotlin.analysis.api.impl.base.util.LibraryUtils
9 | import org.jetbrains.kotlin.analysis.api.projectStructure.KaLibraryModule
10 | import org.jetbrains.kotlin.analysis.api.projectStructure.KaLibrarySourceModule
11 | import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule
12 | import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.StandaloneProjectFactory
13 | import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment
14 | import org.jetbrains.kotlin.platform.TargetPlatform
15 | import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
16 | import java.nio.file.Path
17 |
18 | fun getVirtualFilesByRoots(
19 | roots: List,
20 | kotlinCoreProjectEnvironment: KotlinCoreProjectEnvironment,
21 | ): List =
22 | StandaloneProjectFactory.getVirtualFilesForLibraryRoots(roots, kotlinCoreProjectEnvironment.environment).distinct().flatMap {
23 | LibraryUtils.getAllVirtualFilesFromRoot(it, includeRoot = true)
24 | }
25 |
26 | fun createLibraryModule(
27 | roots: List,
28 | kotlinCoreProjectEnvironment: KotlinCoreProjectEnvironment,
29 | name: String,
30 | isSdk: Boolean,
31 | ): KaLibraryModuleImpl {
32 | val allVirtualFiles = getVirtualFilesByRoots(roots, kotlinCoreProjectEnvironment)
33 | return KaLibraryModuleImpl(allVirtualFiles, roots, isSdk, name, kotlinCoreProjectEnvironment.project)
34 | }
35 |
36 | class KaLibraryModuleImpl(
37 | val virtualFiles: List,
38 | override val binaryRoots: List,
39 | override val isSdk: Boolean,
40 | override val libraryName: String,
41 | override val project: Project,
42 | ) : KaLibraryModule {
43 | @KaExperimentalApi
44 | override val binaryVirtualFiles: Collection
45 | get() = emptyList()
46 |
47 | override val contentScope: GlobalSearchScope = GlobalSearchScope.filesScope(project, virtualFiles)
48 |
49 | override val directDependsOnDependencies: List
50 | get() = emptyList()
51 |
52 | override val directFriendDependencies: List
53 | get() = emptyList()
54 |
55 | @KaPlatformInterface
56 | override val baseContentScope: GlobalSearchScope get() = contentScope
57 |
58 | override val directRegularDependencies: List
59 | get() = emptyList()
60 |
61 | override val transitiveDependsOnDependencies: List
62 | get() = emptyList()
63 |
64 | override val librarySources: KaLibrarySourceModule?
65 | get() = null
66 |
67 | override val targetPlatform: TargetPlatform
68 | get() = JvmPlatforms.unspecifiedJvmPlatform
69 |
70 | override fun toString(): String {
71 | return "KaLibraryModuleImpl('$libraryName')"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/psi/CellPsiUtils.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.psi
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.cells.Cell
5 | import me.darthorimar.rekot.projectStructure.ProjectStructure
6 | import org.jetbrains.kotlin.psi.KtFile
7 | import org.jetbrains.kotlin.psi.KtPsiFactory
8 | import org.jetbrains.kotlin.util.suffixIfNot
9 | import org.koin.core.component.inject
10 |
11 | class CellPsiUtils : AppComponent {
12 | private val projectStructure: ProjectStructure by inject()
13 |
14 | fun createKtFile(cell: Cell, filename: String): KtFile {
15 | val factory = KtPsiFactory(projectStructure.project, eventSystemEnabled = true)
16 | return factory.createFile(filename.suffixIfNot(".kts"), cell.text)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/psi/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.psi
2 |
3 | import org.koin.dsl.module
4 |
5 | val psiModule = module { single { CellPsiUtils() } }
6 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/psi/psiUtills.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.psi
2 |
3 | import com.intellij.openapi.editor.Document
4 | import com.intellij.psi.PsiDocumentManager
5 | import com.intellij.psi.PsiFile
6 |
7 | val PsiFile.document: Document
8 | get() {
9 | return PsiDocumentManager.getInstance(project).getDocument(this) ?: error("No document found for file $name")
10 | }
11 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/screen/HackyMacBugFix.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.screen
2 |
3 | import com.googlecode.lanterna.screen.Screen
4 | import me.darthorimar.rekot.app.AppComponent
5 | import me.darthorimar.rekot.app.SubscriptionContext
6 | import me.darthorimar.rekot.config.AppConfig
7 | import org.koin.core.component.inject
8 | import java.util.*
9 | import kotlin.concurrent.schedule
10 |
11 | /*
12 | * Mac prints some garbage characters when the app is open, so we need to refresh the screen
13 | */
14 | class HackyMacBugFix(private val screen: Screen) : AppComponent {
15 | private val appConfig: AppConfig by inject()
16 |
17 | private val enabled get() = appConfig.hackyMacFix
18 | private var timerInitiated = false
19 | private val timer by lazy { Timer(true) }
20 |
21 | context(SubscriptionContext)
22 | override fun performSubscriptions() {
23 | if (!enabled) return
24 | if (System.getProperty("os.name").lowercase().contains("mac")) {
25 | schedule(1000)
26 | schedule(2000)
27 | schedule(4000)
28 | }
29 | }
30 |
31 | fun scheduleAfterTyping() {
32 | if (!enabled) return
33 | if (timerInitiated) return
34 | timerInitiated = true
35 | schedule(500)
36 | schedule(1000)
37 | }
38 |
39 | private fun schedule(delayMS: Long) {
40 | if (!enabled) return
41 | timer.schedule(delayMS /*ms*/) { screen.refresh(Screen.RefreshType.COMPLETE) }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/screen/KeyboardInputPoller.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.screen
2 |
3 | import com.googlecode.lanterna.input.KeyStroke
4 | import com.googlecode.lanterna.input.KeyType
5 | import com.googlecode.lanterna.screen.Screen
6 | import me.darthorimar.rekot.app.AppComponent
7 | import me.darthorimar.rekot.app.AppState
8 | import me.darthorimar.rekot.events.Event
9 | import org.koin.core.component.inject
10 | import kotlin.concurrent.thread
11 |
12 | class KeyboardInputPoller(private val screen: Screen) : AppComponent {
13 | private val hackyMacBugFix: HackyMacBugFix by inject()
14 |
15 | private val appState: AppState by inject()
16 |
17 | fun startPolling() {
18 | thread(isDaemon = true, name = "KeyboardInputPoller") {
19 | val appState = appState
20 | while (appState.active) {
21 | readAndFire()
22 | }
23 | }
24 | }
25 |
26 | private fun readAndFire() {
27 | readKeyboard()?.let { fireEvent(it) }
28 | }
29 |
30 | private fun readKeyboard(): Event? {
31 | val keyStroke = screen.readInput() ?: return null
32 | hackyMacBugFix.scheduleAfterTyping()
33 | val textToPaste = handlePasteAction(keyStroke)
34 | val character = keyStroke.realCharacter
35 |
36 | return when {
37 | textToPaste != null -> Event.Keyboard.Typing.TextTyping(textToPaste)
38 | keyStroke.isCtrlDown && character == 'e' -> Event.Keyboard.ExecuteCell
39 | keyStroke.isCtrlDown && character == 'n' -> Event.Keyboard.NewCell
40 | keyStroke.isCtrlDown && character == 'd' -> Event.Keyboard.DeleteCell
41 | keyStroke.isCtrlDown && character == 'l' -> Event.Keyboard.ClearCell
42 | keyStroke.isCtrlDown && character == 'r' -> Event.Keyboard.RefreshScreen
43 | keyStroke.isCtrlDown && character == 'b' -> Event.Keyboard.StopExecution
44 | keyStroke.keyType == KeyType.F1 -> Event.Keyboard.ShowHelp
45 | keyStroke.keyType == KeyType.ArrowDown ->
46 | Event.Keyboard.ArrowButton(Event.Keyboard.ArrowButton.Direction.DOWN)
47 |
48 | keyStroke.keyType == KeyType.ArrowUp -> Event.Keyboard.ArrowButton(Event.Keyboard.ArrowButton.Direction.UP)
49 | keyStroke.keyType == KeyType.ArrowLeft ->
50 | Event.Keyboard.ArrowButton(Event.Keyboard.ArrowButton.Direction.LEFT)
51 |
52 | keyStroke.keyType == KeyType.ArrowRight ->
53 | Event.Keyboard.ArrowButton(Event.Keyboard.ArrowButton.Direction.RIGHT)
54 |
55 | keyStroke.keyType == KeyType.Delete -> Event.Keyboard.Delete
56 | keyStroke.keyType == KeyType.Backspace -> Event.Keyboard.Backspace
57 | keyStroke.keyType == KeyType.Enter -> Event.Keyboard.Enter
58 | keyStroke.keyType == KeyType.ReverseTab -> Event.Keyboard.ShiftTab
59 | keyStroke.keyType == KeyType.Tab -> Event.Keyboard.Tab
60 | keyStroke.keyType == KeyType.Escape -> Event.Keyboard.Escape
61 | keyStroke.keyType == KeyType.EOF -> Event.CloseApp
62 | character != null -> Event.Keyboard.Typing.CharTyping(character)
63 | else -> null
64 | }
65 | }
66 |
67 | private fun handlePasteAction(keyStroke: KeyStroke): String? {
68 | val firstChar = keyStroke.realCharacter ?: return null
69 | var moreInput: KeyStroke? = screen.pollInput() ?: return null
70 | val textToPaste = StringBuilder()
71 | while (moreInput != null) {
72 | if (textToPaste.isEmpty()) {
73 | textToPaste.append(firstChar)
74 | }
75 | moreInput.realCharacter?.let { textToPaste.append(it) }
76 | moreInput = screen.pollInput()
77 | }
78 | if (textToPaste.isEmpty()) return null
79 | return textToPaste.toString()
80 | }
81 |
82 | // sometimes some special characters may appear in `KeyStroke`
83 | private val KeyStroke.realCharacter: Char?
84 | get() {
85 | val char = character ?: return null
86 | if (char == '\n') return char
87 | if (char == '\t') return ' '
88 | if (Character.isISOControl(char)) return null
89 | return char
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/screen/ScreenController.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.screen
2 |
3 | import me.darthorimar.rekot.app.AppComponent
4 | import me.darthorimar.rekot.cursor.Cursor
5 |
6 | interface ScreenController : AppComponent {
7 | fun refresh()
8 |
9 | fun fullRefresh()
10 |
11 | val screenSize: ScreenSize
12 |
13 | val cursor: Cursor
14 | }
15 |
16 | data class ScreenSize(val rows: Int, val columns: Int)
17 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/screen/ScreenControllerLanternaImpl.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.screen
2 |
3 | import com.googlecode.lanterna.screen.Screen
4 | import com.googlecode.lanterna.terminal.Terminal
5 | import me.darthorimar.rekot.app.SubscriptionContext
6 | import me.darthorimar.rekot.cursor.Cursor
7 | import me.darthorimar.rekot.events.Event
8 |
9 | class ScreenControllerLanternaImpl(private val terminal: Terminal, private val screen: Screen) : ScreenController {
10 |
11 | context(SubscriptionContext)
12 | override fun performSubscriptions() {
13 | terminal.addResizeListener { _, _ ->
14 | screen.doResizeIfNecessary()
15 | fireEvent(Event.TerminalResized(screenSize))
16 | }
17 | }
18 |
19 | override fun fullRefresh() {
20 | screen.refresh(Screen.RefreshType.COMPLETE)
21 | }
22 |
23 | override fun refresh() {
24 | screen.refresh()
25 | }
26 |
27 | override val cursor: Cursor
28 | get() = Cursor(screen.cursorPosition.row, screen.cursorPosition.column)
29 |
30 | override val screenSize: ScreenSize
31 | get() = ScreenSize(screen.terminalSize.rows, screen.terminalSize.columns)
32 | }
33 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/screen/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.screen
2 |
3 | import com.googlecode.lanterna.screen.Screen
4 | import com.googlecode.lanterna.terminal.Terminal
5 | import me.darthorimar.rekot.editor.renderer.CellViewRenderer
6 | import org.koin.dsl.module
7 |
8 | fun screenModuleFactory(screen: Screen, terminal: Terminal) = module {
9 | single { ScreenControllerLanternaImpl(terminal, screen) }
10 | single { KeyboardInputPoller(screen) }
11 | single { CellViewRenderer(screen) }
12 | single { screen }
13 | single { HackyMacBugFix(screen) }
14 | }
15 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/style/Color.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.style
2 |
3 | import org.jetbrains.kotlin.util.capitalizeDecapitalize.toUpperCaseAsciiOnly
4 |
5 | sealed interface Color {
6 | data class Color256(val index: Int) : Color
7 |
8 | data class ColorRGB(val r: Int, val g: Int, val b: Int) : Color {
9 | override fun toString(): String {
10 | return String.format("#%02X%02X%02X", r, g, b)
11 | }
12 |
13 | companion object {
14 | fun hex(hex: String): ColorRGB {
15 | val cleanedHex = hex.removePrefix("0x").toUpperCaseAsciiOnly()
16 | val intValue = cleanedHex.toLong(16).toInt()
17 | val r = (intValue shr 16) and 0xFF
18 | val g = (intValue shr 8) and 0xFF
19 | val b = intValue and 0xFF
20 | return ColorRGB(r, g, b)
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/style/SimpleStyledLineRenderer.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.style
2 |
3 | object SimpleStyledLineRenderer {
4 | fun renderLine(line: StyledLine, width: Int): String {
5 | var column = 0
6 | val result = StringBuilder()
7 | for ((i, styledString) in line.strings.withIndex()) {
8 | when (styledString) {
9 | is StyledString.Regular -> {
10 | result.append(styledString.text)
11 | column += styledString.text.length
12 | }
13 |
14 | is StyledString.Filled -> {
15 | val remainedLength = line.strings.drop(i + 1).sumOf { it.text.length }
16 | val fillerLength = (width - column - remainedLength).coerceAtLeast(0)
17 | result.append(styledString.text.repeat(fillerLength))
18 | column += fillerLength
19 | }
20 | }
21 | }
22 |
23 | return result.toString()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/style/Style.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.style
2 |
3 | class Style(
4 | val foregroundColor: Color,
5 | val backgroundColor: Color,
6 | val isBold: Boolean,
7 | val isItalic: Boolean,
8 | val isUnderlined: Boolean,
9 | val isBlinking: Boolean,
10 | val isReversed: Boolean,
11 | val isStrikethrough: Boolean,
12 | ) {
13 | @OptIn(ExperimentalStdlibApi::class)
14 | override fun toString(): String {
15 | return buildString {
16 | append("{F=${foregroundColor}, BG=${backgroundColor}")
17 | if (isBold) append(", B")
18 | if (isItalic) append(", I")
19 | if (isUnderlined) append(", U")
20 | if (isBlinking) append(", BL")
21 | if (isReversed) append(", R")
22 | if (isStrikethrough) append(", S")
23 | append("}")
24 | }
25 | }
26 | }
27 |
28 | interface StyleBuilder {
29 | val currentStyle: Style
30 |
31 | fun foregroundColor(color: Color)
32 |
33 | fun backgroundColor(color: Color)
34 |
35 | fun from(style: Style)
36 |
37 | fun with(modifier: StyleModifier)
38 |
39 | fun bold()
40 |
41 | fun italic()
42 |
43 | fun underline()
44 |
45 | fun blink()
46 |
47 | fun reverse()
48 |
49 | fun strikethrough()
50 |
51 | fun noBold()
52 |
53 | fun noItalic()
54 |
55 | fun noUnderline()
56 |
57 | fun noBlink()
58 |
59 | fun noReverse()
60 |
61 | fun noStrikethrough()
62 | }
63 |
64 | class StyleBuilderImpl(initialStyle: Style?) : StyleBuilder {
65 | private lateinit var foregroundColor: Color
66 | private lateinit var backgroundColor: Color
67 | private var isBold: Boolean = false
68 | private var isItalic: Boolean = false
69 | private var isUnderlined: Boolean = false
70 | private var isBlinking: Boolean = false
71 | private var isReversed: Boolean = false
72 | private var isStrikethrough: Boolean = false
73 |
74 | init {
75 | if (initialStyle != null) {
76 | from(initialStyle)
77 | }
78 | }
79 |
80 | override fun from(style: Style) {
81 | foregroundColor = style.foregroundColor
82 | backgroundColor = style.backgroundColor
83 | isBold = style.isBold
84 | isItalic = style.isItalic
85 | isUnderlined = style.isUnderlined
86 | isBlinking = style.isBlinking
87 | isReversed = style.isReversed
88 | isStrikethrough = style.isStrikethrough
89 | }
90 |
91 | override fun with(modifier: StyleModifier) {
92 | modifier.foregroundColor?.let { foregroundColor(it) }
93 | modifier.backgroundColor?.let { backgroundColor(it) }
94 | modifier.isBold?.let { if (it) bold() else noBold() }
95 | modifier.isItalic?.let { if (it) italic() else noItalic() }
96 | modifier.isUnderlined?.let { if (it) underline() else noUnderline() }
97 | modifier.isBlinking?.let { if (it) blink() else noBlink() }
98 | modifier.isReversed?.let { if (it) reverse() else noReverse() }
99 | modifier.isStrikethrough?.let { if (it) strikethrough() else noStrikethrough() }
100 | }
101 |
102 | override val currentStyle: Style
103 | get() = build()
104 |
105 | override fun foregroundColor(color: Color) {
106 | this.foregroundColor = color
107 | }
108 |
109 | override fun backgroundColor(color: Color) {
110 | this.backgroundColor = color
111 | }
112 |
113 | override fun bold() {
114 | this.isBold = true
115 | }
116 |
117 | override fun italic() {
118 | this.isItalic = true
119 | }
120 |
121 | override fun underline() {
122 | this.isUnderlined = true
123 | }
124 |
125 | override fun blink() {
126 | this.isBlinking = true
127 | }
128 |
129 | override fun reverse() {
130 | this.isReversed = true
131 | }
132 |
133 | override fun strikethrough() {
134 | this.isStrikethrough = true
135 | }
136 |
137 | override fun noBold() {
138 | this.isBold = false
139 | }
140 |
141 | override fun noItalic() {
142 | this.isItalic = false
143 | }
144 |
145 | override fun noUnderline() {
146 | this.isUnderlined = false
147 | }
148 |
149 | override fun noBlink() {
150 | this.isBlinking = false
151 | }
152 |
153 | override fun noReverse() {
154 | this.isReversed = false
155 | }
156 |
157 | override fun noStrikethrough() {
158 | this.isStrikethrough = false
159 | }
160 |
161 | fun build(): Style {
162 | return Style(
163 | foregroundColor,
164 | backgroundColor,
165 | isBold,
166 | isItalic,
167 | isUnderlined,
168 | isBlinking,
169 | isReversed,
170 | isStrikethrough,
171 | )
172 | }
173 | }
174 |
175 | fun style(initial: Style? = null, init: StyleBuilder.() -> Unit): Style {
176 | return StyleBuilderImpl(initialStyle = initial).apply(init).build()
177 | }
178 |
179 | fun Style.with(build: StyleBuilder.() -> Unit): Style {
180 | return StyleBuilderImpl(initialStyle = this).apply(build).build()
181 | }
182 |
183 | fun Style.with(modifier: StyleModifier): Style {
184 | return with { with(modifier) }
185 | }
186 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/style/StyleModifier.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.style
2 |
3 | interface StyleModifier {
4 | val foregroundColor: Color?
5 | val backgroundColor: Color?
6 | val isBold: Boolean?
7 | val isItalic: Boolean?
8 | val isUnderlined: Boolean?
9 | val isBlinking: Boolean?
10 | val isReversed: Boolean?
11 | val isStrikethrough: Boolean?
12 |
13 | companion object {
14 | val EMPTY = styleModifier {}
15 | }
16 | }
17 |
18 | interface StyleModifierBuilder : StyleBuilder
19 |
20 | fun styleModifier(init: StyleModifierBuilder.() -> Unit): StyleModifier {
21 | return StyleModifierBuilderImpl().apply(init).build()
22 | }
23 |
24 | private class StyleModifierImpl(
25 | override val foregroundColor: Color?,
26 | override val backgroundColor: Color?,
27 | override val isBold: Boolean?,
28 | override val isItalic: Boolean?,
29 | override val isUnderlined: Boolean?,
30 | override val isBlinking: Boolean?,
31 | override val isReversed: Boolean?,
32 | override val isStrikethrough: Boolean?,
33 | ) : StyleModifier
34 |
35 | private class StyleModifierBuilderImpl : StyleModifierBuilder {
36 | private var foregroundColor: Color? = null
37 | private var backgroundColor: Color? = null
38 | private var isBold: Boolean? = null
39 | private var isItalic: Boolean? = null
40 | private var isUnderlined: Boolean? = null
41 | private var isBlinking: Boolean? = null
42 | private var isReversed: Boolean? = null
43 | private var isStrikethrough: Boolean? = null
44 |
45 | override fun from(style: Style) {
46 | foregroundColor = style.foregroundColor
47 | backgroundColor = style.backgroundColor
48 | isBold = style.isBold
49 | isItalic = style.isItalic
50 | isUnderlined = style.isUnderlined
51 | isBlinking = style.isBlinking
52 | isReversed = style.isReversed
53 | isStrikethrough = style.isStrikethrough
54 | }
55 |
56 | override fun with(modifier: StyleModifier) {
57 | foregroundColor = modifier.foregroundColor ?: foregroundColor
58 | backgroundColor = modifier.backgroundColor ?: backgroundColor
59 | isBold = modifier.isBold ?: isBold
60 | isItalic = modifier.isItalic ?: isItalic
61 | isUnderlined = modifier.isUnderlined ?: isUnderlined
62 | isBlinking = modifier.isBlinking ?: isBlinking
63 | isReversed = modifier.isReversed ?: isReversed
64 | isStrikethrough = modifier.isStrikethrough ?: isStrikethrough
65 | }
66 |
67 | override val currentStyle: Style
68 | get() = style { with(build()) }
69 |
70 | override fun foregroundColor(color: Color) {
71 | this.foregroundColor = color
72 | }
73 |
74 | override fun backgroundColor(color: Color) {
75 | this.backgroundColor = color
76 | }
77 |
78 | override fun bold() {
79 | this.isBold = true
80 | }
81 |
82 | override fun italic() {
83 | this.isItalic = true
84 | }
85 |
86 | override fun underline() {
87 | this.isUnderlined = true
88 | }
89 |
90 | override fun blink() {
91 | this.isBlinking = true
92 | }
93 |
94 | override fun reverse() {
95 | this.isReversed = true
96 | }
97 |
98 | override fun strikethrough() {
99 | this.isStrikethrough = true
100 | }
101 |
102 | override fun noBold() {
103 | this.isBold = false
104 | }
105 |
106 | override fun noItalic() {
107 | this.isItalic = false
108 | }
109 |
110 | override fun noUnderline() {
111 | this.isUnderlined = false
112 | }
113 |
114 | override fun noBlink() {
115 | this.isBlinking = false
116 | }
117 |
118 | override fun noReverse() {
119 | this.isReversed = false
120 | }
121 |
122 | override fun noStrikethrough() {
123 | this.isStrikethrough = false
124 | }
125 |
126 | fun build(): StyleModifier =
127 | StyleModifierImpl(
128 | foregroundColor = foregroundColor,
129 | backgroundColor = backgroundColor,
130 | isBold = isBold,
131 | isItalic = isItalic,
132 | isUnderlined = isUnderlined,
133 | isBlinking = isBlinking,
134 | isReversed = isReversed,
135 | isStrikethrough = isStrikethrough,
136 | )
137 | }
138 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/style/StyleRenderer.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.style
2 |
3 | import com.googlecode.lanterna.SGR
4 | import com.googlecode.lanterna.TextCharacter
5 | import com.googlecode.lanterna.TextColor
6 | import com.googlecode.lanterna.graphics.StyleSet
7 | import com.googlecode.lanterna.graphics.TextGraphics
8 | import me.darthorimar.rekot.app.AppComponent
9 |
10 | class StyleRenderer : AppComponent {
11 | fun render(graphics: TextGraphics, text: StyledText) {
12 | for ((row, line) in text.lines.withIndex()) {
13 | renderString(line, graphics, row)
14 | }
15 | }
16 |
17 | private fun renderString(line: StyledLine, graphics: TextGraphics, row: Int) {
18 | var column = 0
19 | for ((i, styledString) in line.strings.withIndex()) {
20 | graphics.setStyleFrom(styledString.style.toLanternaStyle())
21 | when (styledString) {
22 | is StyledString.Regular -> {
23 | graphics.putString(column, row, styledString.text)
24 | column += styledString.text.length
25 | }
26 |
27 | is StyledString.Filled -> {
28 | val remainedLength = line.strings.drop(i + 1).sumOf { it.text.length }
29 | val fillerLength = (graphics.size.columns - column - remainedLength).coerceAtLeast(0)
30 | graphics.putString(column, row, styledString.text.repeat(fillerLength))
31 | column += fillerLength
32 | }
33 | }
34 | }
35 | graphics.setStyleFrom(line.finalStyle.toLanternaStyle())
36 | graphics.putString(column, row, " ".repeat((graphics.size.columns - column).coerceAtLeast(0)))
37 |
38 | for (glaze in line.styleGlazes) {
39 | for (column in glaze.start..glaze.end.coerceAtMost(graphics.size.columns - 1)) {
40 | val char = graphics.getCharacter(column, row)
41 | graphics.setCharacter(column, row, char.withModifier(glaze.modifier))
42 | }
43 | }
44 | }
45 |
46 | private fun TextCharacter.withModifier(modifier: StyleModifier): TextCharacter {
47 | var r = this
48 | modifier.backgroundColor?.let { r = r.withBackgroundColor(it.toLanternaColor()) }
49 | modifier.foregroundColor?.let { r = r.withForegroundColor(it.toLanternaColor()) }
50 |
51 | modifier.isBold?.let { r = if (it) r.withModifier(SGR.BOLD) else r.withoutModifier(SGR.BOLD) }
52 | modifier.isItalic?.let { r = if (it) r.withModifier(SGR.ITALIC) else r.withoutModifier(SGR.ITALIC) }
53 | modifier.isUnderlined?.let { r = if (it) r.withModifier(SGR.UNDERLINE) else r.withoutModifier(SGR.UNDERLINE) }
54 | modifier.isBlinking?.let { r = if (it) r.withModifier(SGR.BLINK) else r.withoutModifier(SGR.BLINK) }
55 | modifier.isReversed?.let { r = if (it) r.withModifier(SGR.REVERSE) else r.withoutModifier(SGR.REVERSE) }
56 | modifier.isStrikethrough?.let {
57 | r = if (it) r.withModifier(SGR.CROSSED_OUT) else r.withoutModifier(SGR.CROSSED_OUT)
58 | }
59 | return r
60 | }
61 |
62 | private fun Style.toLanternaStyle(): StyleSet<*> =
63 | StyleSet.Set().apply {
64 | foregroundColor = this@toLanternaStyle.foregroundColor.toLanternaColor()
65 | backgroundColor = this@toLanternaStyle.backgroundColor.toLanternaColor()
66 |
67 | if (isBold) enableModifiers(SGR.BOLD)
68 | if (isItalic) enableModifiers(SGR.ITALIC)
69 | if (isUnderlined) enableModifiers(SGR.UNDERLINE)
70 | if (isBlinking) enableModifiers(SGR.BLINK)
71 | if (isReversed) enableModifiers(SGR.REVERSE)
72 | if (isStrikethrough) enableModifiers(SGR.CROSSED_OUT)
73 | }
74 |
75 | private fun Color.toLanternaColor(): TextColor {
76 | return when (this) {
77 | is Color.Color256 -> TextColor.Indexed(index)
78 | is Color.ColorRGB -> TextColor.RGB(r, g, b)
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/style/StyledLine.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.style
2 |
3 | interface StyledLineBuilder : StyleBuilder {
4 | fun gap(gap: Int)
5 |
6 | fun string(text: String)
7 |
8 | fun fill(text: String)
9 |
10 | fun styled(init: StyledLineBuilder.() -> Unit)
11 |
12 | fun styled(style: Style, init: StyledLineBuilder.() -> Unit)
13 |
14 | fun glaze(modifier: StyleModifier, start: Int, end: Int)
15 | }
16 |
17 | class StyleGlaze(val modifier: StyleModifier, val start: Int, val end: Int)
18 |
19 | sealed interface StyledString {
20 | val text: String
21 | val style: Style
22 |
23 | class Regular(override val text: String, override val style: Style) : StyledString {
24 | override fun toString(): String {
25 | return "R<\"$text\", $style>"
26 | }
27 | }
28 |
29 | class Filled(override val text: String, override val style: Style) : StyledString {
30 | override fun toString(): String {
31 | return "F<\"$text\", $style>"
32 | }
33 | }
34 | }
35 |
36 | fun styledLine(init: StyledLineBuilder.() -> Unit): StyledLine {
37 | return StyledLineBuilderImpl(style = null, strings = emptyList()).apply(init).buildLine()
38 | }
39 |
40 | class StyledLine(val strings: List, val styleGlazes: List, val finalStyle: Style) {
41 | override fun toString(): String {
42 | return SimpleStyledLineRenderer.renderLine(this, 80)
43 | }
44 | }
45 |
46 | private class StyledLineBuilderImpl(style: Style?, strings: List) : StyledLineBuilder {
47 | private val strings = strings.toMutableList()
48 | private val styleBuilder = StyleBuilderImpl(style)
49 | private val styleGlazes = mutableListOf()
50 |
51 | override fun foregroundColor(color: Color) = styleBuilder.foregroundColor(color)
52 |
53 | override fun backgroundColor(color: Color) = styleBuilder.backgroundColor(color)
54 |
55 | override fun from(style: Style) = styleBuilder.from(style)
56 |
57 | override fun with(modifier: StyleModifier) = styleBuilder.with(modifier)
58 |
59 | override fun bold() = styleBuilder.bold()
60 |
61 | override fun italic() = styleBuilder.italic()
62 |
63 | override fun underline() = styleBuilder.underline()
64 |
65 | override fun blink() = styleBuilder.blink()
66 |
67 | override fun reverse() = styleBuilder.reverse()
68 |
69 | override fun strikethrough() = styleBuilder.strikethrough()
70 |
71 | override fun noBold() = styleBuilder.noBold()
72 |
73 | override fun noItalic() = styleBuilder.noItalic()
74 |
75 | override fun noUnderline() = styleBuilder.noUnderline()
76 |
77 | override fun noBlink() = styleBuilder.noBlink()
78 |
79 | override fun noReverse() = styleBuilder.noReverse()
80 |
81 | override fun noStrikethrough() = styleBuilder.noStrikethrough()
82 |
83 | override val currentStyle: Style
84 | get() = styleBuilder.build()
85 |
86 | override fun gap(gap: Int) {
87 | string(" ".repeat(gap))
88 | }
89 |
90 | override fun string(text: String) {
91 | strings.add(StyledString.Regular(text, currentStyle))
92 | }
93 |
94 | override fun fill(text: String) {
95 | strings.add(StyledString.Filled(text, currentStyle))
96 | }
97 |
98 | override fun styled(init: StyledLineBuilder.() -> Unit) {
99 | styled(styleBuilder.build(), init)
100 | }
101 |
102 | override fun styled(style: Style, init: StyledLineBuilder.() -> Unit) {
103 | val newLines = StyledLineBuilderImpl(style, emptyList()).apply(init).strings
104 | this.strings += newLines
105 | }
106 |
107 | override fun glaze(modifier: StyleModifier, start: Int, end: Int) {
108 | styleGlazes += StyleGlaze(modifier, start, end)
109 | }
110 |
111 | fun buildLine(): StyledLine {
112 | return StyledLine(strings, styleGlazes, currentStyle)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/style/StyledText.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.style
2 |
3 | class StyledText(val lines: List)
4 |
5 | interface StyledTextBuilder {
6 | fun styledLine(init: StyledLineBuilder.() -> Unit)
7 |
8 | fun with(text: StyledText)
9 |
10 | fun with(lines: List)
11 |
12 | fun fillUp(style: Style, height: Int)
13 | }
14 |
15 | abstract class StyledTextBuilderBaseImpl : StyledTextBuilder {
16 | protected val lines = mutableListOf()
17 |
18 | override fun styledLine(init: StyledLineBuilder.() -> Unit) {
19 | lines.add(me.darthorimar.rekot.style.styledLine(init))
20 | }
21 |
22 | override fun fillUp(style: Style, height: Int) {
23 | if (lines.size >= height) {
24 | return
25 | }
26 | repeat(height - lines.size) { styledLine { from(style) } }
27 | }
28 |
29 | override fun with(text: StyledText) {
30 | lines += text.lines
31 | }
32 |
33 | override fun with(lines: List) {
34 | this.lines += lines
35 | }
36 | }
37 |
38 | class StyledTextBuilderImpl : StyledTextBuilderBaseImpl() {
39 | fun build(): StyledText {
40 | return StyledText(lines)
41 | }
42 | }
43 |
44 | fun styledText(init: StyledTextBuilder.() -> Unit): StyledText {
45 | return StyledTextBuilderImpl().apply(init).build()
46 | }
47 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/style/Styles.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("PropertyName")
2 |
3 | package me.darthorimar.rekot.style
4 |
5 | import me.darthorimar.rekot.app.AppComponent
6 | import me.darthorimar.rekot.config.AppConfig
7 | import me.darthorimar.rekot.config.ColorSpace
8 | import org.koin.core.component.inject
9 |
10 | class Styles : AppComponent {
11 | private val colors: Colors by inject()
12 |
13 | val HELP = style {
14 | foregroundColor(colors.DEFAULT)
15 | backgroundColor(colors.CELL_BG)
16 | }
17 |
18 | val HEADER = style {
19 | backgroundColor(colors.CELL_BG)
20 | foregroundColor(colors.COMMENT)
21 | italic()
22 | }
23 |
24 | val CODE = style {
25 | backgroundColor(colors.EDITOR_BG)
26 | foregroundColor(colors.DEFAULT)
27 | }
28 |
29 | val EMPTY = style {
30 | foregroundColor(colors.DEFAULT)
31 | backgroundColor(colors.SEPARATOR_BG)
32 | }
33 |
34 | val KEYWORD = CODE.with { foregroundColor(colors.KEYWORD) }
35 |
36 | val STRING = CODE.with { foregroundColor(colors.STRING) }
37 |
38 | val STRING_TEMPLATE_ENTRY = CODE.with { foregroundColor(colors.STRING_TEMPLATE_ENTRY) }
39 |
40 | val EXTENSION_FUNCTION_CALL = CODE.with { foregroundColor(colors.FUNCTION) }
41 |
42 | val TOP_LEVEL = styleModifier { italic() }
43 |
44 | val BACKING_FIELD_REFERENCE =
45 | CODE.with {
46 | bold()
47 | underline()
48 | }
49 |
50 | val PROPERTY = CODE.with { foregroundColor(colors.PROPERTY) }
51 |
52 | val NUMBER = CODE.with { foregroundColor(colors.NUMBER) }
53 |
54 | val COMMENT =
55 | CODE.with {
56 | foregroundColor(colors.COMMENT)
57 | italic()
58 | }
59 |
60 | val SOUT = style {
61 | foregroundColor(colors.DEFAULT)
62 | backgroundColor(colors.SOUT_BG)
63 | italic()
64 | }
65 |
66 | val COMPLETION = style {
67 | backgroundColor(colors.COMPLETION_POPUP_BG)
68 | foregroundColor(colors.DEFAULT)
69 | }
70 |
71 | val COMPLETION_SELECTED = styleModifier { backgroundColor(colors.COMPLETION_SELECTED_POPUP_BG) }
72 |
73 | val MUTABLE = styleModifier { italic() }
74 |
75 | val ERROR = styleModifier { foregroundColor(colors.ERROR) }
76 | }
77 |
78 | class Colors : AppComponent {
79 | private val config: AppConfig by inject()
80 |
81 | val CELL_BG = color("0x000000", 0)
82 | val EDITOR_BG = color("0x1E1E22", 234)
83 | val SEPARATOR_BG = color("0x000000", 0)
84 | val ERROR_BG = color("0x6d353c", 131)
85 | val COMPLETION_POPUP_BG = color("0x2b2d30", 236)
86 | val COMPLETION_SELECTED_POPUP_BG = color("0x43454a", 238)
87 | val SOUT_BG = EDITOR_BG
88 |
89 | val DEFAULT = color("0xBCBEC4", 250)
90 | val COMMENT = color("0x7A7E85", 243)
91 | val LINE_NUMBER = color("0x3D3D42", 238)
92 | val KEYWORD = color("0xCF8E6D", 179)
93 | val STRING = color("0x6AAB73", 108)
94 | val STRING_TEMPLATE_ENTRY = color("0xCF8E6D", 179)
95 | val NUMBER = color("0x2AACB8", 38)
96 | val ERROR = color("0x6d353c", 131)
97 |
98 | val FUNCTION = color("0x57AAF7", 75)
99 | val PROPERTY = color("0xC77DBB", 176)
100 | val CLASS = color("0x6CC24A", 113)
101 | val LOCAL_VARIABLE = color("0xF5A623", 214)
102 |
103 | private fun color(hex: String, index: Int): Color {
104 | return when (config.colorSpace) {
105 | ColorSpace.Xterm256 -> Color.Color256(index)
106 | ColorSpace.RGB -> Color.ColorRGB.hex(hex)
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/style/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.style
2 |
3 | import org.koin.dsl.module
4 |
5 | val styleModule = module {
6 | single { StyleRenderer() }
7 | single { Styles() }
8 | single { Colors() }
9 | }
10 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/updates/UpdatesChecker.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.updates
2 |
3 | import kotlinx.serialization.json.Json
4 | import kotlinx.serialization.json.jsonObject
5 | import kotlinx.serialization.json.jsonPrimitive
6 | import me.darthorimar.rekot.app.App
7 | import me.darthorimar.rekot.app.AppComponent
8 | import me.darthorimar.rekot.app.SubscriptionContext
9 | import me.darthorimar.rekot.app.subscribe
10 | import me.darthorimar.rekot.config.APP_GITHUB_REPO
11 | import me.darthorimar.rekot.config.APP_VERSION
12 | import me.darthorimar.rekot.config.AppConfig
13 | import me.darthorimar.rekot.events.Event
14 | import me.darthorimar.rekot.logging.error
15 | import me.darthorimar.rekot.logging.logger
16 | import org.koin.core.component.inject
17 | import java.net.URI
18 | import java.net.http.HttpClient
19 | import java.net.http.HttpRequest
20 | import java.net.http.HttpResponse
21 | import java.util.logging.Level
22 | import kotlin.concurrent.thread
23 | import kotlin.io.path.*
24 |
25 | private val logger = logger()
26 |
27 | class UpdatesChecker : AppComponent {
28 | private val config: AppConfig by inject()
29 |
30 | context(SubscriptionContext)
31 | override fun performSubscriptions() {
32 | subscribe { scheduleUpdateCheck() }
33 | }
34 |
35 | private fun scheduleUpdateCheck() {
36 | if ((config.appDir / DO_NO_UPDATE_FILE).exists()) return
37 | thread(isDaemon = true) {
38 | try {
39 | val latestVersion = getLatestRelease() ?: return@thread
40 | if (latestVersion == APP_VERSION) return@thread
41 | if (versionIsIgnored(latestVersion)) return@thread
42 | markThatUpdateIsNeeded()
43 | } catch (e: Exception) {
44 | logger.error("Error while checking for app updates", e)
45 | }
46 | }
47 | }
48 |
49 | private fun markThatUpdateIsNeeded() {
50 | val updateFile = config.appDir / UPDATE_FILENAME
51 | if (!updateFile.exists()) {
52 | updateFile.createFile()
53 | }
54 | }
55 |
56 | private fun versionIsIgnored(version: String): Boolean {
57 | val ignoredVersionsFile = config.appDir / IGNORED_VERSIONS_FILENAME
58 | if (!ignoredVersionsFile.isRegularFile()) return false
59 | return ignoredVersionsFile.readLines().any { it.trim() == version }
60 | }
61 |
62 | private fun getLatestRelease(): String? {
63 | val url = "https://api.github.com/repos/$APP_GITHUB_REPO/releases/latest"
64 | val client = HttpClient.newHttpClient()
65 | val request =
66 | HttpRequest.newBuilder()
67 | .uri(URI.create(url))
68 | .header("Accept", "application/vnd.github.v3+json")
69 | .GET()
70 | .build()
71 |
72 | val response = client.send(request, HttpResponse.BodyHandlers.ofString())
73 | if (response.statusCode() == 200) {
74 | val jsonResponse = Json.parseToJsonElement(response.body()).jsonObject
75 | return jsonResponse["tag_name"]?.jsonPrimitive?.content
76 | } else {
77 | logger.log(Level.WARNING, "Error while checking for app updates", response.statusCode())
78 | return null
79 | }
80 | }
81 |
82 | companion object {
83 | private const val IGNORED_VERSIONS_FILENAME = "IGNORED_VERSIONS"
84 | private const val UPDATE_FILENAME = "UPDATE"
85 | private const val DO_NO_UPDATE_FILE = "DO_NOT_UPDATE"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/util/Scroller.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.util
2 |
3 | class Scroller(private var height: Int) {
4 | private var _viewPosition = 0
5 |
6 | val viewPosition: Int
7 | get() = _viewPosition
8 |
9 | fun scroll(cursorPosition: Int): Int {
10 | if (cursorPosition < _viewPosition) {
11 | _viewPosition = cursorPosition
12 | } else if (cursorPosition >= _viewPosition + height) {
13 | _viewPosition = cursorPosition - height + 1
14 | }
15 | return viewPosition
16 | }
17 |
18 | fun resize(newHeight: Int) {
19 | height = newHeight
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/util/collectionUtils.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.util
2 |
3 | fun List.limit(offset: Int, count: Int): List {
4 | return subList(offset.coerceAtLeast(0), offset.coerceAtLeast(0) + count.coerceAtMost(size))
5 | }
6 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/util/functions.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.util
2 |
3 | @Suppress("UNCHECKED_CAST") fun id(): (T) -> T = _id as (T) -> T
4 |
5 | private val _id = { a: Any? -> a }
6 |
--------------------------------------------------------------------------------
/app/main/me/darthorimar/rekot/util/result.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.util
2 |
3 | fun Result.retry(f: (Throwable) -> T): Result =
4 | when {
5 | isSuccess -> this
6 | else -> {
7 | val error = exceptionOrNull()!!
8 | runCatching { f(error) }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/test/me/darthorimar/rekot/ProjectConfig.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot
2 |
3 | import io.kotest.core.config.AbstractProjectConfig
4 |
5 | object ProjectConfig : AbstractProjectConfig() {
6 | override val parallelism: Int = 1
7 | }
8 |
--------------------------------------------------------------------------------
/app/test/me/darthorimar/rekot/cases/EditorTest.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.cases
2 |
3 | import io.kotest.core.spec.style.FunSpec
4 | import io.kotest.koin.KoinExtension
5 | import io.kotest.matchers.shouldBe
6 | import me.darthorimar.rekot.app.appModule
7 | import me.darthorimar.rekot.infra.AppTest
8 | import me.darthorimar.rekot.mocks.mockModule
9 |
10 | class EditorTest : FunSpec(), AppTest {
11 | init {
12 | initAppTest()
13 | extension(KoinExtension(listOf(appModule, mockModule)))
14 |
15 | context("Typing") {
16 | test("Char typing") {
17 | initCell()
18 | editor.type('a').P()
19 | cellText shouldBe "a"
20 | }
21 | test("String typing") {
22 | initCell()
23 | editor.type("abcde").P()
24 | cellText shouldBe "abcde"
25 | }
26 | test("Multiple typings with caret move") {
27 | initCell()
28 | editor.type("abcde").P()
29 | editor.type('_').P()
30 | repeat(5) { editor.left() }.P()
31 | editor.type("f").P()
32 | repeat(2) { editor.right() }.P()
33 | editor.type("rr").P()
34 | cellText shouldBe "afbcrrde_"
35 | }
36 | }
37 |
38 | context("Enter handler") {
39 | context("Function declaration") {
40 | test("{") {
41 | initCell("fun foo() {")
42 | editor.enter().P()
43 | cellText shouldBe
44 | """
45 | |fun foo() {
46 | | """
47 | .trimMargin()
48 | }
49 |
50 | test("{}") {
51 | initCell("fun foo() {}")
52 | editor.enter().P()
53 | cellText shouldBe
54 | """
55 | |fun foo() {
56 | |
57 | |}
58 | """
59 | .trimMargin()
60 | }
61 | }
62 |
63 | context("If statement") {
64 | test("{") {
65 | initCell(
66 | """
67 | |fun foo() {
68 | | if (true) {
69 | |}"""
70 | .trimMargin())
71 | editor.enter().P()
72 | cellText shouldBe
73 | """
74 | |fun foo() {
75 | | if (true) {
76 | |
77 | |}"""
78 | .trimMargin()
79 | }
80 |
81 | test("{}") {
82 | initCell(
83 | """
84 | |fun foo() {
85 | | if (true) {}
86 | |}"""
87 | .trimMargin())
88 | editor.enter().P()
89 | cellText shouldBe
90 | """
91 | |fun foo() {
92 | | if (true) {
93 | |
94 | | }
95 | |}"""
96 | .trimMargin()
97 | }
98 | }
99 | }
100 |
101 | context("Braces insertion") {
102 | test("fun foo() {}") {
103 | initCell("fun foo() ")
104 | editor.type('{').P()
105 | cellText shouldBe "fun foo() {}"
106 | }
107 |
108 | test("class X {}") {
109 | initCell("class X")
110 | editor.type('{').P()
111 | cellText shouldBe "class X{}"
112 | }
113 |
114 | test("fun x()") {
115 | initCell("fun x")
116 | editor.type('(').P()
117 | cellText shouldBe "fun x()"
118 | }
119 |
120 | test("if ()") {
121 | initCell("if")
122 | editor.type('(').P()
123 | cellText shouldBe "if()"
124 | }
125 |
126 | test("Expr ()") {
127 | initCell("val q = ")
128 | editor.type('(').P()
129 | cellText shouldBe "val q = ()"
130 | }
131 |
132 | test("while ()") {
133 | initCell("while")
134 | editor.type('(').P()
135 | cellText shouldBe "while()"
136 | }
137 |
138 | test("for ()") {
139 | initCell("for")
140 | editor.type('(').P()
141 | cellText shouldBe "for()"
142 | }
143 |
144 | test("try {}") {
145 | initCell("try")
146 | editor.type('{').P()
147 | cellText shouldBe "try{}"
148 | }
149 |
150 | test("when {}") {
151 | initCell("when")
152 | editor.type('{').P()
153 | cellText shouldBe "when{}"
154 | }
155 |
156 | test("foo()") {
157 | initCell("foo")
158 | editor.type('(').P()
159 | cellText shouldBe "foo()"
160 | }
161 |
162 | test("@Annotation()") {
163 | initCell("@Annotation")
164 | editor.type('(').P()
165 | cellText shouldBe "@Annotation()"
166 | }
167 |
168 | test("do {}") {
169 | initCell("do ")
170 | editor.type('{').P()
171 | cellText shouldBe "do {}"
172 | }
173 |
174 | test("companion object") {
175 | initCell("companion object ")
176 | editor.type('{').P()
177 | cellText shouldBe "companion object {}"
178 | }
179 |
180 | test("\"\"") {
181 | initCell("println()")
182 | editor.type('"').P()
183 | cellText shouldBe "println(\"\")"
184 | }
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/app/test/me/darthorimar/rekot/infra/AppTest.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.infra
2 |
3 | import io.kotest.core.TestConfiguration
4 | import me.darthorimar.rekot.app.AppComponent
5 | import me.darthorimar.rekot.cells.Cells
6 | import me.darthorimar.rekot.cursor.Cursor
7 | import me.darthorimar.rekot.editor.Editor
8 | import me.darthorimar.rekot.editor.view.CodeViewGenerator
9 | import me.darthorimar.rekot.events.Event
10 | import me.darthorimar.rekot.events.EventQueue
11 | import me.darthorimar.rekot.projectStructure.ProjectStructure
12 | import org.koin.test.KoinTest
13 | import org.koin.test.get
14 |
15 | interface AppTest : KoinTest {
16 | val editor
17 | get() = get()
18 |
19 | val cells
20 | get() = get()
21 |
22 | val queue
23 | get() = get()
24 |
25 | val focusedCell
26 | get() = editor.focusedCell
27 |
28 | val cellText: String
29 | get() {
30 | val focused = focusedCell
31 | val cursor = focused.cursor
32 | return focused.lines
33 | .mapIndexed { index, line ->
34 | if (index == cursor.row) line.replaceRange(cursor.column, cursor.column, "") else line
35 | }
36 | .joinToString(separator = "\n")
37 | }
38 |
39 | fun TestConfiguration.initAppTest() {
40 | beforeEach {
41 | AppComponent.performSubscriptions()
42 | get().setUserTyped()
43 | }
44 | afterEach { get().shutdown() }
45 | }
46 |
47 | fun fireEvent(event: Event) {
48 | get().fire(event)
49 | }
50 |
51 | @Suppress("TestFunctionName")
52 | fun Any?.P() {
53 | queue.processAllNonBlocking()
54 | }
55 |
56 | fun initCells(vararg texts: String) {
57 | check(cells.cells.isEmpty())
58 | for (text in texts) {
59 | initCell(text)
60 | }
61 | }
62 |
63 | fun initCell(text: String = "") {
64 | editor.navigateToCell(cells.newCell())
65 | updateCellText(text)
66 | }
67 |
68 | fun updateCellText(text: String) {
69 | if (text.indexOf("") < 0) {
70 | error("Caret position not defined")
71 | }
72 |
73 | val lines = mutableListOf()
74 | var cursor: Cursor? = null
75 | for ((row, line) in text.split("\n").withIndex()) {
76 | val caretPosition = line.indexOf("")
77 | if (caretPosition >= 0) {
78 | lines += line.replace("", "")
79 | cursor = Cursor(row = row, column = caretPosition)
80 | } else {
81 | lines += line
82 | }
83 | }
84 | focusedCell.modify {
85 | setLines(lines)
86 | this.cursor.row = cursor!!.row
87 | this.cursor.column = cursor.column
88 | }
89 | queue.processAllNonBlocking()
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/app/test/me/darthorimar/rekot/infra/CellExecuting.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.infra
2 |
3 | import io.kotest.matchers.types.shouldBeTypeOf
4 | import me.darthorimar.rekot.cells.Cell
5 | import me.darthorimar.rekot.events.Event
6 | import me.darthorimar.rekot.execution.CellExecutionState
7 | import me.darthorimar.rekot.execution.CellExecutor
8 | import me.darthorimar.rekot.execution.ExecutionResult
9 | import org.koin.test.get
10 |
11 | interface CellExecuting : AppTest {
12 | val cellExecutor
13 | get() = get()
14 |
15 | fun executeCell(cell: Cell): CellExecutionState {
16 | cellExecutor.execute(cell)
17 | return awaitCellExecution()
18 | }
19 |
20 | fun awaitCellExecution(): CellExecutionState {
21 | var event: Event?
22 | while (true) {
23 | Thread.sleep(10)
24 | event = queue.processFirstNonBlocking() ?: continue
25 | if (event is Event.CellExecutionStateChanged) {
26 | when (val state = event.state) {
27 | is CellExecutionState.Error -> {
28 | queue.processAllNonBlocking()
29 | return state
30 | }
31 |
32 | is CellExecutionState.Executed -> {
33 | queue.processAllNonBlocking()
34 | return state
35 | }
36 |
37 | CellExecutionState.Executing -> continue
38 | }
39 | }
40 | }
41 | }
42 |
43 | fun executeAllCells(): List {
44 | val results: MutableList = mutableListOf()
45 | for (cell in cells.cells) {
46 | results += executeCell(cell)
47 | }
48 | return results
49 | }
50 |
51 | fun executeFocussedCell(): CellExecutionState {
52 | return executeCell(focusedCell)
53 | }
54 |
55 | val CellExecutionState.result: ExecutionResult
56 | get() {
57 | this.shouldBeTypeOf()
58 | return this.result
59 | }
60 |
61 | val CellExecutionState.sout: String?
62 | get() = result.sout
63 |
64 | val CellExecutionState.error: String
65 | get() {
66 | this.shouldBeTypeOf()
67 | return this.errorMessage
68 | }
69 |
70 | val CellExecutionState.resultValue: Any?
71 | get() {
72 | val result = result
73 | result.shouldBeTypeOf()
74 | return result.value
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/test/me/darthorimar/rekot/mocks/TestConfigFactory.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.mocks
2 |
3 | import java.nio.file.Files
4 | import java.nio.file.Path
5 | import java.nio.file.Paths
6 | import kotlin.io.path.createDirectories
7 | import kotlin.io.path.div
8 | import me.darthorimar.rekot.config.APP_NAME
9 | import me.darthorimar.rekot.config.AppConfig
10 | import me.darthorimar.rekot.config.ColorSpace
11 |
12 | object TestConfigFactory {
13 | fun createTestConfig(): AppConfig {
14 | val appDir = Files.createTempDirectory("${APP_NAME}_test")
15 | return AppConfig(
16 | appDir = appDir,
17 | logsDir = (appDir / "logs").createDirectories(),
18 | tmpDir = (appDir / "tmp").createDirectories(),
19 | stdlibPath = getStdlibPath(),
20 | javaHome = Paths.get(System.getProperty("java.home")),
21 | colorSpace = ColorSpace.RGB,
22 | tabSize = 2,
23 | hackyMacFix = false,
24 | )
25 | .also { it.init() }
26 | }
27 |
28 | private fun getStdlibPath(): Path {
29 | val kotlinStdlib = Sequence::class.java.protectionDomain.codeSource.location
30 |
31 | return Paths.get(kotlinStdlib.file).toAbsolutePath().also { println("Found stdlib for tests at $it") }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/test/me/darthorimar/rekot/mocks/TestScreenController.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.mocks
2 |
3 | import me.darthorimar.rekot.cursor.Cursor
4 | import me.darthorimar.rekot.screen.ScreenController
5 | import me.darthorimar.rekot.screen.ScreenSize
6 |
7 | class TestScreenController : ScreenController {
8 | override fun refresh() {}
9 |
10 | override fun fullRefresh() {}
11 |
12 | override val screenSize: ScreenSize = ScreenSize(40, 80)
13 |
14 | override val cursor: Cursor
15 | get() = Cursor.zero()
16 | }
17 |
--------------------------------------------------------------------------------
/app/test/me/darthorimar/rekot/mocks/di.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.mocks
2 |
3 | import me.darthorimar.rekot.analysis.CompilerErrorInterceptor
4 | import me.darthorimar.rekot.analysis.LoggingErrorInterceptor
5 | import me.darthorimar.rekot.config.AppConfig
6 | import me.darthorimar.rekot.screen.ScreenController
7 | import org.koin.dsl.module
8 |
9 | val mockModule = module {
10 | single { TestScreenController() }
11 | single { TestConfigFactory.createTestConfig() }
12 | single { LoggingErrorInterceptor() }
13 | }
14 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | allprojects { repositories { mavenCentral() } }
--------------------------------------------------------------------------------
/config/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm") version "2.1.0"
3 | kotlin("plugin.serialization") version "2.1.0"
4 | id("com.ncorti.ktfmt.gradle") version "0.21.0"
5 | }
6 |
7 | dependencies {
8 | }
9 |
10 | sourceSets {
11 | main { kotlin.setSrcDirs(listOf("main")) }
12 | test { kotlin.setSrcDirs(listOf("test")) }
13 | }
14 |
15 | ktfmt {
16 | kotlinLangStyle()
17 | manageTrailingCommas = false
18 | maxWidth = 120
19 | }
20 |
--------------------------------------------------------------------------------
/config/main/me/darthorimar/rekot/config/AppConfig.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.config
2 |
3 | import java.nio.file.Path
4 | import kotlin.io.path.absolutePathString
5 | import kotlin.io.path.createDirectories
6 |
7 | class AppConfig(
8 | val appDir: Path,
9 | val logsDir: Path,
10 | val tmpDir: Path,
11 | val stdlibPath: Path,
12 | val javaHome: Path,
13 | val tabSize: Int,
14 | val colorSpace: ColorSpace,
15 | val hackyMacFix: Boolean,
16 | ) {
17 | val completionPopupHeight = 10
18 | val completionPopupMinWidth = 30
19 |
20 | fun init() {
21 | logsDir.createDirectories()
22 | System.setProperty(LOG_DIR_PROPERTY, logsDir.absolutePathString())
23 | }
24 | }
25 |
26 | enum class ColorSpace {
27 | RGB,
28 | Xterm256
29 | }
30 |
--------------------------------------------------------------------------------
/config/main/me/darthorimar/rekot/config/ConfigFactory.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.config
2 |
3 | import java.nio.file.Files
4 | import java.nio.file.Path
5 | import java.nio.file.Paths
6 | import kotlin.io.path.createDirectories
7 | import kotlin.io.path.div
8 | import kotlin.io.path.isDirectory
9 | import kotlin.io.path.isRegularFile
10 | import me.darthorimar.rekot.config.persistent.PersistentConfigFactory
11 |
12 | object ConfigFactory {
13 | fun createConfig(): AppConfig {
14 | val appDir = getDefaultAppDirectory().createDirectories()
15 | val persistentConfig = PersistentConfigFactory.readOrCreateDefault(appDir)
16 | val javaHome =
17 | (persistentConfig.javaHome?.let(Paths::get) ?: getJavaHome()).also { home ->
18 | if (home == null) {
19 | error(
20 | "Java home is not provided. " +
21 | "Please set `java.home` environment variable or pass set it in the ${PersistentConfigFactory.getConfigFilePath(appDir)}.")
22 | }
23 | if (!home.isDirectory()) {
24 | error("$home is not a valid Java home")
25 | }
26 | }!!
27 |
28 | val stdlibPath =
29 | persistentConfig.stdlibPath.let(Paths::get).also { path ->
30 | if (!path.isRegularFile()) {
31 | error("Invalid stdlib path, $path is not a file")
32 | }
33 | }
34 |
35 | val logsDir = (appDir / "logs").createDirectories()
36 | val tmpDir = Files.createTempDirectory(APP_NAME_LOWERCASE)
37 | val tabSize = persistentConfig.tabSize
38 | return AppConfig(
39 | appDir = appDir,
40 | logsDir = logsDir,
41 | tmpDir = tmpDir,
42 | stdlibPath = stdlibPath,
43 | javaHome = javaHome,
44 | tabSize = tabSize,
45 | colorSpace = if (persistentConfig.rgbColors) ColorSpace.RGB else ColorSpace.Xterm256,
46 | hackyMacFix = persistentConfig.hackyMacFix,
47 | )
48 | }
49 |
50 | private fun getJavaHome(): Path? {
51 | val path = System.getProperty("java.home") ?: return null
52 | return Paths.get(path)
53 | }
54 |
55 | fun getDefaultAppDirectory(): Path {
56 | val osName = System.getProperty("os.name").lowercase()
57 |
58 | return when {
59 | osName.contains("win") -> {
60 | val appData = System.getenv("APPDATA") ?: System.getProperty("user.home")
61 | Paths.get(appData, APP_NAME_LOWERCASE)
62 | }
63 |
64 | osName.contains("nix") || osName.contains("nux") || osName.contains("mac") -> {
65 | val xdgConfigHome =
66 | System.getenv("XDG_CONFIG_HOME") ?: Paths.get(System.getProperty("user.home"), ".config").toString()
67 | Paths.get(xdgConfigHome, APP_NAME_LOWERCASE)
68 | }
69 |
70 | else -> {
71 | Paths.get(System.getProperty("user.home"), ".$APP_NAME_LOWERCASE")
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/config/main/me/darthorimar/rekot/config/appConsts.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.config
2 |
3 | const val APP_VERSION = "0.1.4"
4 | const val APP_GITHUB_REPO = "darthorimar/rekot"
5 | const val APP_NAME = "ReKot"
6 | const val APP_NAME_LOWERCASE = "rekot"
7 | const val LOG_DIR_PROPERTY = "LOG_DIR"
8 | const val CAT_EMOJI = "😸"
9 |
10 | val APP_LOGO =
11 | """
12 | 8888888b. 888 d8P 888
13 | 888 Y88b 888 d8P 888
14 | 888 888 888 d8P 888
15 | 888 d88P .d88b. 888d88K .d88b. 888888
16 | 8888888P" d8P Y8b 8888888b d88""88b 888
17 | 888 T88b 88888888 888 Y88b 888 888 888
18 | 888 T88b Y8b. 888 Y88b Y88..88P Y88b.
19 | 888 T88b "Y8888 888 Y88b "Y88P" "Y888
20 | """
21 | .trimIndent()
22 |
23 | val LOGO_COLORS =
24 | listOf(
25 | "8164FC",
26 | "20A6FD",
27 | "5CE7FC",
28 | "B4FEFF",
29 | )
30 |
--------------------------------------------------------------------------------
/config/main/me/darthorimar/rekot/config/persistent/DefaultConfigFactory.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.config.persistent
2 |
3 | import java.nio.file.Path
4 | import kotlin.io.path.absolutePathString
5 | import me.darthorimar.rekot.config.APP_LOGO
6 | import me.darthorimar.rekot.config.APP_NAME
7 | import me.darthorimar.rekot.config.CAT_EMOJI
8 | import me.darthorimar.rekot.config.ColorSpace
9 | import me.darthorimar.rekot.config.stdlib.KotlinStdLibDownloader
10 |
11 | internal object DefaultConfigFactory {
12 | fun createDefaultConfig(appDir: Path, configFile: Path): PersistentConfig {
13 | clear()
14 | printlnColor(green, APP_LOGO.trimIndent())
15 | println()
16 | println("$CAT_EMOJI Welcome to $APP_NAME")
17 | println()
18 | val rgbColors = chooseColorSpace(configFile) == ColorSpace.RGB
19 | println()
20 | val stdlibPath = KotlinStdLibDownloader.download(appDir)
21 | println()
22 | val tabSize = 2
23 |
24 | return PersistentConfig(
25 | stdlibPath = stdlibPath.absolutePathString(),
26 | javaHome = null,
27 | tabSize = tabSize,
28 | rgbColors = rgbColors,
29 | hackyMacFix = false,
30 | )
31 | }
32 |
33 | private fun chooseColorSpace(configFile: Path): ColorSpace {
34 | printlnColor(
35 | blue,
36 | "Please choose terminal color space.\n" +
37 | "Some terminals (like iTerm2) support the full RGB color space, " +
38 | "while others (like Terminal) support only 256 colors.\n" +
39 | "Choosing RGB if your terminal does not support it will result in broken colors.")
40 | println("${cyan}If colors are off, you can change your choice in ${configFile.toAbsolutePath()}")
41 | val result: ColorSpace
42 | while (true) {
43 | printColor(green, "> Do you want to use RGB color space (y/N)")
44 | val input = readLine()?.trim()?.lowercase()
45 | when (input) {
46 | "n",
47 | "no",
48 | "not" -> {
49 | result = ColorSpace.Xterm256
50 | break
51 | }
52 | "yes",
53 | "y" -> {
54 | result = ColorSpace.RGB
55 | break
56 | }
57 | else -> {
58 | result = ColorSpace.Xterm256
59 | break
60 | }
61 | }
62 | }
63 |
64 | return result
65 | }
66 | }
67 |
68 | private fun clear() {
69 | print("\u001B[H\u001B[2J")
70 | System.out.flush()
71 | }
72 |
73 | fun printColor(color: String, text: String) {
74 | print(color + text + reset)
75 | }
76 |
77 | fun printlnColor(color: String, text: String) {
78 | println(color + text + reset)
79 | }
80 |
81 | private const val reset = "\u001B[0m"
82 | private const val black = "\u001B[30m"
83 | private const val red = "\u001B[31m"
84 | private const val green = "\u001B[32m"
85 | private const val yellow = "\u001B[33m"
86 | private const val blue = "\u001B[34m"
87 | private const val purple = "\u001B[35m"
88 | private const val cyan = "\u001B[36m"
89 | private const val white = "\u001B[37m"
90 |
--------------------------------------------------------------------------------
/config/main/me/darthorimar/rekot/config/persistent/PersistentConfig.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.config.persistent
2 |
3 | internal class PersistentConfig(
4 | val stdlibPath: String,
5 | val javaHome: String?,
6 | val tabSize: Int,
7 | val rgbColors: Boolean,
8 | val hackyMacFix: Boolean,
9 | )
10 |
--------------------------------------------------------------------------------
/config/main/me/darthorimar/rekot/config/persistent/PersistentConfigFactory.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.config.persistent
2 |
3 | import java.nio.file.Path
4 | import java.util.*
5 | import kotlin.io.path.div
6 | import kotlin.io.path.isRegularFile
7 | import kotlin.io.path.reader
8 | import kotlin.io.path.writer
9 |
10 | internal object PersistentConfigFactory {
11 | fun readOrCreateDefault(appDir: Path): PersistentConfig {
12 | val config = getConfigFilePath(appDir)
13 | if (config.isRegularFile()) {
14 | val properties = Properties()
15 | config.reader().use { properties.load(it) }
16 | return deserialize(properties)
17 | } else {
18 | val defaultConfig = DefaultConfigFactory.createDefaultConfig(appDir, config)
19 | val properties = serialize(defaultConfig)
20 | config.writer().use { properties.store(it, null) }
21 | return defaultConfig
22 | }
23 | }
24 |
25 | fun getConfigFilePath(appDir: Path) = appDir / "config.properties"
26 |
27 | private fun serialize(config: PersistentConfig): Properties =
28 | Properties().apply {
29 | setProperty(PersistentConfig::stdlibPath.name, config.stdlibPath)
30 | config.javaHome?.let { javaHome -> setProperty(PersistentConfig::javaHome.name, javaHome) }
31 | setProperty(PersistentConfig::tabSize.name, config.tabSize.toString())
32 | setProperty(PersistentConfig::rgbColors.name, config.rgbColors.toString())
33 | setProperty(PersistentConfig::hackyMacFix.name, config.hackyMacFix.toString())
34 | }
35 |
36 | private fun deserialize(properties: Properties): PersistentConfig =
37 | PersistentConfig(
38 | properties.getProperty(PersistentConfig::stdlibPath.name),
39 | properties.getProperty(PersistentConfig::javaHome.name),
40 | properties.getProperty(PersistentConfig::tabSize.name).toInt(),
41 | properties.getProperty(PersistentConfig::rgbColors.name).toBoolean(),
42 | properties.getProperty(PersistentConfig::hackyMacFix.name)?.toBoolean() ?: false,
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/config/main/me/darthorimar/rekot/config/stdlib/KotlinStdLibDownloader.kt:
--------------------------------------------------------------------------------
1 | package me.darthorimar.rekot.config.stdlib
2 |
3 | import java.net.URI
4 | import java.net.http.HttpClient
5 | import java.net.http.HttpRequest
6 | import java.net.http.HttpResponse
7 | import java.nio.file.Path
8 | import kotlin.io.path.div
9 |
10 | internal object KotlinStdLibDownloader {
11 | fun download(toDir: Path): Path {
12 | println("Downloading kotlin stdlib...")
13 | val fileName = "kotlin-stdlib-$DEFAULT_VERSION.jar"
14 | val url = "https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/$DEFAULT_VERSION/$fileName"
15 |
16 | val client = HttpClient.newHttpClient()
17 | val request = HttpRequest.newBuilder().uri(URI.create(url)).build()
18 | val file = toDir / fileName
19 | val response = client.send(request, HttpResponse.BodyHandlers.ofFile(file))
20 |
21 | if (response.statusCode() == 200) {
22 | println("Downloaded to: $file")
23 | return file
24 | } else {
25 | error("Failed to download file. HTTP Status: " + response.statusCode())
26 | }
27 | }
28 |
29 | private const val DEFAULT_VERSION = "2.1.0"
30 | }
31 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 | kotlin.daemon.jvmargs=-Xmx8000m
3 | org.gradle.jvmargs=-Xmx2g
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darthorimar/rekot/f3bcaf0d0e2f09076f66c8c23bf86ec5657b2745/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Dec 13 05:53:24 CET 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/images/completion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darthorimar/rekot/f3bcaf0d0e2f09076f66c8c23bf86ec5657b2745/images/completion.png
--------------------------------------------------------------------------------
/images/errors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darthorimar/rekot/f3bcaf0d0e2f09076f66c8c23bf86ec5657b2745/images/errors.png
--------------------------------------------------------------------------------
/images/multi_cell.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darthorimar/rekot/f3bcaf0d0e2f09076f66c8c23bf86ec5657b2745/images/multi_cell.png
--------------------------------------------------------------------------------
/images/multi_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darthorimar/rekot/f3bcaf0d0e2f09076f66c8c23bf86ec5657b2745/images/multi_line.png
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | if command -v curl &>/dev/null; then
6 | DOWNLOAD_CMD="curl -L -o"
7 | elif command -v wget &>/dev/null; then
8 | DOWNLOAD_CMD="wget -O"
9 | else
10 | echo "Neither curl nor wget is installed. Please install one of them using your package manager."
11 | exit 1
12 | fi
13 |
14 | GITHUB_REPO="darthorimar/rekot"
15 |
16 | echo "Fetching the latest release information for $GITHUB_REPO..."
17 | if command -v curl &>/dev/null; then
18 | LATEST_RELEASE_INFO=$(curl -s "https://api.github.com/repos/darthorimar/rekot/releases/latest")
19 | else
20 | LATEST_RELEASE_INFO=$(wget -qO- "https://api.github.com/repos/darthorimar/rekot/releases/latest")
21 | fi
22 |
23 | JAR_URL=$(echo "$LATEST_RELEASE_INFO" | grep '"browser_download_url":' | grep -Eo 'https://[^"]+\.jar' | head -n 1)
24 |
25 | if [[ -z "$JAR_URL" ]]; then
26 | echo "No JAR file found in the latest release. Please check the repository."
27 | exit 1
28 | fi
29 |
30 | INSTALL_DIR="$HOME/.local/bin"
31 | mkdir -p "$INSTALL_DIR"
32 |
33 | JAR_NAME="rekot.jar"
34 | echo "Downloading $JAR_URL to $INSTALL_DIR/$JAR_NAME ..."
35 | $DOWNLOAD_CMD "$INSTALL_DIR/$JAR_NAME" "$JAR_URL"
36 |
37 | RUN_SCRIPT_NAME="rekot"
38 | RUN_SCRIPT_PATH="$INSTALL_DIR/$RUN_SCRIPT_NAME"
39 |
40 | cat <"$RUN_SCRIPT_PATH"
41 | #!/bin/bash
42 |
43 | set -e
44 |
45 | APP_DIR=\$(java -jar "$INSTALL_DIR/$JAR_NAME" --app-dir)
46 |
47 | function download() {
48 | if command -v curl &>/dev/null; then
49 | DOWNLOAD_CMD="curl -L -o"
50 | elif command -v wget &>/dev/null; then
51 | DOWNLOAD_CMD="wget -O"
52 | else
53 | echo "Neither curl nor wget is installed. Please install one of them using your package manager to check for updates."
54 | return 1
55 | fi
56 |
57 | echo "Checking for updates..."
58 | if command -v curl &>/dev/null; then
59 | LATEST_RELEASE_INFO=\$(curl -s "https://api.github.com/repos/darthorimar/rekot/releases/latest")
60 | else
61 | LATEST_RELEASE_INFO=\$(wget -qO- "https://api.github.com/repos/darthorimar/rekot/releases/latest")
62 | fi
63 |
64 | CURRENT_VERSION=\$(java -jar "$INSTALL_DIR/$JAR_NAME" --version)
65 |
66 | LATEST_VERSION=\$(echo "\$LATEST_RELEASE_INFO" | grep 'tag_name' | head -n 1 | cut -d '"' -f4)
67 |
68 | if [[ "\$CURRENT_VERSION" != "\$LATEST_VERSION" ]]; then
69 | JAR_URL=\$(echo "\$LATEST_RELEASE_INFO" | grep '"browser_download_url":' | grep -Eo 'https://[^"]+\.jar' | head -n 1)
70 |
71 | if [[ -z "\$JAR_URL" ]]; then
72 | echo "No JAR file found in the latest release. Please check the repository."
73 | return 1
74 | fi
75 |
76 | echo "A new version (\$LATEST_VERSION) of an ReKot is available. The current version is \$CURRENT_VERSION."
77 | echo "Do you want to update? (Y)es/(n)o/(d)o not ask again/(s)kip this version"
78 | read -r response
79 |
80 | case "\$response" in
81 | n)
82 | return 0
83 | ;;
84 | d)
85 | touch "\$APP_DIR/DO_NOT_UPDATE"
86 | echo "You will not be asked again."
87 | return 0
88 | ;;
89 | s)
90 | echo "\$LATEST_VERSION" >> \$APP_DIR/IGNORED_VERSIONS
91 | return 0
92 | ;;
93 | *)
94 | ;;
95 | esac
96 |
97 | INSTALL_DIR="\$HOME/.local/bin"
98 | mkdir -p "\$INSTALL_DIR"
99 |
100 | JAR_NAME="rekot.jar"
101 | echo "Downloading \$JAR_URL to \$INSTALL_DIR/\$JAR_NAME ..."
102 | \$DOWNLOAD_CMD "\$INSTALL_DIR/\$JAR_NAME" "\$JAR_URL"
103 | else
104 | echo "Already up to date."
105 | fi
106 | }
107 |
108 | UPDATE_FILE="\$APP_DIR/UPDATE"
109 |
110 | if [[ -f "\$UPDATE_FILE" ]]; then
111 | if download; then
112 | rm -f "\$UPDATE_FILE"
113 | fi
114 | fi
115 |
116 | java -jar "$INSTALL_DIR/$JAR_NAME" "\$@"
117 | EOF
118 |
119 | chmod +x "$RUN_SCRIPT_PATH"
120 |
121 | if ! echo "$PATH" | grep -q "$INSTALL_DIR"; then
122 | SHELL_PROFILE=""
123 | if [[ "$SHELL" == */zsh ]]; then
124 | SHELL_PROFILE="$HOME/.zshrc"
125 | elif [[ "$SHELL" == */bash ]]; then
126 | SHELL_PROFILE="$HOME/.bashrc"
127 | else
128 | SHELL_PROFILE="$HOME/.profile"
129 | fi
130 |
131 | echo "Adding $INSTALL_DIR to PATH in $SHELL_PROFILE..."
132 | echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >>"$SHELL_PROFILE"
133 | echo "Run the following command or restart your terminal for the changes to take effect:"
134 | echo "source $SHELL_PROFILE"
135 | echo ""
136 | fi
137 |
138 | echo "Installation complete! You can now run the application with '$RUN_SCRIPT_NAME'."
139 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | mavenCentral()
4 | gradlePluginPortal()
5 | mavenLocal()
6 | }
7 | }
8 |
9 | plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" }
10 |
11 | rootProject.name = "rekot"
12 |
13 | include(
14 | "app",
15 | "config",
16 | )
17 |
--------------------------------------------------------------------------------
| | | |