├── .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 | 11 | 16 | 18 | false 19 | true 20 | false 21 | true 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/shadowJar.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 |

2 | ReKot 3 |

4 |

ReKot

5 | 6 | ##### Kotlin REPL with an IDE-like experience in your terminal 7 | 8 | [![GitHub Release](https://img.shields.io/github/v/release/darthorimar/rekot)](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 | Multiline code editing 39 | 40 | ### Multiple Cells 41 | 42 | With results that can be reused between the cells 43 | Multiple cells 44 | 45 | ### Code Completion 46 | 47 | Code completion 48 | 49 | ### In-editor Code Highlighting 50 | In-editor code highlighting 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 | --------------------------------------------------------------------------------