From ed71f82985d9e2b0eadb8c7055db27c29deebe89 Mon Sep 17 00:00:00 2001 From: Daniel Alome Date: Tue, 6 Oct 2026 23:39:47 +0100 Subject: [PATCH 1/2] ADFA-1980 | JVM-Tools: run Java and Kotlin programs with a main-class picker CoGo could not run a plain Java or Kotlin program: Quick Run builds and installs an Android app. JVM-Tools adds, for a folder of .java/.kt files with no Gradle build: - Run: compiles every source on the device (kotlin-compiler-embeddable from the IDE's local Maven repository for Kotlin, then the JDK's javac, so Java and Kotlin can use each other) and execs java on the chosen main class. Output streams to Build Output; Cancel Run stops the JVM. Unsaved Java/Kotlin buffers are written before compiling. - Run configuration: lists every class with a main (Java static main, Kotlin top-level main as its FileNameKt facade, @JvmStatic main in an object or companion) and takes program arguments with shell-style quoting. It is a Material dialog built on the IDE activity and shown through PluginWindows, so it matches the IDE's dialogs. Saved per project in the plugin directory. - A Java/Kotlin Program template. The Gradle actions and Quick Build are hidden in these projects, as in Python-Tools. Requires 26.37 for PluginWindows. Programs cannot read stdin: plugin commands have no input stream. --- README.md | 1 + plugins/JVM-Tools/.gitignore | 29 ++ plugins/JVM-Tools/README.md | 43 +++ plugins/JVM-Tools/addon.json | 15 + plugins/JVM-Tools/build.gradle.kts | 75 +++++ plugins/JVM-Tools/gradle.properties | 4 + plugins/JVM-Tools/jvm-tools.html | 227 +++++++++++++ plugins/JVM-Tools/proguard-rules.pro | 1 + plugins/JVM-Tools/settings.gradle.kts | 30 ++ .../JVM-Tools/src/main/AndroidManifest.xml | 54 +++ .../src/main/assets/docs/css/walkthrough.css | 70 ++++ .../JVM-Tools/src/main/assets/docs/index.html | 71 ++++ .../JVM-Tools/src/main/assets/icon_day.png | Bin 0 -> 8418 bytes .../JVM-Tools/src/main/assets/icon_night.png | Bin 0 -> 8712 bytes .../assets/templates/console/Greeter.java.peb | 5 + .../assets/templates/console/Greeter.kt.peb | 3 + .../assets/templates/console/Main.java.peb | 6 + .../main/assets/templates/console/Main.kt.peb | 4 + .../main/assets/templates/console/README.md | 8 + .../main/assets/templates/console/thumb.png | Bin 0 -> 15729 bytes .../com/appdevforall/jvm/plugin/JvmDomain.kt | 32 ++ .../jvm/plugin/JvmToolsDocumentation.kt | 57 ++++ .../appdevforall/jvm/plugin/JvmToolsPlugin.kt | 310 ++++++++++++++++++ .../jvm/plugin/MainClassFinder.kt | 83 +++++ .../jvm/plugin/ProgramArguments.kt | 44 +++ .../jvm/plugin/RunConfigurationDialog.kt | 112 +++++++ .../jvm/plugin/RunConfigurationStore.kt | 36 ++ .../res/drawable/ic_run_configuration.xml | 9 + .../src/main/res/drawable/ic_run_jvm.xml | 9 + .../jvm/plugin/MainClassFinderTest.kt | 90 +++++ .../jvm/plugin/ProgramArgumentsTest.kt | 30 ++ 31 files changed, 1458 insertions(+) create mode 100644 plugins/JVM-Tools/.gitignore create mode 100644 plugins/JVM-Tools/README.md create mode 100644 plugins/JVM-Tools/addon.json create mode 100644 plugins/JVM-Tools/build.gradle.kts create mode 100644 plugins/JVM-Tools/gradle.properties create mode 100644 plugins/JVM-Tools/jvm-tools.html create mode 100644 plugins/JVM-Tools/proguard-rules.pro create mode 100644 plugins/JVM-Tools/settings.gradle.kts create mode 100644 plugins/JVM-Tools/src/main/AndroidManifest.xml create mode 100644 plugins/JVM-Tools/src/main/assets/docs/css/walkthrough.css create mode 100644 plugins/JVM-Tools/src/main/assets/docs/index.html create mode 100644 plugins/JVM-Tools/src/main/assets/icon_day.png create mode 100644 plugins/JVM-Tools/src/main/assets/icon_night.png create mode 100644 plugins/JVM-Tools/src/main/assets/templates/console/Greeter.java.peb create mode 100644 plugins/JVM-Tools/src/main/assets/templates/console/Greeter.kt.peb create mode 100644 plugins/JVM-Tools/src/main/assets/templates/console/Main.java.peb create mode 100644 plugins/JVM-Tools/src/main/assets/templates/console/Main.kt.peb create mode 100644 plugins/JVM-Tools/src/main/assets/templates/console/README.md create mode 100644 plugins/JVM-Tools/src/main/assets/templates/console/thumb.png create mode 100644 plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/JvmDomain.kt create mode 100644 plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/JvmToolsDocumentation.kt create mode 100644 plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/JvmToolsPlugin.kt create mode 100644 plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/MainClassFinder.kt create mode 100644 plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/ProgramArguments.kt create mode 100644 plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/RunConfigurationDialog.kt create mode 100644 plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/RunConfigurationStore.kt create mode 100644 plugins/JVM-Tools/src/main/res/drawable/ic_run_configuration.xml create mode 100644 plugins/JVM-Tools/src/main/res/drawable/ic_run_jvm.xml create mode 100644 plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/MainClassFinderTest.kt create mode 100644 plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/ProgramArgumentsTest.kt diff --git a/README.md b/README.md index 6e04a418..023c87d8 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego | [`plugins/Get-AI-Models/`](plugins/Get-AI-Models/) | Downloads small language models for on-device AI addons. | | [`plugins/Icons-Repository/`](plugins/Icons-Repository/) | Adds vector icons to a project from inside the editor. | | [`plugins/Jetpack-Compose-Preview/`](plugins/Jetpack-Compose-Preview/) | Renders Compose preview functions on the device. | +| [`plugins/JVM-Tools/`](plugins/JVM-Tools/) | Runs Java and Kotlin programs, with a main-class picker and program arguments. | | [`plugins/Keystore-Generator/`](plugins/Keystore-Generator/) | Creates and manages app signing keystores on the device. | | [`plugins/Layout-Editor/`](plugins/Layout-Editor/) | Edits Android XML layouts by dragging views. | | [`plugins/Markdown-Previewer/`](plugins/Markdown-Previewer/) | Shows a live preview of Markdown and HTML files. | diff --git a/plugins/JVM-Tools/.gitignore b/plugins/JVM-Tools/.gitignore new file mode 100644 index 00000000..6af499b7 --- /dev/null +++ b/plugins/JVM-Tools/.gitignore @@ -0,0 +1,29 @@ +# Gradle +.gradle/ +build/ +gradle-app.setting +!gradle-wrapper.jar +.gradletasknamecache + +# IDE +.idea/ +*.iml +*.ipr +*.iws +.project +.classpath +.settings/ +.kotlin/ + +# Local configuration +local.properties + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Test outputs +test-results/ diff --git a/plugins/JVM-Tools/README.md b/plugins/JVM-Tools/README.md new file mode 100644 index 00000000..7c880657 --- /dev/null +++ b/plugins/JVM-Tools/README.md @@ -0,0 +1,43 @@ +# JVM Tools + +Runs Java and Kotlin programs in Code on the Go, the way a desktop IDE does: choose the main class, +give it program arguments, and tap Run. + +## What it adds + +In a JVM project, a folder of `.java` and `.kt` files with no Gradle build, the editor toolbar gets: + +- **Run** - compiles every Java and Kotlin file in the project on the device and runs the chosen main + class. Compiler errors and the program's output stream into Build Output, and the button becomes + **Cancel Run** while the program runs. +- **Run configuration** - lists every class with a `main` method and takes the program arguments. + The choice is saved for each project. + +The Gradle actions (Quick Run, Quick Build, Sync, Debug, Run Tasks, Launch App) are hidden in a JVM +project. Android projects are left alone. + +The **Java/Kotlin Program** template on the New Project screen creates a two-class program in +`src/`, in the language you choose. + +## How it runs code + +Nothing is downloaded. Java is compiled with the JDK that ships with Code on the Go, and Kotlin with +the `kotlin-compiler-embeddable` already in the IDE's local Maven repository. Classes go to a folder +under `$PREFIX/tmp/jvm-tools`, not into the project. + +## Limits + +- Programs cannot read keyboard input; pass values as program arguments. +- Only the Java class library and the Kotlin standard library are on the classpath. +- Gradle projects are not handled. + +## Build + +```sh +cd plugins/JVM-Tools +../../gradlew assemblePlugin # build/plugin/jvm-tools.cgp +../../gradlew assemblePluginDebug # build/plugin/jvm-tools-debug.cgp +../../gradlew testDebugUnitTest +``` + +Supports Code on the Go 26.37 through 26.99. diff --git a/plugins/JVM-Tools/addon.json b/plugins/JVM-Tools/addon.json new file mode 100644 index 00000000..730d9f3c --- /dev/null +++ b/plugins/JVM-Tools/addon.json @@ -0,0 +1,15 @@ +{ + "summary": "Runs Java and Kotlin programs, with a main-class picker and program arguments.", + "description": "Compiles a folder of Java and Kotlin files on the device and runs the main class you choose, with the arguments you give it. Output appears in Build Output. Adds a Java/Kotlin Program template.", + "tags": [ + "java", + "kotlin", + "templates" + ], + "origin": "appdevforall", + "license": "AGPL-3.0-or-later", + "author": { + "name": "App Dev For All", + "url": "https://www.appdevforall.org" + } +} diff --git a/plugins/JVM-Tools/build.gradle.kts b/plugins/JVM-Tools/build.gradle.kts new file mode 100644 index 00000000..d9cd4185 --- /dev/null +++ b/plugins/JVM-Tools/build.gradle.kts @@ -0,0 +1,75 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.application") + id("com.itsaky.androidide.plugins.build") +} + +pluginBuilder { + pluginName = "jvm-tools" +} + +android { + namespace = "com.appdevforall.jvm.plugin" + compileSdk = 36 + + defaultConfig { + applicationId = "com.appdevforall.jvm.plugin" + minSdk = 26 + targetSdk = 36 + versionCode = 1 + versionName = "1.0.0" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + packaging { + resources { + excludes += setOf( + "META-INF/versions/9/OSGI-INF/MANIFEST.MF", + "META-INF/DEPENDENCIES", + "META-INF/LICENSE", + "META-INF/LICENSE.txt", + "META-INF/NOTICE", + "META-INF/NOTICE.txt" + ) + } + } + + testOptions { + unitTests.isReturnDefaultValues = true + } +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + +dependencies { + compileOnly(files("../../libs/plugin-api.jar")) + compileOnly("com.google.android.material:material:1.12.0") + + implementation("org.jetbrains.kotlin:kotlin-stdlib:2.3.21") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") + + testImplementation("junit:junit:4.13.2") +} + +tasks.matching { + it.name.contains("checkDebugAarMetadata") || + it.name.contains("checkReleaseAarMetadata") +}.configureEach { + enabled = false +} diff --git a/plugins/JVM-Tools/gradle.properties b/plugins/JVM-Tools/gradle.properties new file mode 100644 index 00000000..9ba44411 --- /dev/null +++ b/plugins/JVM-Tools/gradle.properties @@ -0,0 +1,4 @@ +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 diff --git a/plugins/JVM-Tools/jvm-tools.html b/plugins/JVM-Tools/jvm-tools.html new file mode 100644 index 00000000..5e8fac90 --- /dev/null +++ b/plugins/JVM-Tools/jvm-tools.html @@ -0,0 +1,227 @@ + + + + + +JVM Tools + + + + + +
+

JVM Tools

+

Compile and run Java and Kotlin programs on the device, with a main-class picker and program arguments

+
Version 1.0.0 · Author: App Dev For All · Package: com.appdevforall.jvm.plugin
+
+ + + +
+

1. Executive Overview

+

+ JVM Tools runs Java and Kotlin programs the way a desktop IDE does: choose the class whose + main should start, give it program arguments, and tap Run. The project is compiled + on the device with the JDK and Kotlin compiler that Code on the Go already ships, and the output + streams into Build Output. Nothing is downloaded. +

+

+ It applies to JVM projects only: a folder of .java and .kt files with no + Gradle build. Android projects keep their Gradle toolbar untouched. +

+
+ +
+

2. Core Functionality

+ + + + + +
FeatureWhat it does
RunCompiles every Java and Kotlin file in the project and runs the chosen main class with its arguments. Becomes Cancel Run while the program runs.
Run configurationLists every class with a main method and takes the program arguments. Saved per project.
Java/Kotlin ProgramA New Project template with Main and Greeter classes in Java or Kotlin.
+
+ +
+

3. Technical Architecture

+ +

Domain gating

+

+ JvmDomain treats the open project as JVM when its root has no Gradle build file and a + .java or .kt file sits within four folders of it. Every contribution + below is gated on that check, so the plugin is invisible in Android projects. +

+ +

Finding main classes

+

+ MainClassFinder reads the sources with comments removed and reports Java classes with a + static void main(String[]), Kotlin files with a top-level fun main (as the + FileNameKt facade, or the @file:JvmName), and Kotlin objects or companion + objects with a @JvmStatic main. Names are qualified with the file's package. +

+ +

Compiling and running

+

Run is a PluginBuildAction whose shell command:

+
    +
  1. lists the project's sources, skipping build/ and dot folders;
  2. +
  3. compiles all sources with K2JVMCompiler from kotlin-compiler-embeddable in the IDE's local Maven repository, when any .kt file exists (Java files are passed too, so Kotlin can see Java classes);
  4. +
  5. compiles the .java files with the JDK's javac against the Kotlin output;
  6. +
  7. replaces itself with java -cp classes[:kotlin-stdlib] MainClass args..., so cancelling the action stops the program.
  8. +
+

+ Classes are written to a per-project folder under $PREFIX/tmp/jvm-tools, never into + the project. The toolchain paths come from IdeEnvironmentService.getTmpDirectory(), + whose parent is $PREFIX. +

+ +

Hiding the Gradle actions

+

+ toolbarActionsToHide() returns ToolbarActionIds.BUILD_HIDEABLE, and + getHiddenToolbarActionIds() adds Quick Build, which is not in that set. +

+
+ +
+

4. Integration Points

+ + + + + + + + +
APIUsed for
BuildActionExtensionThe Run action and the pre-run save of unsaved Java and Kotlin buffers
UIExtensionThe Run configuration toolbar button and hiding Quick Build
DocumentationExtensionLong-press tooltips and the in-app help page
IdeTemplateServiceThe Java/Kotlin Program template
IdeEditorServicegetModifiedFiles, getFileContent and saveFile before a run
PluginWindowsShowing the Run configuration dialog, a Material dialog built on the IDE activity
+

+ Permissions: filesystem.read, filesystem.write, + system.commands, project.structure. Supports Code on the Go 26.37 through + 26.99; 26.37 is the first release with PluginWindows, which shows the Run configuration + dialog. +

+
+ +
+

5. Usage

+
    +
  1. Install the plugin from the Plugin Manager and restart Code on the Go.
  2. +
  3. Create a project with Java/Kotlin Program on the New Project screen, or open a folder of .java and .kt files.
  4. +
  5. Tap Run. The project compiles and the output appears in Build Output.
  6. +
  7. To run another class or pass arguments, tap Run configuration, choose the main class, enter the arguments, and tap Save. Run uses that choice from then on.
  8. +
+

Build the plugin itself with ../../gradlew assemblePlugin from plugins/JVM-Tools.

+
+ +
+

6. Key Benefits

+ +
+ +
+

7. Limits

+ +
+ + + + + diff --git a/plugins/JVM-Tools/proguard-rules.pro b/plugins/JVM-Tools/proguard-rules.pro new file mode 100644 index 00000000..012c823b --- /dev/null +++ b/plugins/JVM-Tools/proguard-rules.pro @@ -0,0 +1 @@ +-keep class com.appdevforall.jvm.plugin.** { *; } diff --git a/plugins/JVM-Tools/settings.gradle.kts b/plugins/JVM-Tools/settings.gradle.kts new file mode 100644 index 00000000..7a049907 --- /dev/null +++ b/plugins/JVM-Tools/settings.gradle.kts @@ -0,0 +1,30 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath(files("../../libs/plugin-api.jar")) + classpath(files("../../libs/gradle-plugin.jar")) + classpath("com.android.tools.build:gradle:9.3.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.21") + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "jvm-tools" diff --git a/plugins/JVM-Tools/src/main/AndroidManifest.xml b/plugins/JVM-Tools/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c3d241be --- /dev/null +++ b/plugins/JVM-Tools/src/main/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/JVM-Tools/src/main/assets/docs/css/walkthrough.css b/plugins/JVM-Tools/src/main/assets/docs/css/walkthrough.css new file mode 100644 index 00000000..ec3bcf7a --- /dev/null +++ b/plugins/JVM-Tools/src/main/assets/docs/css/walkthrough.css @@ -0,0 +1,70 @@ +/* Tier 3 walkthrough styles - kept tiny so the doc loads instantly even + * on slow devices. Dark-mode friendly via CSS color-scheme + media + * query. No external resources / fonts. + */ + +:root { + color-scheme: light dark; + --bg: #ffffff; + --fg: #1b1b1f; + --muted: #5e5e6c; + --accent: #485d92; + --code-bg: #f5f6fa; + --code-fg: #1b1b1f; + --border: #e3e3ea; +} +@media (prefers-color-scheme: dark) { + :root { + --bg: #121215; + --fg: #e6e1e5; + --muted: #b8b8c0; + --accent: #b1c5ff; + --code-bg: #1a1a1f; + --code-fg: #e6e1e5; + --border: #303038; + } +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: var(--bg); + color: var(--fg); + line-height: 1.55; + padding: 24px 20px 64px; + max-width: 760px; + margin: 0 auto; +} +h1 { font-size: 1.6rem; margin: 0 0 4px; } +h2 { font-size: 1.2rem; margin: 32px 0 8px; color: var(--accent); } +h3 { font-size: 1.05rem; margin: 24px 0 6px; } +p, li { font-size: 0.95rem; } +.lede { color: var(--muted); margin: 0 0 24px; } +a { color: var(--accent); } +ul, ol { padding-left: 20px; } +hr { border: 0; border-top: 1px solid var(--border); margin: 24px 0; } + +pre { + background: var(--code-bg); + color: var(--code-fg); + padding: 12px 14px; + overflow-x: auto; + border-radius: 6px; + border: 1px solid var(--border); + font-size: 0.85rem; + line-height: 1.45; + font-family: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, monospace; +} +code { font-family: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, monospace; font-size: 0.9em; } +.callout { + background: rgba(72, 93, 146, 0.08); + border-left: 3px solid var(--accent); + padding: 10px 14px; + margin: 16px 0; + border-radius: 4px; +} +.muted { color: var(--muted); font-size: 0.85rem; } +table { border-collapse: collapse; width: 100%; margin: 12px 0; } +th, td { border-bottom: 1px solid var(--border); padding: 8px 10px; text-align: left; font-size: 0.9rem; } +th { color: var(--muted); font-weight: 600; } diff --git a/plugins/JVM-Tools/src/main/assets/docs/index.html b/plugins/JVM-Tools/src/main/assets/docs/index.html new file mode 100644 index 00000000..92c821bf --- /dev/null +++ b/plugins/JVM-Tools/src/main/assets/docs/index.html @@ -0,0 +1,71 @@ + + + + + +JVM Tools - Commands + + + + +

JVM Tools

+

Run Java and Kotlin programs on the device, straight from the editor toolbar.

+ +

JVM Tools adds a project template and two toolbar commands. The commands appear only while a +JVM project is open; in an Android project the toolbar is left alone. Because Gradle plays no part +in a JVM project, the Gradle actions (Quick Run, Quick Build, Sync, Debug, Run Tasks, Launch App) +are hidden while you work in one.

+ +
+

A project counts as JVM when it has no Gradle build file (build.gradle, +build.gradle.kts, settings.gradle, or settings.gradle.kts) +and holds at least one .java or .kt file within four folders of its +root.

+
+ +

The commands

+ +

Run

+

Compiles every .java and .kt file in the project, then starts the main +class chosen in Run configuration with its program arguments. Folders named build and +folders whose names start with a dot are skipped. Kotlin is compiled first, so Java and Kotlin +classes can use each other.

+

When no main class has been chosen, Run starts the main class of the file open in the editor, +or else the first main class in alphabetical order.

+

Compiler errors and the program's output stream into Build Output. While the program runs, +the button becomes Cancel Run; tap it to stop the program. A run is stopped after 30 +minutes. Unsaved changes in open Java and Kotlin files are saved before compiling.

+ +

Run configuration

+

Chooses which main class Run starts and the program arguments it passes. The list holds every +class with a main method:

+ +

Program arguments are split on spaces. Wrap an argument in single or double quotes to keep its +spaces, for example --name "Ada Lovelace". The choice is saved for each project.

+ +

The project template

+

Java/Kotlin Program on the New Project screen creates a program in +src/: a Main class that prints a greeting built by a +Greeter class. Choose Java or Kotlin when you create it, then tap Run.

+ +

What it cannot do

+ + +

Turning it off

+

Disable JVM Tools in the Plugin Manager. The two commands disappear, the Gradle actions come +back for every project, and the template is removed from the New Project screen.

+ + + diff --git a/plugins/JVM-Tools/src/main/assets/icon_day.png b/plugins/JVM-Tools/src/main/assets/icon_day.png new file mode 100644 index 0000000000000000000000000000000000000000..7dcdcbc27b8ec8c7a58b967b5f93531541c0a5b3 GIT binary patch literal 8418 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 literal 0 HcmV?d00001 diff --git a/plugins/JVM-Tools/src/main/assets/icon_night.png b/plugins/JVM-Tools/src/main/assets/icon_night.png new file mode 100644 index 0000000000000000000000000000000000000000..74d7bf5f1cb4965184a83098c5ea171211c375bf GIT binary patch literal 8712 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 literal 0 HcmV?d00001 diff --git a/plugins/JVM-Tools/src/main/assets/templates/console/Greeter.java.peb b/plugins/JVM-Tools/src/main/assets/templates/console/Greeter.java.peb new file mode 100644 index 00000000..88f00446 --- /dev/null +++ b/plugins/JVM-Tools/src/main/assets/templates/console/Greeter.java.peb @@ -0,0 +1,5 @@ +public class Greeter { + public static String greet(String name) { + return "Hello, " + name + "! This program runs on your device."; + } +} diff --git a/plugins/JVM-Tools/src/main/assets/templates/console/Greeter.kt.peb b/plugins/JVM-Tools/src/main/assets/templates/console/Greeter.kt.peb new file mode 100644 index 00000000..8b6323a0 --- /dev/null +++ b/plugins/JVM-Tools/src/main/assets/templates/console/Greeter.kt.peb @@ -0,0 +1,3 @@ +object Greeter { + fun greet(name: String) = "Hello, $name! This program runs on your device." +} diff --git a/plugins/JVM-Tools/src/main/assets/templates/console/Main.java.peb b/plugins/JVM-Tools/src/main/assets/templates/console/Main.java.peb new file mode 100644 index 00000000..540236e5 --- /dev/null +++ b/plugins/JVM-Tools/src/main/assets/templates/console/Main.java.peb @@ -0,0 +1,6 @@ +public class Main { + public static void main(String[] args) { + String name = args.length > 0 ? String.join(" ", args) : "world"; + System.out.println(Greeter.greet(name)); + } +} diff --git a/plugins/JVM-Tools/src/main/assets/templates/console/Main.kt.peb b/plugins/JVM-Tools/src/main/assets/templates/console/Main.kt.peb new file mode 100644 index 00000000..84e72b38 --- /dev/null +++ b/plugins/JVM-Tools/src/main/assets/templates/console/Main.kt.peb @@ -0,0 +1,4 @@ +fun main(args: Array) { + val name = if (args.isNotEmpty()) args.joinToString(" ") else "world" + println(Greeter.greet(name)) +} diff --git a/plugins/JVM-Tools/src/main/assets/templates/console/README.md b/plugins/JVM-Tools/src/main/assets/templates/console/README.md new file mode 100644 index 00000000..f5ff2d60 --- /dev/null +++ b/plugins/JVM-Tools/src/main/assets/templates/console/README.md @@ -0,0 +1,8 @@ +# Java/Kotlin program + +A command-line program that runs on your device. + +- Tap **Run** to compile the project and run `Main`. The output appears in Build Output. +- Tap **Run configuration** to choose another main class or to pass program arguments, + for example `Ada Lovelace`. +- Add more classes under `src/`. Every Java and Kotlin file in the project is compiled together. diff --git a/plugins/JVM-Tools/src/main/assets/templates/console/thumb.png b/plugins/JVM-Tools/src/main/assets/templates/console/thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..c781d60618bdd0200f0c4930f82b8c7dce33b200 GIT binary patch literal 15729 zcmeHuXH-+s_vH&9Aiejdfb=3DAV?FWH&Fp;p@@KVsnSAGqzDK|Q#v9ly>|lARFo>c zgd!#M03kp^GSB(V%$hYHXU&>V|9l}?yxjM4&bg=TyLY^)u?{r_2L%8C)OxyFW&l70 zz9j<4Nx?rS!GA6QKt)kcOXFd1?%pD0Zf-#M0Sa+^$8R>L^Qly8I_1^4gsagcrFN#K zHLgRcr|%L5IX~-so?}TodseRUD21$NivN*G`DDVYrW5j}5a|V`q-eTU{;L8IhUZZp zX`REn!|N~dk1te?f9*e!sc-+`mG)wT@B(g^-f46yb6h|mVBxCQ!AJT3^?G1Ah-Fpp zEqMd{?p0fY0~}--%EDv-(@M8A2_Vb8e{6gk^2>esDv42Y!XnoceMFuNksK|7oHO*B zI%Dy3?+97XnKDtzcZ_@&xZbn5RMIP2%pz`YDk=&H>^!voz7L};p%VaHoHszB-kN-ywpHqIzVWD=LOF?Nu2b2)1^x2 zso~_P38raj^I7 zo}g7yR<0ZyyYKDeGa8C4%{p;kw?6M`yxZ+xLei+J9aD1GduagPR$Fiue15b(mB1|K zDAjdVj6Rt#*}NxUfg(8Zo`2V25lGk5Q4?lKBnv0}OoaeRzkRvXGW36ZGWzhm84(c* zo6!w*2!CtU9_V?UCGlK2aMuTm#daz=*thwZ#?#SVd$=(sKe>Rv22l^i4rNLtE(RgO z@6oeQNZI-JGgC^nEJw>KD=UxqaBZGgicjsmSQ5!}cJuDRgcryVX}xoP+~xc@bMpm% zlc%>B69s?2BWfioD(W@tFoqX*F~I4)z&u6k={{W*z!SP#>-eY_x^v^v!4T6yN!0yT z1@AQ|JJD|ah^le~A{>Jp#h<;{%$q((n!qkR_xAPT(&--sCmi>#5|bQy9uu(Ottqtcm>vPh@gIjbegSm9!};~ zrU>AGaSOiXh*JL)%uAgB&>2lgY}5}ONLkkKeM&sLz+!dOu(Ko2ya|_0o`mM6rX~y5 z{=%ei$*kir1a0h<#^|_y$0pA#_?5`d6Ga_&iweD?X(fj0tgWm(@_0r^3w}RUNP6>T z!RPm_>4iGqwSk^egRH>hH*bP9H8nH%iI(K}i88#)5jw=z0XdU3*msFtbc#FQOpQY? zHtomXf`Guls^O>g7tzz&h#;;(dp{L~@Xjl9E=1&d)EMj$BW(=?M0Hj@e&)l`w$#(BleR*!Y3mPs(zV^VqanogFRpjunv5UXI|G~!Z4+zW>rVFz_S#B{h-xlEROr9-C zuA9{CDoql(^||>cqRIm|qI}TN2ga_7|7KngeysRjqSxp(I11%E?aSrzBTcZl`6v zwU*pl5a(u!RTvzF#{!{?Z2>4U-Y}n*g){eFHC0tFqz~ciqwa>+ ztfd2rRZgJ$mnL%fo#|@AOq!rUmy*2v#>VlByv=6wYP(7?tlJsnmoPa~;=S397xi1n zLJ0{8I|GBndgr0c@+EXtP`Ky%&kWlU6^u=nD9J-9nkS-sMC3q{_g59X24LA465zNq zRoROm;4!exzv(9Ul$uR%smFDFR;|yS4|$Pj;_Ue6wnYz!z!h>k&Q=pSh@!bQF$fqz zxvJynXTflR8a(`9sS`|758a8Py+`Ut6=!z27YEc@BrI#XXF>1mx9wC}xB4T;z1NRl zKwn>mhK9xrz(ewBfu|wCqwzxd!nU@y=8~+rEc@_{Th-@fKrlvwyUa>|vfwA^hzciO!{$etKje`VI=sOt#;cg&Zej_^^r*?TV!Ea&X z+fTMJQUFW#Lf3oFi;SETcYA0?P6=9-Z5bDjjg2`%p$JYmcJaV&4uMb`pP&CgK2QHm zorE5^&Oze81=XUMwhP@`?ds|(Z`_26v+OY%D{9Qm&23#0XV=pe1|)c0n|jqB@^u*u zk19Jb0cY{>JS;&~bJ1simF)G^2I68hW2adpZP;L2MX&4Jrz%=2%F0|yy=KQWBUq&D z4A*GwhczR4_&!Y}mXwru1qES#mREOO(1Q8uwTa@&%iyFuy$PeMg%B`sYWJ$!Fy5eN z+Jk4EvjT$r{X29 zo0$3zN2Q-X9tK9K_jH?Zj^UIwnd#%8q8>Vc!vg&k=8Qq3XXl%}rEDV3*Aty;XWRz{ z1}ZGYS!`ydTKFFTK(h8LrYQ-l>8<2Q5Bc)JH*lnc_lqHhyfnkIiAEO4+=D+Z{~{;J z<>zf#Zaiyht>5r%7h7xV`Dh!-&C&@svDDE;MZm}3&9)>Uc}gYZ^!q(lTmQDWzgAXO zKgoz6(VkB$lgIJ-g5!kRg_u;Gb zYuF!Caz&1py5IYltt;>|5y&B=kQ1ppeaVQURl>Qry6PfQnj0EAtVXt*#0@liYA+=q zY(_g=m7xtZ&%C{%{MUbg0sW<(+~)?GGnp9t=2wN6pQ>-ez#Qf~=zH-EpriT$>8>pf zFg}mgKm~UQW$5<@*e=T{L!M1~Dkg)Q!~t0v-t3LAyMtNqV^vJ9*OwYCq6E@^JVDZk z10+6jVnsE{V@f(ZJe4u4sIz!50en!ClY3}bYKGXjQ9p1rK z0+1|N>GyLdS$rq~c}w>bXQu$U91nO|c3{6pKi!A-mZ-s1HDJ)pIj_WpC^(;$>eAxR zec8uk-Q+$0!kWi6xc4nbWbFN$N_r-0cR^=-*(lDDcFsZ&sh3PyPg;;XF;WLhApmUL z?M%KpPvBsJ@P&Ze##Q&x&O28q2bYPdlT#{HL!9n1g(pnJ`|N-;&AhbrdrNsok)9YTDN%s(nSeS)6`^ zwLKuekEM#fyz#+m;#9&!yH##gf2qAWH>pSXuhN?VXxw)or?FJ4^gO(h)MZWlSTyASC2Kb8dwpI+M*uJaE{NY>C=D4cHBQgYjB#S@Ns+FsCc4vp+kOZiMj~No{1ixr{5^>a=hQL4hl2ssy;pzPG!dY&s>do^tulU7h zWW20_s*=QrRzvLrytP{>w!mQfje$PKK@Q*py6xBZo;2~l+N1sc9foUtqxK@j5>?a8 zdvfGUpc}-my~e6*5g<0%tuc2=A>Jl1jxR} z89bl5TBY;1+hRCl_p&)|k%tnKExX{YeOBMMA7^mdW&LE?eKS#^uB?IdRho`+$nL3^o+iV9&57kReL^^jzI$ggk9}fOuuZjj0Sn68#y5TdDG$< zjy4R9v)@u+JEG-)vHg%B-62}&QPIc|-sL0d7AFDNWH+G;$9+%MswppRVsS~EZ5rf< zb;L4pSu*$iB=GL3&>CL~h&z z)F(!;E-*avk0lIDj0Fd|)pD;^IUuN;kg+S(#q*9kkhCy|9vIF2R({SLs4Ct|!2cNk z5#e$CtzrF>aNt~O>U_Xir{9)qK#zme`Cix)H-2Y+UH|3viR>R8tmhBTIU+F*MUM2i%V>O z?vwqC0>}Q_JKQ>E$sNlKy;iXe;u%%!-MYYgN(k`#9>&|c>5bZvcNz?j9KW?XJCVdM ztYa0}K;`yWbdSP*NBG}mMpM}4eyKN)PVmUamG498nML+Yh0-jsz)!~aWdHfM0O6O4 z*^I^kub(VMp|jo)Qx=VO{WYNQ#nzq5xF-i#?D*tmWUGj!L(Y3~Cv{mqpzup9VR1zz z4RIt}o7P7C_O|t}e&I;ocOnwjhQx(Apu@PnIawcnIA98L)#@9gY?Fp2wkoo!S2Id8 zysP|KN2|n}=o9wnkM6bYp zF-r+Quxe^}NJw%XwjP)1VdNrv-ct0xd`px9aew$1wwf|g^p-n=b9j8hJ6iemq zNOiU7br?4N(R9M|`aSOQP!_hf8W0`gqH_l0B+AU$W(MVsLO+9t*i%YpokV_d{PN~e z=54;8pGC&*uw`CxrpmtgZ=pN6r7b(DHOD3gRDQo{e`aRZ{*)opVh4Xh;Y0S&oXg$O zZ}%mhYOt3J`>DSJBjJvREuPkNATkHERJ`;F?N2u82P9y*2X`v~{M&&4>#g_)(q#7X z23VLQZR@T2-lXD??o_Ybg0U}e@PAQlJ@oD38zjkBhyWi$4h?8uK)=-X{CGcwP;z=f zac4b4srBDEtxh}RB}%Uci$dSEF1l2;lH=C5tc7r-k}mtiI`j}gks~_4OCq9&S#9$Q zd$*TS+(f-@?r^DNvlLx>rA5V)c1u*|AyrHQJ!B7U-SoGh)!?}a!K~sXcU~%R*?yca z0F6i%FfWirH5(p*HRbG;FKY00Kj)&*{&#~_>Mp+hLmpIp|2R4*%}Vq3^?cSI5)wJk z`qI-4wSFq7=T&_hQmL5_VAc#$q2+y99ag(lf{y(UWbx)Dt3zjua3(SKTHx&ja5z66 zvS0A~41RuI(Gnx2i+c51u=(8h$K{5O*#Z~_EZo;d=^=Lk=`3=f+onz&MVUV%u{Zu= z%tZh_H`Hf5qI{;>V}p_Fw5n!%LveO)C})yK{Rz-Efa#ny8@_NEVi8NtoWW&XY0i|z zn|7pT31E-m>0`>(%m|6NbvmNRR?s&aCuy}^irkb>mYACj!jQ<)5qDzCRzSK1c?Toc zF!tuCG%4mq%8Uco1eYWcRp^D1(Rkp>ra;$4lFT87HNIudfz43>$fl&PvW+DDo9G|D z{K;(XV8b9OM?Rvd%4Z_WnyLMGP~S~}FSz35Q%c^4tiMA`qIs~xNqZ{|kFVnPRfo@r zHvVS(jVnM4px-A7`i{3=x6|kw6U0r(ohdOdx29{ZPd0!4g`K{f-^STqD3~aeRj1@J z((V^1?a1imhJ89)+yA9)0vIL&z^^NwzZ|b4r=jff9uu7n{-5V7w@?E#UH$sX#1&fZ zaylL1i(NB;qlo8+Mmkz27j11q&qdQAqT#1H(XZcVFxh+6kOJA2_U#2iwO57&x^Rt* z)lK(9#WK9eo_mQn7Aa9VSbsK?2w}HQW@9>oZZTZzzL? zHqP}{erfn_gy~LMI2B3@KYWUkEeTM#V#q`CPzVT~o1E(P+x3Bm#gIh4B#jJp#?^+> zUWT-gj(Z(so^YxP?6F3u-jNo$!@l(vn?+;xye%`Ke%aY9`1F2l+AcZp^q+a&KY9V!+Rz*)mrTu{|;QxI4dqAuQH}p|g4nl{}Y7NhmGi`?!N!OSc}uPZcQf)I3ge;r7OKK;-Or(+|--YpE~OAEYFL#dd?$aUNAC z+aHXi%yoJwofx1O&ybgYr9Q6N7BHinR$Y4YPyChXk`<^SH=!Lx8X_HsrbxeuJ3Kv3 z3(GJTI%d!ZQyt59{!gG#y_>d`3gPYh$-6o8gB=~;mc;bK z&L59oqS{M$7NN{yVO!zmbmD^*vurwc{)-Ap%|NK}WK>J_c0RY7>j=RHuY>z$goE zg8|m6NTHZX=N>yvBn)NxTtt%8q2`aCaUo4>8 z_Cx|hB=R3-%~YzS7v$dc*|ya@brefxY+4%+?1AczOfnB%<;@ra)PjX?m=IN9${#aY z5I3Y52Tyj#hS5cR=L3NH@T*Aw7b8E%$sH|h9=Uq0%g zR(oT+wt5#iz3uyLe}P-5f;NiBJbRWEZy1{JZaE=C*TC^SXO&glmQEU5K+E<90Jv^? zzSF9BsXN4eK%@4k%$lPKZ5OECQDo~N)ZRm^o|H%#5MH>d=#a-kE-5MZ;aMN;oNtg% z*;(sYjWu-B_o`Z+6$bxB#|robhP0x%dGIqk!#>|WPEq{}be7(~`mzLDwEHsr7`u9^ z#=rl)P}i_mlY~ee?W*4I`>n^dJDZhDq#~uCe(V5gxk|mq97Je3^usgFv#98G9R&#l zT%4~`vuOEm-jfzAdc+R5%bYzGr#`+>R$Y8J^##%*^ySmOK z_bl^o6HW^kR#PAmk`hW6=09YAQ_0k2(14&++o@=cDF|I8u_*L`%&AQRO&|HP4kPPO4k)S;&&4JtOTZlia(A- zG@pdw0+AsHMvnc~-&VFll8nBfrk9S-24E~=sCb&`<{f&yVanlGiIhLj|YW?+Rp6P2J!+$@R@~D3p{WCVg1Xg&MSV zyM37l?ze(>v8Q`2mhtaqD#IOvBw55V#TON}&P~S7UiWS&J2qF6RkT zayV{+!IWOzW*X~#-oj)Tti3zsqfwv`Q&^ISwc`5W{X%yj0xhA}dHS5b+4N=u>^m)O zDE7L9yCj8Igo9y6k;8R1l zgZoxBSM3=d0O}Iv!HP-p((BE0F;ubpy~MuT+zAg&hjwvGjj}X zVt*CT{jcT_sM}z2r!HN4`T7JX$}#NUaoSF7S$n?ByCm4#j7x377UbCknR@pV`i|*h zCBu`WH^tS?S=5cdJ%WD6dBhIou9lmIE?^tpsD+gO<$)U=%_}4@IoE9>o_NAK#{d=M zmQ54bjoDD|w`0n1Vf>jo8_}m=;Dn$QVsynoM(zCl6Bz1Nd^JwO?Qv*rL&c@7Z-{ZO zjfpQgx2Fg+t7sMDP|jz{3MLrY72ihWi+`mJRZrG8LT%u%IdQ#zJg`~U^HQ0i$21gq z(2)DF&mvDEu+C_qHtLXy;0BM1E7ze<>|^OPV<<-mqXgacrvYIYcXuwBZqOu@Xa3~v z!LBBL39n#*^I>{jtI^9-r^zEjGnWKmegaAaHK5a4en4%z;6U` zrSaDryw?y-)A4NO@yFxh9f5!;tc&WA8GhEeXD;&V2t*Ow_n+@g=k-sNwt99>7AF~^ zR~x>P>IG4LMEJbw%PJ5d>pBfY4Up>V0CLvkqi>mPZjPqbzQ#Y&hMhbX*&Sa59GYP_ zl3-)3WlmB4gt0sZ>yD?Z+b`2oY9A^17>c)uQx?eDa?Sjikn9!*GxoDNA0e%ykxMh} zX+p|jgVG}D%njpB2Hy+gw6kK1OyeTT1K43;7G=VA9?6`R!>FqQ?3+h?$kcO$#?O~vqPOr~c71w(Xrb$aHq9O^gQYE- z3TC?q4jfHHM8k=ngf)dS4^_T_Hc_M|JrLOjS)K+*?wubaS+qF@af+QndP7JUpXX+{ zjODo#Ad_61(rT*1fT66z_~^rdcXBv#-45>6jGYXMzD95)c-78->yMQ6#0C81 z+tGi%*P3=`J$f87IRMYEL1DC^X=CkGSqo0h`uBsVm~JSzy?d<3eFb!DBWR4a2SAf$ z{B~YlDdS;6X!G~)w5n2TDdEsIwUBnj#i6p>G%XF6QUMpGY1rSFV@p|fc;&rpCTzh~ z8Uj0PH@UEunuE}*`u97<(cSGg7(=y3bogBmGgo}Cwsj}0g-zyv5c(SPo#h_t%b&MXeyP_=mA(|Auz@lkC}c)4XI zYFtB2?x>p{>`Us`t!41vCDv)5AwgW+NuM&kw`*CjQ;cI-`_2J5d9}TZ;5;YxCT;AI!60vFNoY(f{+Edt zc?70#x|*7hYdiXxDSG$Gpg;Vf57gVfJvis+OLS6he&m1-*g)`e)t)}*Uv)msBIx;8fh^HF(yMj z9Uph-WR&V`!6F~O8or~2IMkpM-+Oy>(^7G(jS6@g1GaeVa1ol5T5JFNOQ~F2e&SlY zH%U3zz|PRfho0MZg;$Jw%WtzWt#va575lo+D>0*M>TQzRe;;lQ&1xG>Xiq>eiZKng z0jZrZQyP7JEwG!71jP32;5=Kh|FZ-gzZ#$FT4u8ixs22U5BckrS;#JcIIe$9aJ1OU zE3yl!0~*Qc51j<%(wLwPoO;3Igzhij)j`Av7<|vDd{Jrl!o5JSlsLSLmuG7t@Py_h zhuVFW+}Zq`c#s0@ga&rCLf+RCZEmRZF3u@jl#GDz>`IjCx*e=!OcT#gIf!ir-e`i3 zFpzTNw7E5s_lec|q9bGfHU+mWVU##L;w)=5nJVM-#9i98Z$RxyBih(6?J+(s^fIKBM<+oFuN~{%sXxHmsk!@*pLgrQ2AK=} z?L{${KyMYVoX14}W3(EunVwIgHVhhEMx3QhXl-miZ(5(gov0U}VG<=faN!75sX*(Q z9q+9Hc!SD)sa-~B$lRN%^yMUO@@mSyuNwAEAWl*7xRn01$982fpzU)T^9V+;>BK)? z!b+k)&$|Da(L*Zt=`$Ff31PzdEm)sInK!lQ1EKwI^Bp#?J@Z2DzMN?x zZdQc!B{M^>2xAap>agk;F;L$RLb*D}Gp}q@GF5ZG-mKRz0phcue4PamrgiYMOc11l zcBI;_-~wA6#3Td`DVf}Rl#Ea#cFE{SUHjcvS7b8;W7 zXJf?;;1oF?vNV}4L{ECNnbT?h{NMFTb&x1a(W zamv!-tz}+Wf7&och|kFB==6&QU%x*6kM{(^b#)L4Wo7JpUZeRw(nN1rly7-&yEAUB zNiQD46rP(@GFwjm&hpyFMY-aqnJCqf<0~2+2IZIqfAeF z@(_6tF^Vv=n<%8sWX^x6XvEA!D$*Hw{}k_?nAcEznGI0C{DtKVvtLY`=z2|rxP=c13D#a1F(%Cx5d^sy)czi?Tto_E0%NWbju7|V$LwBc z73RjAr1h`BBsK)V6D0OdtU(&0p{QXfst9Ip3_A7pW@1L2Gj5C*#Q)SX&&DluS3mlJ z@KOMSb#Q{BtwH}Yq3N* zDeE>>=`YG8-y~9j4K6T;S$&Hg(w`|E5VxG!NKli%-v28u4)g8r7OXQh!jpWBUinOW z1d|fagi#ghUmTh2GPzCrE=z06dXS<07Ot=FPfsnbA5sr?DJ7L5Gz+zT&u^Fve83a5 zYnZ939@r-ee8OTy8Adg-9+LVrfHrfge7*uDnLlMOA)}R(8;1NJFfs}7-w|bX#+#Rx zwp`)wZpzUT0q=FrJHjJnNI@(x@`mZ0flKqLJf=yB3mRcL@RJvy2XXe3SFeBcu~c6j znRm!$@%v5jzeC%-#sRN9)iPw02gYI@*oohRP>E6DpS2VTJI-7U5DC}Wxgh;tV7eAG zIKOM6-E7z2s;}G^ecN7sedsu*uGG1r?ib{~Hh8BiS!u`p`<4R67_-#t5CBBuD+&$W zX2)gB-_sOsYn|Lr7~{HQA}(|dl~K)FU06`)>QptOk&{q;R$2HT*uPQWz5dV_I3o>t z4IQ0d-?Wd&`185`J0D#|K`{TdkFIr?xUxXRH<8s%^sae=VsgWs*?;FZOjLxO zHU8%7D*{v;r)wjIrTvxA!VnU2HbIZu%uNzGIhA{&!+@6O-~(3s6rNO(OBfEh+SQ zxwHD-hI7k`$Y<(m^gZ*W{%MR<7nw>eybT^*{BeimEjckO%S#IvQV?lYm^KCvQxytRp6{Ib5KGs**&>~ z8YG8g@1DPlwSEz+!c1_}zXA;+pYvu{24|jc!Xu6+(Kn z487XQt7(_W0)WEx)gw5#h;ec_6Upt-sk07cs9ZX_zi>fc+zg z#L!Y4#r#ye{FI_MQpWS{@B8C^PLYHHT27K^^$9uLMeg?dl*~r&tR;7zcPBfNQs|2q zS^O7Lha38;znDGH4fzJzZ|i&pvP|=cO88aJ65xI>`KJht+VqeXJtWg}U(r_t*Pl&DxJ; zR4v2Bv#T+YOA3nocHWBkZz@;~lO;y@a(sbU0yAs8lr4XyH~!_v;jomrbn=2t9}aS9 zFmIZ!JJ^pf59#S{H}byCl$FZBc~2RfM#0|UG7A!v<;>d310~TJYctF-t}mmUV6!ga zGb;2ZoZ(PhF>BZy(pvOKR_7xtyW`hsVYAjZOir8)T?x&QI3sy^n<+c{<4?I{-XFRZdy`z@`QF? zMk!fgM|TQ)x#g{z=)L!WJ9pwGZ1~OH%lcR=nahG26>Sq~y_OrtEaY(Nw&0{P$45tV z>-7%FiJvj%YLCQ=pVlvM9QRtsXf5!>*&JZGz3Tb!MpGj4wC7NMY<_d~=AhwuZL=sV zVE*zJAPa(8dsgqqlM<<) zoYh(yLhX&u88iELsJm+)Jl?d-bRKl9E%f^8t4YicXc@Y=lw}O958N5yZ1S`YLtqm7 zS5f#zfKBT^yus(TK?3PN{-*tSa+~=bX9SUY3`o7~if5e|9es-!A7_B0r>rsURqZM{ zhWbUOhkmU#NE+2@1wf}(hGK{9Kb%G3f{}xT7T-1X>#1=WJH>BFiL^<;3AeCz(qMdU zhj+?BFJpq4hykhi%awt5PY)1;mbjL#1NRg}x$9qaxSTY|a6H-xm;CU1d;Tn6 zC6s3(A1>L=&dyFhverntyfx9L{huEhfI5aX7i)1WAG%y#&Ss7a8{O z)KdJh8sWIe@J4-eX=(rP6PXD457gs|J0M(>LGc3b=V{KJbR8TgOgVFG~Yk=HoG~CK~_J)|#O@t`4jtnE`{<0KCoTk!r66n_*RuXSBT zM#e1Hu?2qN=SugF4Ig&x1^Af5vDBQW-iWSC_js+lIuKl*auDfNZXkJJMGaJdj4r`> zQwBlCxCBDgXSrJ!RH@jGb!P=Z{!l<%rE{;%w~V}IUfNA)Htv_0nx5Viw`q|+MOVYZ zHA6$ON2#gwu>J(*Q&0&b#Ig%?d%W98+bn?D1@12(8=IT0LCJ%ZO$f4-b$_wFnN1Qe z7TX-{b(IPcB<5w86e*434~|Wy)dsHDT~@DN#(&&68XhP*VTeq&%sj4o{avZcb zCkPUr!SiEjmm~rh)4Q|vI;*IbvVh;99YcDD^ORpR#=qp2-(I)wydRnK$6Ik=>$?2D zpBWG2GuPG6JXd_$+S}7XKeBbMB`bR=t-NH6z;;Iwx2bmDM*n$mkCp;FcwuASd7AHPI(B4uBsd7T*wi zD&%!h?BVHYwB$cwRKUZ-vs7SQhPDB`k2ikzL~-X*1IxOcusjUx0$CRG3g+qQIUGey z76^r1qG4koMcqu8=_hqqr5#~qm#EwKF5qg@Xg*@z^*tI>KU?uZSAaobDR`}_fPmZN z9Umi7Ne=}jLa@_W7ojH7I$5B5MIJwwI@%_Xp^qF~Q#E{C#WpX1uKmVc`l(9(J1T6+&0s;aWUuhCD{0Ngffa@WD^lv z%4l=#QAp$!N)`bq=<}!#-d*Pj7MX6e zX-Dar3WD&8)ozsXG8h%SaIm%I<6lkiY+ejRXLX&FVtWVFpe17kRjBGlmck>6WPT!b ze;r80SNh19yXsIVvd3i&t8x7Q%tVcAaKgkp3Tfxv7e29q|OUMcythF7umN3r?&d6V&?E>N6iBE`>^ z*uU$q1QWp+viyke2D}O1_R!17gi`xDXE6pAY4>!b7YoQP)cf-c?q3xcuoUPFJDT?q31`cPIia(6e( zSte&UVQ@{zDwDYcMRt9p*>hf$C6RsPxG5z$d7qnu!fT}PwGTgtmFyi=Vej5XklkYSPZuu+)2TR(e1HTQ`PRtVv3yTg@;ouy@ zPYZ|Tj+5~b=+o2b_Mg!6a4=6s-Oo`N35oo|LS_!LS<)yRB%KHA6HCe(OOz zp{LJVrsD3dW2*7z00*=(wq?Q80yDL9>!eZgNOp3PuzhJG$0mBRK?yN)^0n_&H@);l zW9oWYy(`Bet)!m)Wc1mc43(I&dg(HWtekF3fL4;iFFeth&-idx948xdpx>*=qj{@# zl4pAy*g-QNe<+_ z6gk)as46v#u~0?>ZT7mZ_7$4VP(DX;V54S3SQs<`zvqnG*$29e_8c0_i?Uo` zET|<(F)1Rw#F1 0 && children.any { it.isDirectory && !MainClassFinder.isSkipped(it) && hasJvmSource(it, depth - 1) } + } +} diff --git a/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/JvmToolsDocumentation.kt b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/JvmToolsDocumentation.kt new file mode 100644 index 00000000..be122691 --- /dev/null +++ b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/JvmToolsDocumentation.kt @@ -0,0 +1,57 @@ +package com.appdevforall.jvm.plugin + +import com.itsaky.androidide.plugins.extensions.PluginTooltipButton +import com.itsaky.androidide.plugins.extensions.PluginTooltipEntry + +internal object JvmToolsDocumentation { + + const val CATEGORY = "plugin_${JvmToolsPlugin.PLUGIN_ID}" + const val DOCS_ASSET_PATH = "docs" + + fun entries(): List = listOf( + PluginTooltipEntry( + tag = tagFor(JvmToolsPlugin.ACTION_RUN), + summary = "Compiles every Java and Kotlin file in the project and runs the chosen main class.", + detail = """ +

Compiles all .java and .kt files in the project on the device, + then starts the main class chosen in Run configuration with its program arguments. + With no choice saved, Run starts the main class of the file open in the editor, or the + first one it finds.

+

Compiler errors and the program's output stream into Build Output. While the + program runs, the button becomes Cancel Run; tap it to stop the program. A run is + stopped after 30 minutes.

+

The program cannot read keyboard input: Scanner and + readLine() see the end of input. Pass values as program arguments instead.

+ """.trimIndent(), + buttons = buttons("run"), + ), + PluginTooltipEntry( + tag = tagFor(JvmToolsPlugin.ACTION_RUN_CONFIGURATION), + summary = "Chooses which main class Run starts and the arguments it passes.", + detail = """ +

Lists every class in the project that has a main method: Java classes with + public static void main(String[] args), Kotlin files with a top-level + fun main(), and Kotlin objects with a @JvmStatic main.

+

Program arguments are split on spaces. Wrap an argument in quotes to keep its spaces, + for example --name "Ada Lovelace". The choice is saved for this project.

+ """.trimIndent(), + buttons = buttons("run-configuration"), + ), + ) + + fun tagFor(actionId: String): String = "${JvmToolsPlugin.PLUGIN_ID}.$actionId" + + private fun buttons(anchor: String): List = listOf( + PluginTooltipButton( + description = "How this command works", + uri = "index.html#$anchor", + order = 0, + ), + PluginTooltipButton( + description = "About Code on the Go plugins", + uri = "i/plugins-adfa.html", + order = 1, + directPath = true, + ), + ) +} diff --git a/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/JvmToolsPlugin.kt b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/JvmToolsPlugin.kt new file mode 100644 index 00000000..d21a476c --- /dev/null +++ b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/JvmToolsPlugin.kt @@ -0,0 +1,310 @@ +package com.appdevforall.jvm.plugin + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import com.itsaky.androidide.plugins.IPlugin +import com.itsaky.androidide.plugins.PluginContext +import com.itsaky.androidide.plugins.extensions.BuildActionCategory +import com.itsaky.androidide.plugins.extensions.BuildActionExtension +import com.itsaky.androidide.plugins.extensions.CommandSpec +import com.itsaky.androidide.plugins.extensions.DocumentationExtension +import com.itsaky.androidide.plugins.extensions.PluginBuildAction +import com.itsaky.androidide.plugins.extensions.PluginTooltipEntry +import com.itsaky.androidide.plugins.extensions.ToolbarAction +import com.itsaky.androidide.plugins.extensions.ToolbarActionIds +import com.itsaky.androidide.plugins.extensions.UIExtension +import com.itsaky.androidide.plugins.services.IdeEditorService +import com.itsaky.androidide.plugins.services.IdeEnvironmentService +import com.itsaky.androidide.plugins.services.IdeProjectService +import com.itsaky.androidide.plugins.services.IdeTemplateService +import com.itsaky.androidide.plugins.services.IdeTooltipService +import com.itsaky.androidide.plugins.services.IdeUIService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.util.concurrent.atomic.AtomicBoolean + +class JvmToolsPlugin : IPlugin, BuildActionExtension, UIExtension, DocumentationExtension { + + private var pluginContext: PluginContext? = null + private var projectService: IdeProjectService? = null + private var editorService: IdeEditorService? = null + private var environmentService: IdeEnvironmentService? = null + private var templateService: IdeTemplateService? = null + private var uiService: IdeUIService? = null + private var tooltipService: IdeTooltipService? = null + private var configurations: RunConfigurationStore? = null + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private val registeredTemplates = mutableListOf() + private val tooltipBindingScheduled = AtomicBoolean(false) + + override fun initialize(context: PluginContext): Boolean { + pluginContext = context + projectService = context.services.get(IdeProjectService::class.java) + editorService = context.services.get(IdeEditorService::class.java) + environmentService = context.services.get(IdeEnvironmentService::class.java) + templateService = context.services.get(IdeTemplateService::class.java) + uiService = context.services.get(IdeUIService::class.java) + tooltipService = context.services.get(IdeTooltipService::class.java) + configurations = RunConfigurationStore(File(context.resources.getPluginDirectory(), CONFIGURATIONS_FILE)) + return true + } + + override fun activate(): Boolean { + registerTemplates() + return true + } + + override fun deactivate(): Boolean { + templateService?.let { service -> registeredTemplates.forEach { service.unregisterTemplate(it) } } + registeredTemplates.clear() + return true + } + + override fun dispose() { + scope.cancel() + pluginContext = null + projectService = null + editorService = null + environmentService = null + templateService = null + uiService = null + tooltipService = null + configurations = null + } + + override fun toolbarActionsToHide(): Set { + if (!isJvmProjectOpen()) return emptySet() + scheduleTooltipBinding() + return ToolbarActionIds.BUILD_HIDEABLE + } + + override fun getHiddenToolbarActionIds(): Set = + if (isJvmProjectOpen()) setOf(QUICK_BUILD_ACTION_ID) else emptySet() + + override fun getToolbarActions(): List = listOf( + ToolbarAction( + id = ACTION_RUN_CONFIGURATION, + title = RUN_CONFIGURATION_LABEL, + icon = R.drawable.ic_run_configuration, + order = RUN_CONFIGURATION_ORDER, + action = ::showRunConfiguration, + ).apply { isVisibleProvider = ::isJvmProjectOpen } + ) + + override fun getBuildActions(): List { + if (!isJvmProjectOpen()) return emptyList() + val root = projectService?.getCurrentProject()?.rootDir ?: return emptyList() + val toolchain = resolveToolchain() ?: return emptyList() + scheduleTooltipBinding() + val configuration = effectiveConfiguration(root, MainClassFinder.find(root)) + return listOf( + PluginBuildAction( + id = ACTION_RUN, + name = RUN_LABEL, + description = configuration?.let { "Compile the project and run ${it.mainClass}" } + ?: "Compile the project and run its main class", + icon = R.drawable.ic_run_jvm, + category = BuildActionCategory.BUILD, + command = CommandSpec.ShellCommand("sh", listOf("-c", runScript(root, toolchain, configuration))), + timeoutMs = RUN_TIMEOUT_MS, + ) + ) + } + + override fun onActionStarted(actionId: String) { + if (actionId != ACTION_RUN) return + val editor = editorService ?: return + val root = projectService?.getCurrentProject()?.rootDir?.canonicalFile ?: return + val unsaved = editor.getModifiedFiles().filter { MainClassFinder.isSource(it) && it.canonicalFile.startsWith(root) } + unsaved.forEach { file -> editor.getFileContent(file)?.let(file::writeText) } + scope.launch { unsaved.forEach { editor.saveFile(it) } } + } + + override fun getTooltipCategory(): String = JvmToolsDocumentation.CATEGORY + + override fun getTooltipEntries(): List = JvmToolsDocumentation.entries() + + override fun getTier3DocsAssetPath(): String = JvmToolsDocumentation.DOCS_ASSET_PATH + + private fun isJvmProjectOpen(): Boolean = JvmDomain.isJvmProject(projectService?.getCurrentProject()?.rootDir) + + private fun effectiveConfiguration(root: File, mainClasses: List): RunConfiguration? { + val saved = configurations?.get(root) + if (saved != null && mainClasses.any { it.name == saved.mainClass }) return saved + val current = editorService?.getCurrentFile()?.canonicalFile + val main = mainClasses.firstOrNull { it.source.canonicalFile == current } ?: mainClasses.firstOrNull() ?: return null + return RunConfiguration(main.name, saved?.arguments ?: "") + } + + private fun showRunConfiguration() { + val root = projectService?.getCurrentProject()?.rootDir ?: return + val activity = uiService?.takeIf { it.isUIAvailable() }?.getCurrentActivity() ?: return + scope.launch { + val mainClasses = withContext(Dispatchers.IO) { MainClassFinder.find(root) } + val selected = effectiveConfiguration(root, mainClasses) ?: RunConfiguration("", "") + RunConfigurationDialog.show( + activity, + mainClasses, + selected, + onHelp = { view -> + tooltipService?.showTooltip( + view, + JvmToolsDocumentation.CATEGORY, + JvmToolsDocumentation.tagFor(ACTION_RUN_CONFIGURATION), + ) + }, + ) { configuration -> + scope.launch(Dispatchers.IO) { configurations?.put(root, configuration) } + } + } + } + + private fun registerTemplates() { + val service = templateService ?: return + val context = pluginContext ?: return + val template = service.createTemplateBuilder(TEMPLATE_NAME) + .description("A command-line program in Java or Kotlin. Choose the language when you create it, then tap Run.") + .showLanguageOption() + .thumbnailFromAssets("templates/console/thumb.png", context) + .addTemplateFromAssets("src/Main.java", "templates/console/Main.java.peb", context) + .addTemplateFromAssets("src/Greeter.java", "templates/console/Greeter.java.peb", context) + .addTemplateFromAssets("src/Main.kt", "templates/console/Main.kt.peb", context) + .addTemplateFromAssets("src/Greeter.kt", "templates/console/Greeter.kt.peb", context) + .addStaticFromAssets("README.md", "templates/console/README.md", context) + .build(context.resources.getPluginDirectory()) + if (service.registerTemplate(template)) registeredTemplates += template.name + } + + private fun resolveToolchain(): Toolchain? { + val tmp = environmentService?.getTmpDirectory() ?: return null + val prefix = tmp.parentFile ?: return null + val jdk = File(prefix, "lib/jvm/java-21-openjdk/bin") + return Toolchain( + java = File(jdk, "java"), + javac = File(jdk, "javac"), + mavenRepository = File(prefix.parentFile, "home/maven/localMvnRepository"), + workDirectory = File(tmp, "jvm-tools"), + ) + } + + private fun runScript(root: File, toolchain: Toolchain, configuration: RunConfiguration?): String = buildString { + append("JAVA=").append(shellQuote(toolchain.java.absolutePath)).append('\n') + append("JAVAC=").append(shellQuote(toolchain.javac.absolutePath)).append('\n') + append("MAVEN=").append(shellQuote(toolchain.mavenRepository.absolutePath)).append('\n') + append("PROJECT=").append(shellQuote(root.absolutePath)).append('\n') + append("WORK=").append(shellQuote(File(toolchain.workDirectory, root.absolutePath.hashCode().toUInt().toString(16)).absolutePath)).append('\n') + append("MAIN=").append(shellQuote(configuration?.mainClass ?: "")).append('\n') + append("set --") + ProgramArguments.parse(configuration?.arguments ?: "").forEach { append(' ').append(shellQuote(it)) } + append('\n') + append(RUN_SCRIPT) + } + + private fun shellQuote(value: String): String = "'" + value.replace("'", "'\\''") + "'" + + private fun scheduleTooltipBinding() { + if (tooltipService == null) return + val activity = uiService?.takeIf { it.isUIAvailable() }?.getCurrentActivity() ?: return + if (!tooltipBindingScheduled.compareAndSet(false, true)) return + activity.runOnUiThread { + val decor = activity.window?.decorView + if (decor == null) { + tooltipBindingScheduled.set(false) + return@runOnUiThread + } + decor.post { + tooltipBindingScheduled.set(false) + (decor as? ViewGroup)?.let(::bindTooltipsIn) + } + } + } + + private fun bindTooltipsIn(group: ViewGroup) { + for (index in 0 until group.childCount) { + when (val child = group.getChildAt(index)) { + is ImageButton -> bindTooltip(child) + is ViewGroup -> bindTooltipsIn(child) + } + } + } + + private fun bindTooltip(button: ImageButton) { + val tooltips = tooltipService ?: return + if (button.contentDescription?.toString() !in RUN_BUTTON_LABELS) return + button.setOnLongClickListener { view: View -> + tooltips.showTooltip(view, JvmToolsDocumentation.CATEGORY, JvmToolsDocumentation.tagFor(ACTION_RUN)) + true + } + } + + private data class Toolchain(val java: File, val javac: File, val mavenRepository: File, val workDirectory: File) + + companion object { + internal const val PLUGIN_ID = "com.appdevforall.jvm.plugin" + internal const val ACTION_RUN = "jvm.run" + internal const val ACTION_RUN_CONFIGURATION = "jvm.runConfiguration" + + private const val RUN_LABEL = "Run" + private const val RUN_CONFIGURATION_LABEL = "Run configuration" + private val RUN_BUTTON_LABELS = setOf(RUN_LABEL, "Cancel $RUN_LABEL") + private const val RUN_CONFIGURATION_ORDER = 14 + private const val RUN_TIMEOUT_MS = 30 * 60_000L + private const val QUICK_BUILD_ACTION_ID = "ide.editor.build.quickBuild" + private const val CONFIGURATIONS_FILE = "run-configurations.json" + private const val TEMPLATE_NAME = "Java/Kotlin Program" + + private val RUN_SCRIPT: String = """ + set -u + if [ -z "§MAIN" ]; then + echo "No main class found. Add a Java class with public static void main(String[] args), or a Kotlin file with a top-level fun main()." >&2 + exit 2 + fi + if [ ! -x "§JAVA" ] || [ ! -x "§JAVAC" ]; then + echo "The device JDK is missing from §(dirname "§JAVA")." >&2 + exit 3 + fi + cd "§PROJECT" || exit 2 + rm -rf "§WORK" + mkdir -p "§WORK/classes" + find . \( -name build -o -name '.?*' \) -prune -o -type f \( -name '*.java' -o -name '*.kt' \) -print | sort > "§WORK/sources" + quote() { sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/.*/"&"/'; } + grep '\.java§' "§WORK/sources" | quote > "§WORK/java-sources" + CP="§WORK/classes" + if grep -q '\.kt§' "§WORK/sources"; then + find_jar() { + for version in §(ls -1 "§MAVEN/§1" 2>/dev/null | sort -r); do + jar=§(ls -1 "§MAVEN/§1/§version"/*.jar 2>/dev/null | grep -vE '(-sources|-javadoc)\.jar§' | head -n1) + if [ -n "§jar" ]; then echo "§jar"; return 0; fi + done + return 1 + } + compiler=§(find_jar org/jetbrains/kotlin/kotlin-compiler-embeddable) || { echo "The Kotlin compiler is missing from §MAVEN." >&2; exit 4; } + stdlib=§(find_jar org/jetbrains/kotlin/kotlin-stdlib) || { echo "The Kotlin standard library is missing from §MAVEN." >&2; exit 4; } + boot="§compiler:§stdlib" + for artifact in org/jetbrains/kotlin/kotlin-script-runtime org/jetbrains/kotlin/kotlin-reflect \ + org/jetbrains/kotlin/kotlin-daemon-embeddable org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm \ + org/jetbrains/annotations; do + jar=§(find_jar "§artifact") && boot="§boot:§jar" + done + quote < "§WORK/sources" > "§WORK/kotlin-sources" + echo ">> Compiling Kotlin" + "§JAVA" -Dkotlin.colors.enabled=false -cp "§boot" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler \ + -no-stdlib -no-reflect -classpath "§stdlib" -d "§WORK/classes" @"§WORK/kotlin-sources" || exit 1 + CP="§CP:§stdlib" + fi + if [ -s "§WORK/java-sources" ]; then + echo ">> Compiling Java" + "§JAVAC" -cp "§CP" -d "§WORK/classes" @"§WORK/java-sources" || exit 1 + fi + echo ">> Running §MAIN" + exec "§JAVA" -cp "§CP" "§MAIN" "§@" + """.trimIndent().replace('§', '$') + } +} diff --git a/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/MainClassFinder.kt b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/MainClassFinder.kt new file mode 100644 index 00000000..3a365c3c --- /dev/null +++ b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/MainClassFinder.kt @@ -0,0 +1,83 @@ +package com.appdevforall.jvm.plugin + +import java.io.File + +internal data class MainClass(val name: String, val source: File) + +internal object MainClassFinder { + + private const val MAX_SOURCES = 2000 + + private val COMMENT = Regex("""/\*[\s\S]*?\*/|//[^\n]*""") + private val JAVA_PACKAGE = Regex("""^\s*package\s+([\w.]+)\s*;""", RegexOption.MULTILINE) + private val JAVA_TYPE = Regex("""\b(?:class|interface|enum|record)\s+(\w+)""") + private val JAVA_MAIN = Regex( + """\bstatic\s+(?:(?:public|final|synchronized|strictfp)\s+)*void\s+main\s*\(\s*(?:final\s+)?String\s*(?:\[\s*]|\.\.\.|\s\w+\s*\[\s*])""" + ) + private val KOTLIN_PACKAGE = Regex("""^\s*package\s+([\w.]+)""", RegexOption.MULTILINE) + private val KOTLIN_JVM_NAME = Regex("""@file:JvmName\(\s*"(\w+)"\s*\)""") + private val KOTLIN_TOP_LEVEL_MAIN = Regex("""^(?:(?:public|suspend)\s+)*fun\s+main\s*\(""", RegexOption.MULTILINE) + private val KOTLIN_JVM_STATIC_MAIN = Regex("""@JvmStatic\s+(?:(?:public|suspend)\s+)*fun\s+main\s*\(""") + private val KOTLIN_OWNER = Regex("""\bobject\s+(\w+)|\bcompanion\s+object\b""") + private val KOTLIN_CLASS = Regex("""\bclass\s+(\w+)""") + + fun isSource(file: File): Boolean = file.extension == "java" || file.extension == "kt" + + fun isSkipped(dir: File): Boolean = dir.name == "build" || dir.name.startsWith(".") + + fun find(root: File): List = + sources(root) + .flatMap { file -> mainClassNames(file.name, file.readText()).map { MainClass(it, file) } } + .distinctBy { it.name } + .sortedBy { it.name } + + fun sources(root: File): List = + root.walkTopDown() + .onEnter { it == root || !isSkipped(it) } + .filter { it.isFile && isSource(it) } + .take(MAX_SOURCES) + .toList() + + fun mainClassNames(fileName: String, text: String): List { + val code = COMMENT.replace(text, "") + return when { + fileName.endsWith(".java") -> javaMainClasses(code) + fileName.endsWith(".kt") -> kotlinMainClasses(fileName, code) + else -> emptyList() + } + } + + private fun javaMainClasses(code: String): List { + val pkg = JAVA_PACKAGE.find(code)?.groupValues?.get(1) + return JAVA_MAIN.findAll(code) + .mapNotNull { main -> JAVA_TYPE.findAll(code.substring(0, main.range.first)).lastOrNull()?.groupValues?.get(1) } + .map { qualify(pkg, it) } + .toList() + } + + private fun kotlinMainClasses(fileName: String, code: String): List { + val pkg = KOTLIN_PACKAGE.find(code)?.groupValues?.get(1) + val names = mutableListOf() + if (KOTLIN_TOP_LEVEL_MAIN.containsMatchIn(code)) { + names += KOTLIN_JVM_NAME.find(code)?.groupValues?.get(1) ?: facadeClassName(fileName) + } + KOTLIN_JVM_STATIC_MAIN.findAll(code).forEach { main -> + ownerOf(code.substring(0, main.range.first))?.let { names += it } + } + return names.map { qualify(pkg, it) } + } + + private fun ownerOf(before: String): String? { + val owner = KOTLIN_OWNER.findAll(before).lastOrNull() ?: return null + return owner.groups[1]?.value + ?: KOTLIN_CLASS.findAll(before.substring(0, owner.range.first)).lastOrNull()?.groupValues?.get(1) + } + + private fun facadeClassName(fileName: String): String { + val base = fileName.removeSuffix(".kt").map { if (it.isJavaIdentifierPart()) it else '_' }.joinToString("") + val start = if (base.firstOrNull()?.isJavaIdentifierStart() == true) base else "_$base" + return start.replaceFirstChar { it.uppercaseChar() } + "Kt" + } + + private fun qualify(pkg: String?, name: String): String = if (pkg.isNullOrEmpty()) name else "$pkg.$name" +} diff --git a/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/ProgramArguments.kt b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/ProgramArguments.kt new file mode 100644 index 00000000..26a44d7d --- /dev/null +++ b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/ProgramArguments.kt @@ -0,0 +1,44 @@ +package com.appdevforall.jvm.plugin + +internal object ProgramArguments { + + fun parse(input: String): List { + val args = mutableListOf() + val current = StringBuilder() + var inArgument = false + var quote: Char? = null + var i = 0 + while (i < input.length) { + val c = input[i] + when { + quote == '\'' -> if (c == '\'') quote = null else current.append(c) + quote == '"' -> when { + c == '"' -> quote = null + c == '\\' && i + 1 < input.length && input[i + 1] in "\"\\" -> current.append(input[++i]) + else -> current.append(c) + } + c.isWhitespace() -> if (inArgument) { + args += current.toString() + current.clear() + inArgument = false + } + c == '\'' || c == '"' -> { + quote = c + inArgument = true + } + c == '\\' && i + 1 < input.length -> { + current.append(input[++i]) + inArgument = true + } + else -> { + current.append(c) + inArgument = true + } + } + i++ + } + require(quote == null) { "Close the $quote quote in the program arguments." } + if (inArgument) args += current.toString() + return args + } +} diff --git a/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/RunConfigurationDialog.kt b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/RunConfigurationDialog.kt new file mode 100644 index 00000000..e77a4ae1 --- /dev/null +++ b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/RunConfigurationDialog.kt @@ -0,0 +1,112 @@ +package com.appdevforall.jvm.plugin + +import android.app.Activity +import android.content.DialogInterface +import android.text.InputType +import android.util.TypedValue +import android.view.View +import android.widget.LinearLayout +import android.widget.RadioGroup +import android.widget.ScrollView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.radiobutton.MaterialRadioButton +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import com.google.android.material.textview.MaterialTextView +import com.itsaky.androidide.plugins.base.PluginWindows + +internal object RunConfigurationDialog { + + private const val TITLE = "Run configuration" + + fun show( + activity: Activity, + mainClasses: List, + selected: RunConfiguration, + onHelp: (View) -> Unit, + onSave: (RunConfiguration) -> Unit, + ) { + if (mainClasses.isEmpty()) { + PluginWindows.showDialog( + MaterialAlertDialogBuilder(activity) + .setTitle(TITLE) + .setMessage( + "No main class found. Add a Java class with public static void main(String[] args), " + + "or a Kotlin file with a top-level fun main()." + ) + .setPositiveButton(android.R.string.ok, null) + .create() + ) + return + } + + val classes = RadioGroup(activity) + mainClasses.forEachIndexed { index, main -> + classes.addView(MaterialRadioButton(activity).apply { + id = index + 1 + text = main.name + }) + } + classes.check(mainClasses.indexOfFirst { it.name == selected.mainClass }.coerceAtLeast(0) + 1) + + val argumentsLayout = TextInputLayout(activity).apply { + hint = "Program arguments" + helperText = "Separate arguments with spaces. Quote an argument that contains spaces." + setOnLongClickListener { view -> + onHelp(view) + true + } + } + val arguments = TextInputEditText(argumentsLayout.context).apply { + setText(selected.arguments) + inputType = InputType.TYPE_CLASS_TEXT or + InputType.TYPE_TEXT_FLAG_MULTI_LINE or + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + } + argumentsLayout.addView(arguments) + + val padding = dp(activity, 24) + val content = LinearLayout(activity).apply { + orientation = LinearLayout.VERTICAL + setPadding(padding, dp(activity, 8), padding, 0) + addView(MaterialTextView(activity).apply { + text = "Main class" + setTextAppearance(android.R.style.TextAppearance_Material_Subhead) + setOnLongClickListener { view -> + onHelp(view) + true + } + }) + addView(classes) + addView( + argumentsLayout, + LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + .apply { topMargin = dp(activity, 16) }, + ) + } + + val dialog = MaterialAlertDialogBuilder(activity) + .setTitle(TITLE) + .setView(ScrollView(activity).apply { addView(content) }) + .setPositiveButton("Save", null) + .setNegativeButton(android.R.string.cancel, null) + .create() + dialog.setOnShowListener { + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { + val text = arguments.text?.toString().orEmpty() + try { + ProgramArguments.parse(text) + } catch (e: IllegalArgumentException) { + argumentsLayout.error = e.message + return@setOnClickListener + } + onSave(RunConfiguration(mainClasses[classes.checkedRadioButtonId - 1].name, text)) + dialog.dismiss() + } + } + PluginWindows.showDialog(dialog) + } + + private fun dp(activity: Activity, value: Int): Int = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value.toFloat(), activity.resources.displayMetrics).toInt() +} diff --git a/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/RunConfigurationStore.kt b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/RunConfigurationStore.kt new file mode 100644 index 00000000..9eb016d9 --- /dev/null +++ b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/RunConfigurationStore.kt @@ -0,0 +1,36 @@ +package com.appdevforall.jvm.plugin + +import org.json.JSONObject +import java.io.File + +internal data class RunConfiguration(val mainClass: String, val arguments: String) + +internal class RunConfigurationStore(private val file: File) { + + @Synchronized + fun get(projectRoot: File): RunConfiguration? { + val entry = read().optJSONObject(projectRoot.absolutePath) ?: return null + return RunConfiguration(entry.getString(KEY_MAIN_CLASS), entry.getString(KEY_ARGUMENTS)) + } + + @Synchronized + fun put(projectRoot: File, configuration: RunConfiguration) { + val all = read().put( + projectRoot.absolutePath, + JSONObject() + .put(KEY_MAIN_CLASS, configuration.mainClass) + .put(KEY_ARGUMENTS, configuration.arguments), + ) + file.parentFile?.mkdirs() + val staged = File(file.parentFile, "${file.name}.tmp") + staged.writeText(all.toString()) + check(staged.renameTo(file)) { "Could not save the run configuration to $file" } + } + + private fun read(): JSONObject = if (file.isFile) JSONObject(file.readText()) else JSONObject() + + private companion object { + const val KEY_MAIN_CLASS = "mainClass" + const val KEY_ARGUMENTS = "arguments" + } +} diff --git a/plugins/JVM-Tools/src/main/res/drawable/ic_run_configuration.xml b/plugins/JVM-Tools/src/main/res/drawable/ic_run_configuration.xml new file mode 100644 index 00000000..7349d835 --- /dev/null +++ b/plugins/JVM-Tools/src/main/res/drawable/ic_run_configuration.xml @@ -0,0 +1,9 @@ + + + diff --git a/plugins/JVM-Tools/src/main/res/drawable/ic_run_jvm.xml b/plugins/JVM-Tools/src/main/res/drawable/ic_run_jvm.xml new file mode 100644 index 00000000..88f62917 --- /dev/null +++ b/plugins/JVM-Tools/src/main/res/drawable/ic_run_jvm.xml @@ -0,0 +1,9 @@ + + + diff --git a/plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/MainClassFinderTest.kt b/plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/MainClassFinderTest.kt new file mode 100644 index 00000000..156136bd --- /dev/null +++ b/plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/MainClassFinderTest.kt @@ -0,0 +1,90 @@ +package com.appdevforall.jvm.plugin + +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class MainClassFinderTest { + + @get:Rule + val folder = TemporaryFolder() + + @Test + fun `java main is named after the class that declares it, with its package`() { + val source = """ + package com.example.app; + + class Helper {} + + public final class Launcher { + public static void main(String[] args) {} + } + """.trimIndent() + + assertEquals(listOf("com.example.app.Launcher"), MainClassFinder.mainClassNames("Launcher.java", source)) + } + + @Test + fun `java main accepts varargs, c-style arrays and modifiers in any order`() { + assertEquals(listOf("A"), MainClassFinder.mainClassNames("A.java", "class A { static public void main(String... a) {} }")) + assertEquals(listOf("B"), MainClassFinder.mainClassNames("B.java", "class B { public static void main(String a[]) {} }")) + } + + @Test + fun `java without a runnable main is not listed`() { + val source = """ + class A { + // public static void main(String[] args) {} + /* public static void main(String[] args) {} */ + public void main(String[] args) {} + static void main(int x) {} + } + """.trimIndent() + + assertEquals(emptyList(), MainClassFinder.mainClassNames("A.java", source)) + } + + @Test + fun `kotlin top-level main runs as the file facade`() { + assertEquals(listOf("tools.MainKt"), MainClassFinder.mainClassNames("main.kt", "package tools\n\nfun main() {}\n")) + assertEquals(listOf("My_appKt"), MainClassFinder.mainClassNames("my-app.kt", "suspend fun main(args: Array) {}\n")) + assertEquals(listOf("Launcher"), MainClassFinder.mainClassNames("App.kt", "@file:JvmName(\"Launcher\")\nfun main() {}\n")) + } + + @Test + fun `kotlin jvmstatic main runs in its object or in the class of its companion`() { + val source = """ + object Tool { + @JvmStatic fun main(args: Array) {} + } + + class App { + companion object { + @JvmStatic + fun main(args: Array) {} + } + } + """.trimIndent() + + assertEquals(listOf("Tool", "App"), MainClassFinder.mainClassNames("Both.kt", source)) + } + + @Test + fun `kotlin main inside a class without JvmStatic is not listed`() { + assertEquals(emptyList(), MainClassFinder.mainClassNames("A.kt", "class A {\n fun main() {}\n}\n")) + } + + @Test + fun `find walks the project, skipping build and hidden folders`() { + folder.newFolder("src", "tools") + folder.newFile("src/Main.java").writeText("public class Main { public static void main(String[] a) {} }") + folder.newFile("src/tools/Echo.kt").writeText("package tools\nfun main() {}\n") + folder.newFolder("build") + folder.newFile("build/Old.java").writeText("class Old { public static void main(String[] a) {} }") + folder.newFolder(".cg") + folder.newFile(".cg/Hidden.kt").writeText("fun main() {}\n") + + assertEquals(listOf("Main", "tools.EchoKt"), MainClassFinder.find(folder.root).map { it.name }) + } +} diff --git a/plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/ProgramArgumentsTest.kt b/plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/ProgramArgumentsTest.kt new file mode 100644 index 00000000..813efe0b --- /dev/null +++ b/plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/ProgramArgumentsTest.kt @@ -0,0 +1,30 @@ +package com.appdevforall.jvm.plugin + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test + +class ProgramArgumentsTest { + + @Test + fun `splits on whitespace`() { + assertEquals(listOf("a", "b", "c"), ProgramArguments.parse(" a b\t\nc ")) + assertEquals(emptyList(), ProgramArguments.parse(" ")) + } + + @Test + fun `quotes keep spaces and can hold the other quote`() { + assertEquals(listOf("--name", "Ada Lovelace", "it's"), ProgramArguments.parse("--name \"Ada Lovelace\" \"it's\"")) + assertEquals(listOf("say \"hi\"", "a\\b"), ProgramArguments.parse("'say \"hi\"' 'a\\b'")) + } + + @Test + fun `backslash escapes outside single quotes`() { + assertEquals(listOf("a b", "\"x\"", ""), ProgramArguments.parse("a\\ b \"\\\"x\\\"\" \"\"")) + } + + @Test + fun `an unclosed quote is rejected`() { + assertThrows(IllegalArgumentException::class.java) { ProgramArguments.parse("\"Ada") } + } +} From 100bab78d7c8d4da77d87972674d797c59086d0e Mon Sep 17 00:00:00 2001 From: Daniel Alome Date: Wed, 7 Oct 2026 21:25:33 +0100 Subject: [PATCH 2/2] ADFA-1980 | JVM-Tools: run a main in a nested type by its binary name MainClassFinder named a main after the last type declared before it, so a record or nested class above main took its name and java failed with "Could not find or load main class". A main now belongs to the type whose braces enclose it, and nested types run as Outer$Inner; Kotlin @JvmStatic mains in nested objects and companions follow the same rule. String and char literals are blanked before braces are counted, and Foo.class / Foo::class are no longer read as declarations. A main in a local or anonymous class is not listed, since its binary name cannot be derived from the source. The help pages say so. --- plugins/JVM-Tools/jvm-tools.html | 5 +- .../JVM-Tools/src/main/assets/docs/index.html | 2 + .../jvm/plugin/MainClassFinder.kt | 76 ++++++++++++++----- .../jvm/plugin/MainClassFinderTest.kt | 61 +++++++++++++++ 4 files changed, 122 insertions(+), 22 deletions(-) diff --git a/plugins/JVM-Tools/jvm-tools.html b/plugins/JVM-Tools/jvm-tools.html index 5e8fac90..58f2c254 100644 --- a/plugins/JVM-Tools/jvm-tools.html +++ b/plugins/JVM-Tools/jvm-tools.html @@ -147,7 +147,8 @@

Finding main classes

MainClassFinder reads the sources with comments removed and reports Java classes with a static void main(String[]), Kotlin files with a top-level fun main (as the FileNameKt facade, or the @file:JvmName), and Kotlin objects or companion - objects with a @JvmStatic main. Names are qualified with the file's package. + objects with a @JvmStatic main. A main belongs to the type whose braces enclose it, so a + nested type is run by its binary name (Outer$Inner). Names are qualified with the file's package.

Compiling and running

@@ -215,7 +216,7 @@

7. Limits

  • No keyboard input: plugin commands have no stdin, so Scanner and readLine() see the end of input.
  • No third-party libraries: only the Java class library and the Kotlin standard library are on the classpath.
  • -
  • A main in a nested Java class is not listed.
  • +
  • A main in a local or anonymous class is not listed.
diff --git a/plugins/JVM-Tools/src/main/assets/docs/index.html b/plugins/JVM-Tools/src/main/assets/docs/index.html index 92c821bf..0ea20247 100644 --- a/plugins/JVM-Tools/src/main/assets/docs/index.html +++ b/plugins/JVM-Tools/src/main/assets/docs/index.html @@ -46,6 +46,8 @@

Run configuration

  • a Kotlin object or companion object with a @JvmStatic main
  • +

    A class nested inside another is listed by the name Java runs it under, such as +Outer$Inner.

    Program arguments are split on spaces. Wrap an argument in single or double quotes to keep its spaces, for example --name "Ada Lovelace". The choice is saved for each project.

    diff --git a/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/MainClassFinder.kt b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/MainClassFinder.kt index 3a365c3c..4046ded4 100644 --- a/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/MainClassFinder.kt +++ b/plugins/JVM-Tools/src/main/kotlin/com/appdevforall/jvm/plugin/MainClassFinder.kt @@ -8,9 +8,9 @@ internal object MainClassFinder { private const val MAX_SOURCES = 2000 - private val COMMENT = Regex("""/\*[\s\S]*?\*/|//[^\n]*""") + private val COMMENT_OR_LITERAL = Regex(""""{3}[\s\S]*?"{3}|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|/\*[\s\S]*?\*/|//[^\n]*""") private val JAVA_PACKAGE = Regex("""^\s*package\s+([\w.]+)\s*;""", RegexOption.MULTILINE) - private val JAVA_TYPE = Regex("""\b(?:class|interface|enum|record)\s+(\w+)""") + private val JAVA_TYPE = Regex("""(? { - val code = COMMENT.replace(text, "") + val code = COMMENT_OR_LITERAL.replace(text) { if (it.value.startsWith("/")) " " else it.value } + val structure = COMMENT_OR_LITERAL.replace(code, "\"\"") return when { - fileName.endsWith(".java") -> javaMainClasses(code) - fileName.endsWith(".kt") -> kotlinMainClasses(fileName, code) + fileName.endsWith(".java") -> javaMainClasses(structure) + fileName.endsWith(".kt") -> kotlinMainClasses(fileName, code, structure) else -> emptyList() } } - private fun javaMainClasses(code: String): List { - val pkg = JAVA_PACKAGE.find(code)?.groupValues?.get(1) - return JAVA_MAIN.findAll(code) - .mapNotNull { main -> JAVA_TYPE.findAll(code.substring(0, main.range.first)).lastOrNull()?.groupValues?.get(1) } + private fun javaMainClasses(structure: String): List { + val pkg = JAVA_PACKAGE.find(structure)?.groupValues?.get(1) + val bodies = bodies(structure, JAVA_TYPE) { Scope(it.groupValues[1], Kind.TYPE) } + return JAVA_MAIN.findAll(structure) + .mapNotNull { main -> binaryName(scopesAt(structure, bodies, main.range.first)) } .map { qualify(pkg, it) } .toList() } - private fun kotlinMainClasses(fileName: String, code: String): List { - val pkg = KOTLIN_PACKAGE.find(code)?.groupValues?.get(1) + private fun kotlinMainClasses(fileName: String, code: String, structure: String): List { + val pkg = KOTLIN_PACKAGE.find(structure)?.groupValues?.get(1) val names = mutableListOf() - if (KOTLIN_TOP_LEVEL_MAIN.containsMatchIn(code)) { + if (KOTLIN_TOP_LEVEL_MAIN.containsMatchIn(structure)) { names += KOTLIN_JVM_NAME.find(code)?.groupValues?.get(1) ?: facadeClassName(fileName) } - KOTLIN_JVM_STATIC_MAIN.findAll(code).forEach { main -> - ownerOf(code.substring(0, main.range.first))?.let { names += it } + val bodies = bodies(structure, KOTLIN_TYPE) { match -> + when { + match.groups[1] != null -> Scope("", Kind.COMPANION) + match.groups[2] != null -> Scope(match.groupValues[2], Kind.OBJECT) + else -> Scope(match.groupValues[3], Kind.TYPE) + } + } + KOTLIN_JVM_STATIC_MAIN.findAll(structure).forEach { main -> + val scopes = scopesAt(structure, bodies, main.range.first) + when (scopes.lastOrNull()?.kind) { + Kind.OBJECT -> binaryName(scopes) + Kind.COMPANION -> binaryName(scopes.dropLast(1)) + else -> null + }?.let { names += it } } return names.map { qualify(pkg, it) } } - private fun ownerOf(before: String): String? { - val owner = KOTLIN_OWNER.findAll(before).lastOrNull() ?: return null - return owner.groups[1]?.value - ?: KOTLIN_CLASS.findAll(before.substring(0, owner.range.first)).lastOrNull()?.groupValues?.get(1) + private fun bodies(structure: String, declaration: Regex, scope: (MatchResult) -> Scope): Map = + buildMap { + declaration.findAll(structure).forEach { match -> + val brace = structure.indexOf('{', match.range.last + 1) + if (brace >= 0) put(brace, scope(match)) + } + } + + private fun scopesAt(structure: String, bodies: Map, position: Int): List { + val scopes = ArrayList() + for (index in 0 until position) { + when (structure[index]) { + '{' -> scopes += bodies[index] + '}' -> scopes.removeLastOrNull() + } + } + return scopes + } + + private fun binaryName(scopes: List): String? { + val types = scopes.filterNotNull() + if (types.isEmpty() || types.size < scopes.size || types.any { it.kind == Kind.COMPANION }) return null + return types.joinToString("$") { it.name } } private fun facadeClassName(fileName: String): String { diff --git a/plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/MainClassFinderTest.kt b/plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/MainClassFinderTest.kt index 156136bd..49066808 100644 --- a/plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/MainClassFinderTest.kt +++ b/plugins/JVM-Tools/src/test/kotlin/com/appdevforall/jvm/plugin/MainClassFinderTest.kt @@ -31,6 +31,46 @@ class MainClassFinderTest { assertEquals(listOf("B"), MainClassFinder.mainClassNames("B.java", "class B { public static void main(String a[]) {} }")) } + @Test + fun `java main after a nested type belongs to the class that encloses it`() { + val source = """ + public class Main { + record Point(int x, int y) {} + static final String BRACE = "}"; + static final char OPEN = '{'; + public static void main(String[] args) {} + } + """.trimIndent() + + assertEquals(listOf("Main"), MainClassFinder.mainClassNames("Main.java", source)) + } + + @Test + fun `java main in a nested type runs under its binary name`() { + val source = """ + package com.example; + + public class Outer { + static class Inner { + public static void main(String[] args) {} + } + interface Tool { + static void main(String[] args) {} + } + void run() { + class Local { + public static void main(String[] args) {} + } + } + } + """.trimIndent() + + assertEquals( + listOf("com.example.Outer\$Inner", "com.example.Outer\$Tool"), + MainClassFinder.mainClassNames("Outer.java", source), + ) + } + @Test fun `java without a runnable main is not listed`() { val source = """ @@ -70,6 +110,27 @@ class MainClassFinderTest { assertEquals(listOf("Tool", "App"), MainClassFinder.mainClassNames("Both.kt", source)) } + @Test + fun `kotlin jvmstatic main in a nested object or companion runs under its binary name`() { + val source = """ + package tools + + class Outer { + val type = Outer::class + object Tool { + @JvmStatic fun main(args: Array) {} + } + class App(val name: String) { + companion object Factory { + @JvmStatic fun main(args: Array) {} + } + } + } + """.trimIndent() + + assertEquals(listOf("tools.Outer\$Tool", "tools.Outer\$App"), MainClassFinder.mainClassNames("Outer.kt", source)) + } + @Test fun `kotlin main inside a class without JvmStatic is not listed`() { assertEquals(emptyList(), MainClassFinder.mainClassNames("A.kt", "class A {\n fun main() {}\n}\n"))