├── .github └── workflows │ ├── editor-build.yml │ ├── editor-publish.yml │ ├── platform-build.yml │ └── platform-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── component ├── build.gradle.kts ├── buildSrc │ ├── build.gradle.kts │ ├── deps │ │ └── build.gradle.kts │ ├── settings.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ ├── commonMavenConfiguration.kt │ │ ├── localRepository.kt │ │ └── util.kt ├── editor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── n34t0 │ │ └── compose │ │ ├── codeEditor │ │ ├── CodeEditor.kt │ │ ├── ProjectFile.kt │ │ ├── Theme.kt │ │ ├── codecompletion │ │ │ ├── CodeCompletionIndexedElement.kt │ │ │ ├── CodeCompletionKeyEvents.kt │ │ │ ├── CodeCompletionListState.kt │ │ │ ├── CodeCompletionPopup.kt │ │ │ ├── CodeCompletionState.kt │ │ │ └── filters │ │ │ │ ├── Contains.kt │ │ │ │ ├── Filter.kt │ │ │ │ ├── Matches.kt │ │ │ │ └── StartsWith.kt │ │ ├── diagnostics │ │ │ ├── DiagnosticElement.kt │ │ │ ├── DiagnosticElementState.kt │ │ │ ├── DiagnosticState.kt │ │ │ ├── DiagnosticTooltip.kt │ │ │ ├── DiagnosticTooltipState.kt │ │ │ └── Severity.kt │ │ ├── editor │ │ │ ├── Editor.kt │ │ │ ├── EditorKeyEvents.kt │ │ │ ├── EditorState.kt │ │ │ ├── OffsetState.kt │ │ │ ├── draw │ │ │ │ ├── BackgroundDrawer.kt │ │ │ │ ├── DrawState.kt │ │ │ │ ├── HighlightDrawer.kt │ │ │ │ ├── LineSegment.kt │ │ │ │ └── WavedLineDrawer.kt │ │ │ ├── text │ │ │ │ ├── EditorTextField.kt │ │ │ │ ├── EditorTextFieldState.kt │ │ │ │ ├── LineNumbers.kt │ │ │ │ ├── Paddings.kt │ │ │ │ └── TextState.kt │ │ │ └── tooltip │ │ │ │ ├── EditorTooltip.kt │ │ │ │ └── EditorTooltipState.kt │ │ ├── gtd │ │ │ └── GtdState.kt │ │ ├── keyevent │ │ │ ├── KeyEventHandler.kt │ │ │ └── KeyEventHandlerImpl.kt │ │ ├── search │ │ │ ├── SearchBar.kt │ │ │ ├── SearchKeyEvents.kt │ │ │ └── SearchState.kt │ │ ├── statusbar │ │ │ ├── BusyState.kt │ │ │ └── StatusBar.kt │ │ └── tooltip │ │ │ └── Tooltip.kt │ │ ├── fork │ │ ├── ActualJvm.kt │ │ ├── DesktopPlatform.desktop.kt │ │ ├── ListUtils.kt │ │ ├── TempListUtils.kt │ │ ├── gestures │ │ │ └── TapGestureDetector.kt │ │ └── text │ │ │ ├── ContextMenu.desktop.kt │ │ │ ├── CoreText.kt │ │ │ ├── CoreTextField.kt │ │ │ ├── DesktopCursorHandle.desktop.kt │ │ │ ├── InlineTextContent.kt │ │ │ ├── KeyCommand.kt │ │ │ ├── KeyEventHelpers.desktop.kt │ │ │ ├── KeyMapping.desktop.kt │ │ │ ├── KeyMapping.kt │ │ │ ├── KeyboardActionRunner.kt │ │ │ ├── LongPressTextDragObserver.kt │ │ │ ├── MaxLinesHeightModifier.kt │ │ │ ├── PointerIconModifier.desktop.kt │ │ │ ├── StringHelpers.desktop.kt │ │ │ ├── StringHelpers.kt │ │ │ ├── TextFieldCursor.kt │ │ │ ├── TextFieldDelegate.kt │ │ │ ├── TextFieldGestureModifiers.kt │ │ │ ├── TextFieldKeyInput.desktop.kt │ │ │ ├── TextFieldKeyInput.kt │ │ │ ├── TextFieldPressGestureFilter.kt │ │ │ ├── TextFieldScroll.kt │ │ │ ├── TextFieldSize.kt │ │ │ ├── TextLayoutResultProxy.kt │ │ │ ├── TouchMode.desktop.kt │ │ │ ├── UndoManager.jvm.kt │ │ │ ├── UndoManager.kt │ │ │ └── selection │ │ │ ├── DesktopSelectionHandles.desktop.kt │ │ │ ├── MultiWidgetSelectionDelegate.kt │ │ │ ├── Selectable.kt │ │ │ ├── Selection.kt │ │ │ ├── SelectionAdjustment.kt │ │ │ ├── SelectionHandles.kt │ │ │ ├── SelectionManager.desktop.kt │ │ │ ├── SelectionManager.kt │ │ │ ├── SelectionMode.kt │ │ │ ├── SelectionRegistrar.kt │ │ │ ├── SelectionRegistrarImpl.kt │ │ │ ├── SimpleLayout.kt │ │ │ ├── TextFieldSelectionDelegate.kt │ │ │ ├── TextFieldSelectionManager.desktop.kt │ │ │ ├── TextFieldSelectionManager.kt │ │ │ ├── TextPreparedSelection.kt │ │ │ ├── TextSelectionDelegate.kt │ │ │ └── TextSelectionMouseDetector.kt │ │ └── stubs │ │ └── PlatformStub.kt ├── gradle.properties ├── gradle │ ├── project.properties │ ├── projectProperties.gradle.kts │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── platform │ ├── api │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── n34t0 │ │ │ └── platform │ │ │ ├── CodeCompletionElement.java │ │ │ ├── GotoDeclarationData.java │ │ │ ├── GotoDeclarationTarget.java │ │ │ ├── Platform.java │ │ │ └── Project.java │ ├── build.gradle.kts │ └── lib │ │ ├── build.gradle.kts │ │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ ├── com │ │ │ │ │ └── intellij │ │ │ │ │ │ ├── codeInsight │ │ │ │ │ │ ├── daemon │ │ │ │ │ │ │ └── impl │ │ │ │ │ │ │ │ └── IpwStatusBarUpdater.java │ │ │ │ │ │ └── lookup │ │ │ │ │ │ │ └── impl │ │ │ │ │ │ │ └── IpwLookupImpl.java │ │ │ │ │ │ ├── ide │ │ │ │ │ │ ├── IpwEventQueue.java │ │ │ │ │ │ └── ui │ │ │ │ │ │ │ └── laf │ │ │ │ │ │ │ └── IpwLafManagerImpl.java │ │ │ │ │ │ ├── openapi │ │ │ │ │ │ ├── editor │ │ │ │ │ │ │ └── impl │ │ │ │ │ │ │ │ └── IpwEditorImpl.java │ │ │ │ │ │ └── progress │ │ │ │ │ │ │ └── util │ │ │ │ │ │ │ ├── IpwPotemkinProgress.java │ │ │ │ │ │ │ └── IpwProgressWindow.java │ │ │ │ │ │ └── ui │ │ │ │ │ │ └── IpwEditorNotificationsImpl.java │ │ │ │ └── io │ │ │ │ │ └── github │ │ │ │ │ └── n34t0 │ │ │ │ │ └── platform │ │ │ │ │ └── impl │ │ │ │ │ ├── CcElement.java │ │ │ │ │ ├── GTDData.java │ │ │ │ │ ├── GTDTarget.java │ │ │ │ │ ├── IntellijPlatformWrapper.java │ │ │ │ │ ├── IpwProject.java │ │ │ │ │ ├── edt │ │ │ │ │ ├── EdtAdapter.java │ │ │ │ │ └── EdtUtil.java │ │ │ │ │ └── services │ │ │ │ │ ├── ApplicationService.java │ │ │ │ │ ├── EditorService.java │ │ │ │ │ ├── FileService.java │ │ │ │ │ ├── ModuleService.java │ │ │ │ │ ├── ProjectService.java │ │ │ │ │ ├── Service.java │ │ │ │ │ └── impl │ │ │ │ │ ├── AbstractModuleService.java │ │ │ │ │ ├── AbstractProjectService.java │ │ │ │ │ ├── ApplicationServiceImpl.java │ │ │ │ │ ├── EditorServiceImpl.java │ │ │ │ │ ├── FileServiceImpl.java │ │ │ │ │ ├── GotoDeclarationService.java │ │ │ │ │ ├── InspectionsUtil.java │ │ │ │ │ ├── IpwDataProvider.java │ │ │ │ │ ├── IpwService.java │ │ │ │ │ ├── IpwUtil.java │ │ │ │ │ ├── ModuleServiceImpl.java │ │ │ │ │ ├── ProjectServiceImpl.java │ │ │ │ │ ├── TempModuleServiceImpl.java │ │ │ │ │ └── TempProjectServiceImpl.java │ │ │ └── resources │ │ │ │ ├── META-INF │ │ │ │ ├── AnalysisImpl.xml │ │ │ │ ├── Editor.xml │ │ │ │ ├── ExternalSystemExtensions.xml │ │ │ │ ├── LangExtensionPoints.xml │ │ │ │ ├── LangExtensions.xml │ │ │ │ └── PlatformExtensions.xml │ │ │ │ └── componentSets │ │ │ │ └── PlatformLangComponents.xml │ │ └── test │ │ │ ├── java │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── n34t0 │ │ │ │ └── platform │ │ │ │ ├── AddLibFileTests.java │ │ │ │ ├── AddLibProjectTests.java │ │ │ │ ├── GTDTests.java │ │ │ │ ├── IsolationTests.java │ │ │ │ ├── MultipleOpenFilesTests.java │ │ │ │ ├── MultipleProjectsTests.java │ │ │ │ ├── ProjectFileOperationsTests.java │ │ │ │ ├── SimpleTests.java │ │ │ │ ├── UpdateTests.java │ │ │ │ └── WrapperExtension.java │ │ │ └── resources │ │ │ └── log4j.properties │ │ └── testData │ │ ├── files │ │ ├── GTDTest.java │ │ ├── GTDTest.kt │ │ ├── IsolationTest1.java │ │ ├── IsolationTest2.java │ │ ├── LibTest.java │ │ ├── LibTest2.java │ │ ├── ListTest.kt │ │ ├── TestAr.java │ │ ├── TestSo.java │ │ ├── UpdateTest.java │ │ └── test.kt │ │ ├── lib │ │ ├── api.jar │ │ ├── implA.jar │ │ └── sub │ │ │ ├── implB.jar │ │ │ └── implC.jar │ │ └── projects │ │ ├── GTDJavaProject │ │ └── org │ │ │ └── example │ │ │ ├── Main.java │ │ │ └── a │ │ │ └── ClassA.java │ │ ├── GTDKotlinProject │ │ └── org │ │ │ └── example │ │ │ ├── a │ │ │ └── Sum.kt │ │ │ └── main.kt │ │ ├── fileOperationsProject │ │ └── org │ │ │ └── example │ │ │ ├── Main.java │ │ │ └── a │ │ │ ├── Api.java │ │ │ └── ImplA.java │ │ ├── isolationLibProject1 │ │ └── org │ │ │ └── example │ │ │ ├── Impl1.java │ │ │ └── Main.java │ │ ├── isolationLibProject2 │ │ └── org │ │ │ └── example │ │ │ ├── Impl2.java │ │ │ └── Main.java │ │ ├── isolationProject1 │ │ └── org │ │ │ └── example │ │ │ ├── IsoClass1.java │ │ │ └── Main.java │ │ ├── isolationProject2 │ │ └── org │ │ │ └── example │ │ │ ├── IsoClass2.java │ │ │ └── Main.java │ │ ├── javaProject │ │ └── org │ │ │ └── example │ │ │ ├── Main.java │ │ │ └── a │ │ │ └── ClassA.java │ │ ├── libJavaProject │ │ └── org │ │ │ └── example │ │ │ ├── ClassD.java │ │ │ └── Main.java │ │ ├── libJavaProject2 │ │ └── org │ │ │ └── example │ │ │ ├── ClassD.java │ │ │ └── Main.java │ │ ├── simpleJavaProject │ │ └── Test.java │ │ ├── simpleKotlinProject │ │ └── test.kt │ │ └── updateJavaProject │ │ └── org │ │ └── example │ │ ├── Main.java │ │ └── a │ │ └── ClassA.java └── settings.gradle.kts ├── demo ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jars │ └── kotlin-stdlib.jar ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── github │ └── n34t0 │ └── compose │ └── codeEditor │ └── demo │ ├── diagnostics.kt │ └── main.kt ├── gradle.properties └── screenshots └── codeEditor.gif /.github/workflows/editor-build.yml: -------------------------------------------------------------------------------- 1 | name: Editor build 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up JDK 11 12 | uses: actions/setup-java@v2 13 | with: 14 | java-version: '11' 15 | distribution: 'adopt' 16 | - name: Validate Gradle wrapper 17 | uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b 18 | - name: Build Editor with Gradle 19 | run: | 20 | cd component/editor 21 | chmod +x ../gradlew 22 | ../gradlew build -------------------------------------------------------------------------------- /.github/workflows/editor-publish.yml: -------------------------------------------------------------------------------- 1 | name: Editor publication 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release: 7 | description: 'Release (yes/no)' 8 | required: false 9 | default: 'no' 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up JDK 11 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '11' 21 | distribution: 'adopt' 22 | - name: Validate Gradle wrapper 23 | uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b 24 | - name: Set publication type 25 | if: ${{ github.event.inputs.release == 'yes' }} 26 | run: | 27 | echo "release_parameter=-Prelease" >> $GITHUB_ENV 28 | - name: Publish Editor with Gradle 29 | env: 30 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} 31 | ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.SIGNING_KEYID }} 32 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} 33 | ORG_GRADLE_PROJECT_nexusUsername: ${{ secrets.NEXUS_USERNAME }} 34 | ORG_GRADLE_PROJECT_nexusPassword: ${{ secrets.NEXUS_PASSWORD }} 35 | run: | 36 | cd component/editor 37 | chmod +x ../gradlew 38 | ../gradlew publish ${{ env.release_parameter }} 39 | - name: Release 40 | if: ${{ github.event.inputs.release == 'yes' }} 41 | run: | 42 | cd component 43 | ./gradlew closeAndReleaseRepository 44 | env: 45 | ORG_GRADLE_PROJECT_nexusUsername: ${{ secrets.NEXUS_USERNAME }} 46 | ORG_GRADLE_PROJECT_nexusPassword: ${{ secrets.NEXUS_PASSWORD }} 47 | -------------------------------------------------------------------------------- /.github/workflows/platform-build.yml: -------------------------------------------------------------------------------- 1 | name: Platform build 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up JDK 11 12 | uses: actions/setup-java@v2 13 | with: 14 | java-version: '11' 15 | distribution: 'adopt' 16 | - name: Validate Gradle wrapper 17 | uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b 18 | - name: Build Platform with Gradle 19 | run: | 20 | cd component/platform 21 | chmod +x ../gradlew 22 | ../gradlew build -------------------------------------------------------------------------------- /.github/workflows/platform-publish.yml: -------------------------------------------------------------------------------- 1 | name: Platform publication 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release: 7 | description: 'Release (yes/no)' 8 | required: false 9 | default: 'no' 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up JDK 11 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '11' 21 | distribution: 'adopt' 22 | - name: Validate Gradle wrapper 23 | uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b 24 | - name: Set publication type 25 | if: ${{ github.event.inputs.release == 'yes' }} 26 | run: | 27 | echo "release_parameter=-Prelease" >> $GITHUB_ENV 28 | - name: Publish Platform with Gradle 29 | env: 30 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} 31 | ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.SIGNING_KEYID }} 32 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} 33 | ORG_GRADLE_PROJECT_nexusUsername: ${{ secrets.NEXUS_USERNAME }} 34 | ORG_GRADLE_PROJECT_nexusPassword: ${{ secrets.NEXUS_PASSWORD }} 35 | run: | 36 | cd component/platform 37 | chmod +x ../gradlew 38 | ../gradlew publish ${{ env.release_parameter }} 39 | - name: Release 40 | if: ${{ github.event.inputs.release == 'yes' }} 41 | run: | 42 | cd component 43 | ./gradlew closeAndReleaseRepository 44 | env: 45 | ORG_GRADLE_PROJECT_nexusUsername: ${{ secrets.NEXUS_USERNAME }} 46 | ORG_GRADLE_PROJECT_nexusPassword: ${{ secrets.NEXUS_PASSWORD }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compose Code Editor 2 | 3 | A desktop code editor app using Jetpack Compose for Desktop and IntelliJ Platform. 4 | 5 | ## Project Structure 6 | 7 | The code is contained in the `component` folder. There are several modules: 8 | - `:editor` - Compose Code Editor component code 9 | - `:platform:api` - API of the platform that implements the IDE functions 10 | - `:platform:lib` - API implementation based on IntelliJ Platform 11 | 12 | The demo application is contained in the `demo` folder. 13 | 14 | ## Running demo application 15 | 16 | Make sure that the `JAVA_HOME` environment variable is set before starting. For example, in macOS, just run: 17 | ```shell 18 | export JAVA_HOME=$(/usr/libexec/java_home) 19 | ``` 20 | 21 | Running application: 22 | 23 | ```shell 24 | cd demo 25 | ./gradlew run 26 | ``` 27 | 28 | ![Demo](screenshots/codeEditor.gif) 29 | 30 | ## Features 31 | 32 | - Auto-suggestion and code completion: `Ctrl+Space` 33 | - Go to declaration: `Ctrl+B` (`cmd+B` on mac) or by click with `Ctrl` (`cmd` on mac) 34 | - Search: 35 | - `Ctrl+F` open the search bar 36 | - `Ctrl+Enter` return the focus to the editor 37 | - `F3`, `Shift+F3` move to the next, previous search result. The search bar also supports the `Up` `Down` `Enter` `Shift+Enter` keys for moving through search results 38 | - `Esc` close the search bar 39 | - Functionality for highlighting code sections with the output of diagnostic messages 40 | 41 | ## Usage 42 | 43 | ### Initialization 44 | 45 | Create an instance of the `Platform` and initialize it. Only one initialized instance can exist at runtime. The platform cannot be re-initialized after stopping. 46 | 47 | ```kotlin 48 | val platform = createPlatformInstance() 49 | platform.init() 50 | ``` 51 | 52 | ### Creating project 53 | 54 | Open a project or a separate file and get an instance of the `Project`. 55 | 56 | ```kotlin 57 | val project = platform.openProject("/path/to/project") 58 | val singleFileProject = platform.openFile("/path/to/file") 59 | ``` 60 | 61 | ### Adding libraries 62 | 63 | ```kotlin 64 | project.addLibraries("jars/kotlin-stdlib.jar") 65 | ``` 66 | 67 | If a directory is specified, all libraries from it and all its subdirectories will be loaded. 68 | 69 | ### Creating compose component 70 | 71 | Create an instance of the `ProjectFile` that contains information about the project and the file to be edited. 72 | And pass it to the `CodeEditor` component. 73 | 74 | ```kotlin 75 | val projectFile = createProjectFile( 76 | project = project, 77 | projectDir = "/path/to/project", 78 | absoluteFilePath = "/file/to/edit" 79 | ) 80 | 81 | CodeEditor(projectFile) 82 | 83 | val singleFile = createProjectFile( 84 | project = singleFileProject, 85 | absoluteFilePath = "/path/to/file" 86 | ) 87 | 88 | CodeEditor(singleFile) 89 | ``` 90 | 91 | ### Closing project 92 | 93 | ```kotlin 94 | project.closeProject() 95 | ``` 96 | 97 | ### Stopping 98 | 99 | ```kotlin 100 | platform.stop() 101 | ``` 102 | 103 | Closes all open projects and deletes temporary files. 104 | 105 | ### Diagnostic messages 106 | 107 | The editor supports the output of diagnostic messages. 108 | To do this, pass the list of `DiagnosticElement` to the component. 109 | 110 | -------------------------------------------------------------------------------- /component/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("io.codearte.nexus-staging") version "0.30.0" 3 | } 4 | 5 | nexusStaging { 6 | serverUrl = "https://s01.oss.sonatype.org/service/local/" 7 | packageGroup = "io.github.n34t0" 8 | } 9 | 10 | subprojects { 11 | group = "io.github.n34t0" 12 | 13 | plugins.withId("java") { 14 | extensions.getByType(JavaPluginExtension::class.java).apply { 15 | sourceCompatibility = JavaVersion.VERSION_11 16 | targetCompatibility = JavaVersion.VERSION_11 17 | 18 | withJavadocJar() 19 | withSourcesJar() 20 | } 21 | } 22 | 23 | plugins.withId("maven-publish") { 24 | extensions.getByType(PublishingExtension::class.java).apply { 25 | repositories { 26 | maven { 27 | val releaseRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" 28 | val snapshotRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" 29 | url = uri(if (hasProperty("release")) releaseRepoUrl else snapshotRepoUrl) 30 | credentials { 31 | username = (findProperty("nexusUsername") ?: "").toString() 32 | password = (findProperty("nexusPassword") ?: "").toString() 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | tasks.withType().configureEach { 40 | onlyIf { 41 | gradle.taskGraph.allTasks.any { it.name.endsWith("publish") } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /component/buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | rootProject.apply { 6 | from(rootProject.file("../gradle/projectProperties.gradle.kts")) 7 | } 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30") 15 | } 16 | -------------------------------------------------------------------------------- /component/buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include("deps") 2 | -------------------------------------------------------------------------------- /component/buildSrc/src/main/kotlin/commonMavenConfiguration.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.publish.maven.MavenPom 2 | 3 | fun MavenPom.addCommonPom() { 4 | url.set("https://github.com/n34t0/compose-code-editor") 5 | licenses { 6 | license { 7 | name.set("The Apache License, Version 2.0") 8 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 9 | } 10 | } 11 | developers { 12 | developer { 13 | id.set("n34t0") 14 | name.set("Alex Hosh") 15 | email.set("n34to0@gmail.com") 16 | } 17 | } 18 | scm { 19 | connection.set("scm:git:git://github.com/n34t0/compose-code-editor.git") 20 | developerConnection.set("scm:git:ssh://github.com:n34t0/compose-code-editor.git") 21 | url.set("https://github.com/n34t0/compose-code-editor/tree/master") 22 | } 23 | } -------------------------------------------------------------------------------- /component/buildSrc/src/main/kotlin/localRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2017 JetBrains s.r.o. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * Modified by Alex Hosh (n34to0@gmail.com) 2021. 17 | */ 18 | 19 | // usages in build scripts are not tracked properly 20 | @file:Suppress("unused") 21 | 22 | import org.gradle.api.Project 23 | import org.gradle.api.artifacts.ModuleDependency 24 | import org.gradle.api.artifacts.dsl.RepositoryHandler 25 | import org.gradle.api.artifacts.repositories.IvyArtifactRepository 26 | import java.io.File 27 | 28 | private fun Project.buildLocalDependenciesDir(): File = 29 | rootProject.gradle.gradleUserHomeDir.resolve(rootProject.findProperty("deps.rootFolder") as String) 30 | 31 | private fun Project.buildOrganization(): String = rootProject.findProperty("deps.organization") as String 32 | 33 | private fun Project.buildLocalRepoDir(): File = buildLocalDependenciesDir().resolve("repo") 34 | 35 | private fun Project.ideModuleName(): String = rootProject.findProperty("intellijSdk.platform") as String 36 | 37 | private fun Project.ideModuleVersion(): String = rootProject.findProperty("intellijSdk.version") as String 38 | 39 | fun RepositoryHandler.buildLocalRepo(project: Project): IvyArtifactRepository = ivy { 40 | val baseDir = project.buildLocalRepoDir() 41 | url = baseDir.toURI() 42 | 43 | patternLayout { 44 | ivy("[organisation]/[module]/[revision]/[module].ivy.xml") 45 | ivy("[organisation]/[module]/[revision]/ivy/[module].ivy.xml") 46 | ivy("[organisation]/${project.ideModuleName()}/[revision]/ivy/[module].ivy.xml") // bundled plugins 47 | 48 | artifact("[organisation]/[module]/[revision]/artifacts/lib/[artifact](-[classifier]).[ext]") 49 | artifact("[organisation]/[module]/[revision]/artifacts/[artifact](-[classifier]).[ext]") 50 | artifact("[organisation]/intellij-core/[revision]/artifacts/[artifact](-[classifier]).[ext]") 51 | artifact("[organisation]/${project.ideModuleName()}/[revision]/artifacts/plugins/[module]/lib/[artifact](-[classifier]).[ext]") // bundled plugins 52 | artifact("[organisation]/sources/[artifact]-[revision](-[classifier]).[ext]") 53 | artifact("[organisation]/[module]/[revision]/[artifact](-[classifier]).[ext]") 54 | } 55 | 56 | metadataSources { 57 | ivyDescriptor() 58 | } 59 | } 60 | 61 | @kotlin.jvm.JvmOverloads 62 | fun Project.intellijDep(module: String? = null) = 63 | "${buildOrganization()}:${module ?: ideModuleName()}:${ideModuleVersion()}" 64 | 65 | fun Project.intellijPluginDep(plugin: String) = intellijDep(plugin) 66 | 67 | fun Project.intellijPlatformDir(): File = buildLocalRepoDir().resolve(buildOrganization()) 68 | .resolve(ideModuleName()) 69 | .resolve(ideModuleVersion()) 70 | .resolve("artifacts") 71 | 72 | fun ModuleDependency.includeJars(vararg names: String, rootProject: Project? = null) { 73 | names.forEach { 74 | var baseName = it.removeSuffix(".jar") 75 | if (rootProject != null && rootProject.extensions.extraProperties.has("versions.jar.$baseName")) { 76 | baseName += "-${rootProject.extensions.extraProperties["versions.jar.$baseName"]}" 77 | } 78 | artifact { 79 | name = baseName 80 | type = "jar" 81 | extension = "jar" 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /component/buildSrc/src/main/kotlin/util.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Project 2 | 3 | fun Project.getVersion(moduleName: String) = 4 | (property("${moduleName}.version") as String) + if (!hasProperty("release")) "-SNAPSHOT" else "" 5 | -------------------------------------------------------------------------------- /component/editor/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.compose 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | plugins { 5 | kotlin("jvm") 6 | `maven-publish` 7 | signing 8 | id("org.jetbrains.compose") version "1.0.0-alpha4-build331" 9 | } 10 | 11 | group = group as String + ".compose" 12 | version = getVersion("editor") 13 | val platformVersion = getVersion("platform") 14 | 15 | repositories { 16 | mavenLocal() 17 | mavenCentral() 18 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 19 | google() 20 | } 21 | 22 | dependencies { 23 | implementation(compose.desktop.currentOs) 24 | implementation(compose.materialIconsExtended) 25 | api(project(":platform:api")) 26 | implementation(project(":platform:lib")) { 27 | exclude("platform.build") 28 | } 29 | 30 | runtimeOnly("io.github.n34t0:platform-lib:${platformVersion}:idea") 31 | runtimeOnly("io.github.n34t0:platform-lib:${platformVersion}:java") 32 | runtimeOnly("io.github.n34t0:platform-lib:${platformVersion}:kotlin") 33 | runtimeOnly("io.github.n34t0:platform-lib:${platformVersion}:properties") 34 | } 35 | 36 | tasks.withType().configureEach { 37 | kotlinOptions.jvmTarget = "11" 38 | kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" 39 | } 40 | 41 | tasks.jar { 42 | manifest { 43 | attributes( 44 | mapOf( 45 | "Implementation-Title" to "code-editor", 46 | "Implementation-Version" to project.version 47 | ) 48 | ) 49 | } 50 | } 51 | 52 | publishing { 53 | publications { 54 | create("mavenCodeEditor") { 55 | artifactId = "code-editor" 56 | 57 | from(components["java"]) 58 | 59 | pom { 60 | name.set("Code Editor compose component") 61 | description.set("A code editor compose component") 62 | addCommonPom() 63 | } 64 | } 65 | } 66 | } 67 | 68 | signing { 69 | val signingKeyId: String? by project 70 | val signingKey: String? by project 71 | val signingPassword: String? by project 72 | useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) 73 | sign(publishing.publications["mavenCodeEditor"]) 74 | } -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/CodeEditor.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.Surface 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.rememberCoroutineScope 8 | import androidx.compose.runtime.saveable.rememberSaveable 9 | import androidx.compose.ui.Modifier 10 | import io.github.n34t0.compose.codeEditor.diagnostics.DiagnosticElement 11 | import io.github.n34t0.compose.codeEditor.editor.Editor 12 | import io.github.n34t0.compose.codeEditor.editor.EditorState 13 | import io.github.n34t0.compose.codeEditor.search.SearchBar 14 | import io.github.n34t0.compose.codeEditor.statusbar.StatusBar 15 | import io.github.n34t0.compose.stubs.PlatformStub 16 | import io.github.n34t0.compose.stubs.isStub 17 | import io.github.n34t0.platform.Project 18 | import io.github.n34t0.platform.impl.IntellijPlatformWrapper 19 | 20 | /** 21 | * This composable provides basic code editing functionality with support for autosuggestion of code, 22 | * search and go to declaration. 23 | * 24 | * @param projectFile The [ProjectFile] that contains information about the project and the file being edited. 25 | * @param modifier optional [Modifier]. 26 | * @param diagnostics List of diagnostic messages to be rendered in the code. 27 | * @param onTextChange Called when code text is changed. 28 | */ 29 | @Composable 30 | fun CodeEditor( 31 | projectFile: ProjectFile, 32 | modifier: Modifier, 33 | diagnostics: List, 34 | onTextChange: (String) -> Unit, 35 | ) { 36 | val scope = rememberCoroutineScope() 37 | 38 | val editorState = rememberSaveable { 39 | EditorState( 40 | projectFile = projectFile, 41 | scope = scope, 42 | onOuterGotoDeclaration = { _, _, _ -> } 43 | ) 44 | } 45 | 46 | editorState.setDiagnostics(diagnostics) 47 | 48 | MaterialTheme( 49 | colors = AppTheme.colors.material, 50 | typography = AppTheme.typography.material 51 | ) { 52 | Surface { 53 | Column(modifier) { 54 | SearchBar(editorState.searchState) 55 | Editor(editorState, onTextChange, Modifier.weight(1f)) 56 | StatusBar(editorState.diagnosticMessagesUnderCaret, editorState.busyState) 57 | } 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * Returns an instance of the [ProjectFile]. 64 | * 65 | * @param project The project to which the file belongs. 66 | * @param projectDir Path to the project folder. 67 | * @param absoluteFilePath Absolute path to the file. 68 | */ 69 | fun createProjectFile( 70 | project: Project, 71 | projectDir: String?, 72 | absoluteFilePath: String 73 | ) = ProjectFile(project, projectDir, absoluteFilePath) 74 | 75 | /** 76 | * Returns an instance of the [Platform]. 77 | */ 78 | fun createPlatformInstance() = if (!isStub) IntellijPlatformWrapper() else PlatformStub() 79 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/ProjectFile.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor 2 | 3 | import androidx.compose.runtime.Stable 4 | import io.github.n34t0.platform.CodeCompletionElement 5 | import io.github.n34t0.platform.GotoDeclarationData 6 | import io.github.n34t0.platform.Project 7 | import java.nio.file.Path 8 | import kotlin.io.path.readText 9 | import kotlin.io.path.writeText 10 | 11 | @Stable 12 | class ProjectFile( 13 | val project: Project, 14 | val projectDir: String?, 15 | val absoluteFilePath: String 16 | ) { 17 | private val filePath: Path = Path.of(absoluteFilePath) 18 | val relativeFilePath: String = projectDir?.let { 19 | absoluteFilePath.substring(projectDir.length + 1) 20 | } ?: filePath.fileName.toString() 21 | 22 | init { 23 | projectDir?.let { require(absoluteFilePath.startsWith(projectDir)) } 24 | } 25 | 26 | fun load(): String = filePath.readText() 27 | 28 | fun save(text: String) = filePath.writeText(text) 29 | 30 | fun getCodeCompletionList(text: String, caretOffset: Int): List { 31 | save(text) 32 | return project.getCodeCompletion(absoluteFilePath, caretOffset) 33 | } 34 | 35 | fun getGotoDeclarationData(text: String, caretOffset: Int): GotoDeclarationData { 36 | save(text) 37 | return project.gotoDeclaration(absoluteFilePath, caretOffset) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/codecompletion/CodeCompletionIndexedElement.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.codecompletion 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import io.github.n34t0.platform.CodeCompletionElement 7 | 8 | internal class CodeCompletionIndexedElement( 9 | val id: Int, 10 | val element: CodeCompletionElement, 11 | val onClick: () -> Unit = {}, 12 | val onDoubleClick: () -> Unit = {} 13 | ) { 14 | var selected by mutableStateOf(false) 15 | private set 16 | 17 | fun select() { 18 | selected = true 19 | } 20 | 21 | fun unselect() { 22 | selected = false 23 | } 24 | 25 | override fun toString(): String { 26 | return "CodeCompletionIndexedElement(id=$id, element=$element)" 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/codecompletion/filters/Contains.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.codecompletion.filters 2 | 3 | import io.github.n34t0.compose.codeEditor.codecompletion.CodeCompletionIndexedElement 4 | 5 | internal class Contains : Filter { 6 | override fun matches( 7 | element: CodeCompletionIndexedElement, 8 | prefix: String, 9 | ignoreCase: Boolean 10 | ): Boolean = element.element.name.contains(prefix, ignoreCase) 11 | } 12 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/codecompletion/filters/Filter.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.codecompletion.filters 2 | 3 | import io.github.n34t0.compose.codeEditor.codecompletion.CodeCompletionIndexedElement 4 | 5 | internal interface Filter { 6 | fun matches( 7 | element: CodeCompletionIndexedElement, 8 | prefix: String, 9 | ignoreCase: Boolean = true 10 | ): Boolean 11 | } 12 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/codecompletion/filters/Matches.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.codecompletion.filters 2 | 3 | import io.github.n34t0.compose.codeEditor.codecompletion.CodeCompletionIndexedElement 4 | 5 | internal class Matches : Filter { 6 | override fun matches( 7 | element: CodeCompletionIndexedElement, 8 | prefix: String, 9 | ignoreCase: Boolean 10 | ) = element.element.name.equals(prefix, ignoreCase) 11 | } 12 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/codecompletion/filters/StartsWith.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.codecompletion.filters 2 | 3 | import io.github.n34t0.compose.codeEditor.codecompletion.CodeCompletionIndexedElement 4 | 5 | internal class StartsWith : Filter { 6 | override fun matches( 7 | element: CodeCompletionIndexedElement, 8 | prefix: String, 9 | ignoreCase: Boolean 10 | ) = element.element.name.startsWith(prefix, ignoreCase) 11 | } 12 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/diagnostics/DiagnosticElement.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.diagnostics 2 | 3 | /** 4 | * Contains information about the diagnostic message and its location in the code. 5 | * 6 | * Line numbering begins with 1. 7 | * Character numbering begins with 0. 8 | */ 9 | data class DiagnosticElement( 10 | val startLine: Int, 11 | val startCharacter: Int, 12 | val endLine: Int, 13 | val endCharacter: Int, 14 | val message: String, 15 | val severity: Severity 16 | ) 17 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/diagnostics/DiagnosticElementState.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.diagnostics 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.runtime.derivedStateOf 5 | import androidx.compose.runtime.getValue 6 | import io.github.n34t0.compose.codeEditor.editor.text.TextState 7 | 8 | @Stable 9 | internal class DiagnosticElementState( 10 | val message: String, 11 | val severity: Severity, 12 | val startLine: Int, 13 | val startCharacter: Int, 14 | val endLine: Int, 15 | val endCharacter: Int, 16 | private val textState: TextState 17 | ) { 18 | val startTextOffset by derivedStateOf { textState.getOffsetForCharacter(startLine - 1, startCharacter) } 19 | val endTextOffset by derivedStateOf { textState.getOffsetForCharacter(endLine - 1, endCharacter) } 20 | } 21 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/diagnostics/DiagnosticState.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.diagnostics 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.runtime.derivedStateOf 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateListOf 7 | import io.github.n34t0.compose.codeEditor.AppTheme 8 | import io.github.n34t0.compose.codeEditor.editor.draw.DrawState 9 | import io.github.n34t0.compose.codeEditor.editor.draw.LineSegment 10 | import io.github.n34t0.compose.codeEditor.editor.text.TextState 11 | import kotlinx.coroutines.CoroutineScope 12 | 13 | @Stable 14 | internal class DiagnosticState( 15 | scope: CoroutineScope, 16 | textState: TextState, 17 | private val drawState: DrawState 18 | ) { 19 | 20 | val tooltipState = DiagnosticTooltipState(scope) 21 | 22 | val diagnostics = mutableStateListOf() 23 | 24 | val list by derivedStateOf { 25 | diagnostics.map { 26 | DiagnosticElementState( 27 | message = it.message, 28 | severity = it.severity, 29 | startLine = it.startLine, 30 | startCharacter = it.startCharacter, 31 | endLine = it.endLine, 32 | endCharacter = it.endCharacter, 33 | textState = textState 34 | ) 35 | }.sortedBy { it.severity } 36 | } 37 | 38 | private val errorDrawer = drawState.createWavedLineDrawer(AppTheme.colors.severityError, zIndex = 19) 39 | private val warningDrawer = drawState.createBackgroundDrawer(AppTheme.colors.severityWarning, zIndex = 0) 40 | private val infoDrawer = drawState.createWavedLineDrawer(AppTheme.colors.severityInfo, zIndex = 18) 41 | 42 | init { 43 | drawState.putLineSegments( 44 | infoDrawer, 45 | derivedStateOf { mapToLineSegments(Severity.INFO) } 46 | ) 47 | drawState.putLineSegments( 48 | warningDrawer, 49 | derivedStateOf { mapToLineSegments(Severity.WARNING) } 50 | ) 51 | drawState.putLineSegments( 52 | errorDrawer, 53 | derivedStateOf { mapToLineSegments(Severity.ERROR) } 54 | ) 55 | } 56 | 57 | fun updateList(list: List) { 58 | diagnostics.clear() 59 | diagnostics.addAll(list) 60 | } 61 | 62 | private fun mapToLineSegments(severity: Severity): List { 63 | return list 64 | .filter { it.severity == severity } 65 | .flatMap { 66 | drawState.getLineSegments( 67 | startCharacter = it.startCharacter, 68 | endCharacter = it.endCharacter, 69 | startLine = it.startLine, 70 | endLine = it.endLine 71 | ) 72 | } 73 | } 74 | 75 | fun getDiagnosticMessagesForOffset( 76 | offset: Int, 77 | delimiter: String = ". ", 78 | ending: String = "." 79 | ): String = 80 | if (offset == -1) "" 81 | else { 82 | val filteredList = list 83 | .filter { offset >= it.startTextOffset && offset <= it.endTextOffset } 84 | when (filteredList.size) { 85 | 0 -> "" 86 | 1 -> filteredList[0].message 87 | else -> filteredList.joinToString(delimiter) { it.message } + ending 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/diagnostics/DiagnosticTooltip.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.diagnostics 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.ExperimentalComposeUiApi 6 | import io.github.n34t0.compose.codeEditor.editor.tooltip.Tooltip 7 | 8 | @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) 9 | @Composable 10 | internal fun DiagnosticTooltip( 11 | message: String, 12 | tooltipState: DiagnosticTooltipState 13 | ) { 14 | tooltipState.setMessage(message) 15 | 16 | if (tooltipState.isVisible) { 17 | Tooltip( 18 | message = tooltipState.message, 19 | positionProvider = tooltipState.placement.positionProvider(), 20 | onDismissRequest = tooltipState::hide 21 | ) 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/diagnostics/DiagnosticTooltipState.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.diagnostics 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.TooltipPlacement 5 | import androidx.compose.runtime.Stable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.setValue 9 | import androidx.compose.ui.ExperimentalComposeUiApi 10 | import androidx.compose.ui.unit.DpOffset 11 | import androidx.compose.ui.unit.dp 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.Job 14 | import kotlinx.coroutines.delay 15 | import kotlinx.coroutines.launch 16 | 17 | @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) 18 | @Stable 19 | internal class DiagnosticTooltipState( 20 | private val scope: CoroutineScope 21 | ) { 22 | var isVisible by mutableStateOf(false) 23 | val placement = TooltipPlacement.CursorPoint(DpOffset(0.dp, 16.dp)) 24 | private val delay = 500L 25 | private var job: Job? = null 26 | var message = "" 27 | private set 28 | 29 | fun setMessage(message: String) { 30 | if (this.message !== message) { 31 | if (message.isNotEmpty()) { 32 | job?.cancel() 33 | isVisible = false 34 | job = scope.launch { 35 | delay(delay) 36 | isVisible = true 37 | } 38 | } else { 39 | hide() 40 | } 41 | this.message = message 42 | } 43 | } 44 | 45 | fun hide() { 46 | job?.cancel() 47 | isVisible = false 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/diagnostics/Severity.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.diagnostics 2 | 3 | enum class Severity { ERROR, WARNING, INFO } 4 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/editor/OffsetState.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.editor 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.unit.IntOffset 8 | 9 | @Stable 10 | internal class OffsetState { 11 | companion object { 12 | val Unspecified = IntOffset(-1, -1) 13 | } 14 | 15 | var value by mutableStateOf(Unspecified) 16 | private set 17 | 18 | fun setX(x: Int) { 19 | value = value.copy(x = x) 20 | } 21 | 22 | fun setY(y: Int) { 23 | value = value.copy(y = y) 24 | } 25 | 26 | fun set(offset: IntOffset) { 27 | value = offset 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/editor/draw/BackgroundDrawer.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.editor.draw 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.FilterQuality 6 | import androidx.compose.ui.graphics.Paint 7 | import androidx.compose.ui.graphics.PaintingStyle 8 | import androidx.compose.ui.graphics.drawscope.DrawScope 9 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 10 | 11 | internal class BackgroundDrawer( 12 | private val backgroundColor: Color? = null, 13 | private val borderColor: Color? = null, 14 | private val height: State, 15 | zIndex: Int 16 | ) : HighlightDrawer(zIndex) { 17 | 18 | private val paintFill = backgroundColor?.let { 19 | Paint().apply { 20 | color = backgroundColor 21 | isAntiAlias = false 22 | filterQuality = FilterQuality.None 23 | } 24 | } 25 | 26 | private val paintBorder = borderColor?.let { 27 | Paint().apply { 28 | color = borderColor 29 | style = PaintingStyle.Stroke 30 | strokeWidth = 1f 31 | isAntiAlias = false 32 | filterQuality = FilterQuality.None 33 | } 34 | } 35 | 36 | override fun draw(lineSegments: List, drawScope: DrawScope) { 37 | drawScope.drawIntoCanvas { canvas -> 38 | lineSegments.forEach { (xl, xr, y) -> 39 | paintFill?.let { 40 | canvas.drawRect(xl, y - height.value, xr, y, paintFill) 41 | } 42 | paintBorder?.let { 43 | canvas.drawRect(xl - strokeShift, y - height.value - strokeShift, 44 | xr + strokeShift, y + strokeShift, paintBorder) 45 | } 46 | } 47 | } 48 | } 49 | 50 | override fun equals(other: Any?): Boolean { 51 | if (this === other) return true 52 | if (javaClass != other?.javaClass) return false 53 | 54 | other as BackgroundDrawer 55 | 56 | if (backgroundColor != other.backgroundColor) return false 57 | if (borderColor != other.borderColor) return false 58 | 59 | return true 60 | } 61 | 62 | override fun hashCode(): Int { 63 | var result = backgroundColor?.hashCode() ?: 0 64 | result = 31 * result + (borderColor?.hashCode() ?: 0) 65 | return result 66 | } 67 | 68 | companion object { 69 | private const val strokeShift = .5f 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/editor/draw/DrawState.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.editor.draw 2 | 3 | import androidx.compose.foundation.ScrollState 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.State 6 | import androidx.compose.runtime.derivedStateOf 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.draw.drawWithCache 9 | import androidx.compose.ui.graphics.Color 10 | import io.github.n34t0.compose.codeEditor.editor.text.TextState 11 | import java.util.TreeMap 12 | 13 | @Stable 14 | internal class DrawState( 15 | private val textState: TextState 16 | ) { 17 | private val lineHeight = derivedStateOf { textState.lineHeight } 18 | 19 | val drawers = TreeMap>>() 20 | 21 | fun putLineSegments(drawer: HighlightDrawer, lineSegments: State>) { 22 | drawers[drawer] = lineSegments 23 | } 24 | 25 | fun removeDrawer(drawer: HighlightDrawer) { 26 | drawers.remove(drawer) 27 | } 28 | 29 | fun createWavedLineDrawer(color: Color, zIndex: Int): WavedLineDrawer = WavedLineDrawer(color, zIndex) 30 | 31 | fun createBackgroundDrawer( 32 | backgroundColor: Color? = null, 33 | borderColor: Color? = null, 34 | zIndex: Int 35 | ): BackgroundDrawer = BackgroundDrawer(backgroundColor, borderColor, lineHeight, zIndex) 36 | 37 | fun getLineSegments(startTextOffset: Int, endTextOffset: Int): List { 38 | val startLineIndex = textState.getLineForOffset(startTextOffset) 39 | val endLineIndex = textState.getLineForOffset(endTextOffset) 40 | val startCharacter = startTextOffset - textState.getLineStart(startLineIndex) 41 | val endCharacter = endTextOffset - textState.getLineStart(endLineIndex) 42 | return getLineSegments(startCharacter, endCharacter, startLineIndex + 1, endLineIndex + 1) 43 | } 44 | 45 | fun getLineSegments(startCharacter: Int, endCharacter: Int, startLine: Int, endLine: Int): List { 46 | return if (startLine == endLine) { 47 | listOf(lineSegment(startCharacter, endCharacter, startLine)) 48 | } else { 49 | val list = mutableListOf() 50 | list.add(lineSegment(startCharacter, startLine)) 51 | var line = startLine + 1 52 | while (line < endLine) { 53 | list.add(lineSegment(line = line)) 54 | line++ 55 | } 56 | list.add(lineSegment(endCharacter = endCharacter, line = endLine)) 57 | list 58 | } 59 | } 60 | 61 | private fun lineSegment(startCharacter: Int = 0, endCharacter: Int, line: Int): LineSegment = 62 | LineSegment( 63 | startCharacter * textState.charWidth, 64 | endCharacter * textState.charWidth, 65 | textState.getLineBottom(line - 1) 66 | ) 67 | 68 | private fun lineSegment(startCharacter: Int = 0, line: Int): LineSegment = 69 | LineSegment( 70 | startCharacter * textState.charWidth, 71 | textState.getLineRight(line - 1), 72 | textState.getLineBottom(line - 1) 73 | ) 74 | } 75 | 76 | internal fun Modifier.drawHighlights( 77 | drawState: DrawState, 78 | scrollState: ScrollState 79 | ) = drawWithCache { 80 | val scrollOffset = scrollState.value 81 | 82 | onDrawBehind { 83 | drawState.drawers.entries.forEach { (drawer, listState) -> 84 | drawer.draw( 85 | lineSegments = 86 | if (scrollOffset == 0) listState.value 87 | else listState.value.map { it.copy(y = it.y - scrollOffset) }, 88 | drawScope = this 89 | ) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/editor/draw/HighlightDrawer.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.editor.draw 2 | 3 | import androidx.compose.ui.graphics.drawscope.DrawScope 4 | 5 | internal abstract class HighlightDrawer( 6 | private val zIndex: Int 7 | ) : Comparable { 8 | 9 | abstract fun draw(lineSegments: List, drawScope: DrawScope) 10 | 11 | override fun compareTo(other: HighlightDrawer): Int { 12 | val comp = zIndex.compareTo(other.zIndex) 13 | return if (comp != 0) comp 14 | else this.hashCode() - other.hashCode() 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/editor/draw/LineSegment.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.editor.draw 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.runtime.Stable 5 | 6 | @Immutable 7 | internal class LineSegment { 8 | val xl: Float 9 | val xr: Float 10 | val y: Float 11 | 12 | @Suppress("ConvertSecondaryConstructorToPrimary") 13 | constructor(xl: Float, xr: Float, y: Float) { 14 | this.xl = if (xl > xr) xr else xl 15 | this.xr = xr 16 | this.y = y 17 | } 18 | 19 | @Stable 20 | operator fun component1(): Float = xl 21 | 22 | @Stable 23 | operator fun component2(): Float = xr 24 | 25 | @Stable 26 | operator fun component3(): Float = y 27 | 28 | @Stable 29 | fun copy(xl: Float = this.xl, xr: Float = this.xr, y: Float = this.y) = LineSegment(xl, xr, y) 30 | } 31 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/editor/draw/WavedLineDrawer.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.editor.draw 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.FilterQuality 6 | import androidx.compose.ui.graphics.Paint 7 | import androidx.compose.ui.graphics.PaintingStyle 8 | import androidx.compose.ui.graphics.Path 9 | import androidx.compose.ui.graphics.PathEffect 10 | import androidx.compose.ui.graphics.StampedPathEffectStyle 11 | import androidx.compose.ui.graphics.drawscope.DrawScope 12 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 13 | 14 | internal class WavedLineDrawer( 15 | private val lineColor: Color, 16 | zIndex: Int 17 | ) : HighlightDrawer(zIndex) { 18 | 19 | private val paint = Paint().apply { 20 | color = lineColor 21 | isAntiAlias = false 22 | filterQuality = FilterQuality.None 23 | style = PaintingStyle.Stroke 24 | strokeWidth = 1f 25 | pathEffect = wavedPathEffect 26 | } 27 | 28 | override fun draw(lineSegments: List, drawScope: DrawScope) { 29 | drawScope.drawIntoCanvas { canvas -> 30 | lineSegments.forEach { (xl, xr, y) -> 31 | canvas.save() 32 | val ny = y + lineBottomPadding 33 | canvas.clipRect(xl, ny, xr, ny + wavedLineHeight) 34 | canvas.drawLine( 35 | Offset(xl - lineHorizontalPadding, ny), 36 | Offset(xr + lineHorizontalPadding, ny), 37 | paint 38 | ) 39 | canvas.restore() 40 | } 41 | } 42 | } 43 | 44 | override fun equals(other: Any?): Boolean { 45 | if (this === other) return true 46 | if (javaClass != other?.javaClass) return false 47 | 48 | other as WavedLineDrawer 49 | 50 | if (lineColor != other.lineColor) return false 51 | 52 | return true 53 | } 54 | 55 | override fun hashCode(): Int { 56 | return lineColor.hashCode() 57 | } 58 | 59 | companion object { 60 | private const val lineHorizontalPadding = 3f 61 | private const val lineBottomPadding = -1f 62 | private const val wavedLineHeight = 3f 63 | 64 | private val wavedPath = Path().apply { 65 | lineTo(wavedLineHeight, wavedLineHeight) 66 | lineTo(wavedLineHeight * 2, 0f) 67 | lineTo(wavedLineHeight * 2 - 1, 0f) 68 | lineTo(wavedLineHeight, wavedLineHeight - 1) 69 | lineTo(1f, 0f) 70 | close() 71 | } 72 | 73 | private val wavedPathEffect = PathEffect.stampedPathEffect( 74 | shape = wavedPath, 75 | advance = wavedLineHeight * 2 - 2, 76 | phase = 0f, 77 | style = StampedPathEffectStyle.Translate 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/editor/text/EditorTextField.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.editor.text 2 | 3 | import io.github.n34t0.compose.codeEditor.AppTheme 4 | import androidx.compose.foundation.ScrollState 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.text.selection.LocalTextSelectionColors 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.SolidColor 15 | import io.github.n34t0.compose.fork.text.CoreTextField 16 | 17 | @Composable 18 | internal fun EditorTextField( 19 | textState: TextState, 20 | scrollState: ScrollState, 21 | onScroll: (Float) -> Unit = {}, 22 | onLineNumbersWidthChange: (Int) -> Unit = {}, 23 | modifier: Modifier = Modifier 24 | ) { 25 | val textFieldState = remember { EditorTextFieldState(textState) } 26 | 27 | Row { 28 | LineNumbers( 29 | textState = textState, 30 | textFieldState = textFieldState, 31 | scrollState = scrollState, 32 | onWidthChange = onLineNumbersWidthChange 33 | ) 34 | 35 | TextField( 36 | textFieldState = textFieldState, 37 | onScroll = onScroll, 38 | modifier = modifier 39 | ) 40 | } 41 | } 42 | 43 | @Composable 44 | private fun TextField( 45 | textFieldState: EditorTextFieldState, 46 | onScroll: (Float) -> Unit = {}, 47 | modifier: Modifier = Modifier 48 | ) { 49 | CompositionLocalProvider(LocalTextSelectionColors provides AppTheme.colors.selectionColors) { 50 | CoreTextField( 51 | value = textFieldState.textFieldValue, 52 | onValueChange = textFieldState::onTextFieldValueChange, 53 | onTextLayout = textFieldState::onTextLayoutChange, 54 | onScroll = onScroll, 55 | modifier = Modifier 56 | .fillMaxSize() 57 | .padding(Paddings.textFieldPadding) 58 | .then(modifier), 59 | textStyle = MaterialTheme.typography.body1, 60 | cursorBrush = SolidColor(MaterialTheme.colors.onBackground) 61 | ) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/editor/text/Paddings.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.editor.text 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.ui.unit.dp 5 | 6 | internal object Paddings { 7 | val textFieldLeftPadding = 5.dp 8 | val lineNumbersLeftPadding = 8.dp 9 | val lineNumbersRightPadding = 8.dp 10 | val verticalPadding = 2.dp 11 | 12 | val lineNumbersHorizontalPaddingSum = lineNumbersLeftPadding + lineNumbersRightPadding 13 | 14 | val lineNumbersPadding = PaddingValues.Absolute( 15 | left = lineNumbersLeftPadding, 16 | top = verticalPadding, 17 | right = lineNumbersRightPadding, 18 | bottom = verticalPadding 19 | ) 20 | 21 | val textFieldPadding = PaddingValues.Absolute( 22 | left = textFieldLeftPadding, 23 | top = verticalPadding, 24 | bottom = verticalPadding 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/editor/tooltip/EditorTooltip.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.editor.tooltip 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.ExperimentalComposeUiApi 5 | import androidx.compose.ui.unit.IntOffset 6 | import androidx.compose.ui.unit.IntRect 7 | import androidx.compose.ui.unit.IntSize 8 | import androidx.compose.ui.unit.LayoutDirection 9 | import androidx.compose.ui.window.PopupPositionProvider 10 | 11 | @OptIn(ExperimentalComposeUiApi::class) 12 | @Composable 13 | internal fun EditorTooltip( 14 | tooltipState: EditorTooltipState 15 | ) { 16 | if (tooltipState.isVisible) { 17 | tooltipState.caretRect?.let { 18 | val positionProvider = EditorTooltipPositionProvider(it, 2) 19 | 20 | Tooltip( 21 | message = tooltipState.message, 22 | positionProvider = positionProvider, 23 | onDismissRequest = tooltipState::hide 24 | ) 25 | } 26 | } 27 | } 28 | 29 | private class EditorTooltipPositionProvider( 30 | val caret: IntRect, 31 | val padding: Int 32 | ) : PopupPositionProvider { 33 | override fun calculatePosition( 34 | anchorBounds: IntRect, 35 | windowSize: IntSize, 36 | layoutDirection: LayoutDirection, 37 | popupContentSize: IntSize 38 | ): IntOffset { 39 | val w = popupContentSize.width 40 | var x = caret.center.x - w / 2 41 | if (x + w > anchorBounds.right) { 42 | x = anchorBounds.right - w 43 | } 44 | if (x < anchorBounds.left) { 45 | x = anchorBounds.left 46 | } 47 | 48 | val h = popupContentSize.height 49 | var y = caret.bottom + padding 50 | if (y + h > anchorBounds.bottom) { 51 | y = caret.top - h - padding 52 | } 53 | 54 | return IntOffset(x, y) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/editor/tooltip/EditorTooltipState.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.editor.tooltip 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.runtime.derivedStateOf 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.setValue 8 | import androidx.compose.ui.geometry.Rect 9 | import androidx.compose.ui.unit.IntRect 10 | import androidx.compose.ui.unit.round 11 | import io.github.n34t0.compose.codeEditor.editor.OffsetState 12 | import io.github.n34t0.compose.codeEditor.editor.text.TextState 13 | 14 | @Stable 15 | internal class EditorTooltipState( 16 | private val textState: TextState, 17 | private val layoutOffset: OffsetState 18 | ) { 19 | var isVisible by mutableStateOf(false) 20 | private set 21 | 22 | var message = "" 23 | private set 24 | 25 | val caretRect by derivedStateOf { 26 | if (textState.isSelected()) null 27 | else textState.getCaretRect().toIntRect().translate(layoutOffset.value) 28 | } 29 | 30 | fun show(message: String) { 31 | if (isVisible) hide() 32 | this.message = message 33 | isVisible = true 34 | } 35 | 36 | fun hide() { 37 | message = "" 38 | isVisible = false 39 | } 40 | } 41 | 42 | private fun Rect.toIntRect() = IntRect(topLeft.round(), bottomRight.round()) 43 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/keyevent/KeyEventHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.keyevent 2 | 3 | import androidx.compose.ui.input.key.KeyEvent 4 | 5 | internal fun interface KeyEventHandler { 6 | fun onKeyEvent(event: KeyEvent): Boolean 7 | } 8 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/search/SearchKeyEvents.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.search 2 | 3 | import androidx.compose.ui.ExperimentalComposeUiApi 4 | import androidx.compose.ui.input.key.Key 5 | import io.github.n34t0.compose.codeEditor.keyevent.KeyEventHandlerImpl 6 | import io.github.n34t0.compose.codeEditor.keyevent.KeyModifier.CTRL 7 | import io.github.n34t0.compose.codeEditor.keyevent.KeyModifier.SHIFT 8 | 9 | @OptIn(ExperimentalComposeUiApi::class) 10 | internal fun KeyEventHandlerImpl.onEscape(action: () -> Unit): KeyEventHandlerImpl { 11 | addKeyDownAction(Key.Escape) { 12 | action() 13 | true 14 | } 15 | return this 16 | } 17 | 18 | @OptIn(ExperimentalComposeUiApi::class) 19 | internal fun KeyEventHandlerImpl.onCtrlEnter(action: () -> Unit): KeyEventHandlerImpl { 20 | addKeyDownAction(Key.Enter, CTRL) { 21 | action() 22 | true 23 | } 24 | return this 25 | } 26 | 27 | @OptIn(ExperimentalComposeUiApi::class) 28 | internal fun KeyEventHandlerImpl.onF3(action: () -> Unit): KeyEventHandlerImpl { 29 | addKeyDownAction(Key.F3) { 30 | action() 31 | true 32 | } 33 | return this 34 | } 35 | 36 | @OptIn(ExperimentalComposeUiApi::class) 37 | internal fun KeyEventHandlerImpl.onShiftF3(action: () -> Unit): KeyEventHandlerImpl { 38 | addKeyDownAction(Key.F3, SHIFT) { 39 | action() 40 | true 41 | } 42 | return this 43 | } 44 | 45 | @OptIn(ExperimentalComposeUiApi::class) 46 | internal fun KeyEventHandlerImpl.onEnter(action: () -> Unit): KeyEventHandlerImpl { 47 | addKeyDownAction(Key.Enter) { 48 | action() 49 | true 50 | } 51 | addKeyTypeAction('\n') 52 | return this 53 | } 54 | 55 | @OptIn(ExperimentalComposeUiApi::class) 56 | internal fun KeyEventHandlerImpl.onShiftEnter(action: () -> Unit): KeyEventHandlerImpl { 57 | addKeyDownAction(Key.Enter, SHIFT) { 58 | action() 59 | true 60 | } 61 | addKeyTypeAction('\n', SHIFT) 62 | return this 63 | } 64 | 65 | @OptIn(ExperimentalComposeUiApi::class) 66 | internal fun KeyEventHandlerImpl.onUp(action: () -> Unit): KeyEventHandlerImpl { 67 | addKeyDownAction(Key.DirectionUp) { 68 | action() 69 | true 70 | } 71 | return this 72 | } 73 | 74 | @OptIn(ExperimentalComposeUiApi::class) 75 | internal fun KeyEventHandlerImpl.onDown(action: () -> Unit): KeyEventHandlerImpl { 76 | addKeyDownAction(Key.DirectionDown) { 77 | action() 78 | true 79 | } 80 | return this 81 | } 82 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/statusbar/BusyState.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.statusbar 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import kotlinx.coroutines.sync.Mutex 8 | import kotlinx.coroutines.sync.withLock 9 | 10 | @Stable 11 | internal class BusyState { 12 | private var busyCount = 0 13 | private var lock = Mutex() 14 | 15 | var isBusy by mutableStateOf(false) 16 | 17 | suspend fun busy() { 18 | lock.withLock { 19 | busyCount++ 20 | isBusy = true 21 | } 22 | } 23 | 24 | suspend fun free() { 25 | lock.withLock { 26 | if (--busyCount < 0) busyCount = 0 27 | isBusy = busyCount > 0 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/statusbar/StatusBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.statusbar 2 | 3 | import io.github.n34t0.compose.codeEditor.AppTheme 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.requiredSize 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.material.CircularProgressIndicator 14 | import androidx.compose.material.MaterialTheme 15 | import androidx.compose.material.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.drawBehind 20 | import androidx.compose.ui.geometry.Offset 21 | import androidx.compose.ui.graphics.SolidColor 22 | import androidx.compose.ui.graphics.StrokeCap 23 | import androidx.compose.ui.graphics.drawscope.clipRect 24 | import androidx.compose.ui.text.style.TextOverflow 25 | import androidx.compose.ui.unit.dp 26 | 27 | @Composable 28 | internal fun StatusBar( 29 | message: String, 30 | busyState: BusyState 31 | ) = Box( 32 | modifier = Modifier 33 | .height(28.dp) 34 | .fillMaxWidth() 35 | .drawBehind { // top border 36 | clipRect { 37 | drawLine( 38 | brush = SolidColor(AppTheme.colors.borderLight), 39 | cap = StrokeCap.Square, 40 | start = Offset.Zero, 41 | end = Offset(x = size.width, y = 0f) 42 | ) 43 | } 44 | } 45 | .padding(vertical = 4.dp) 46 | ) { 47 | Row( 48 | modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize(), 49 | verticalAlignment = Alignment.CenterVertically 50 | ) { 51 | Text( 52 | text = message, 53 | modifier = Modifier.weight(1f), 54 | style = MaterialTheme.typography.caption, 55 | maxLines = 1, 56 | overflow = TextOverflow.Ellipsis 57 | ) 58 | 59 | if (busyState.isBusy) { 60 | Spacer(Modifier.width(8.dp)) 61 | CircularProgressIndicator( 62 | modifier = Modifier.requiredSize(14.dp), 63 | color = AppTheme.colors.indicatorColor, 64 | strokeWidth = 2.dp 65 | ) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/codeEditor/tooltip/Tooltip.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.editor.tooltip 2 | 3 | import io.github.n34t0.compose.codeEditor.AppTheme 4 | import androidx.compose.foundation.BorderStroke 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.material.Surface 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.Dp 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.window.Popup 16 | import androidx.compose.ui.window.PopupPositionProvider 17 | 18 | @Composable 19 | internal fun Tooltip( 20 | message: String, 21 | positionProvider: PopupPositionProvider, 22 | onDismissRequest: (() -> Unit)? = null 23 | ) = Popup( 24 | popupPositionProvider = positionProvider, 25 | onDismissRequest = onDismissRequest 26 | ) { 27 | Surface( 28 | elevation = 2.dp, 29 | border = BorderStroke(Dp.Hairline, AppTheme.colors.borderLight) 30 | ) { 31 | Box( 32 | Modifier.padding(5.dp) 33 | ) { 34 | Text( 35 | text = message, 36 | modifier = Modifier.align(Alignment.CenterStart), 37 | style = MaterialTheme.typography.caption 38 | ) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/ActualJvm.kt: -------------------------------------------------------------------------------- 1 | // ktlint-disable filename 2 | 3 | /* 4 | * Copyright 2021 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package io.github.n34t0.compose.fork 20 | 21 | internal typealias AtomicReference = java.util.concurrent.atomic.AtomicReference 22 | 23 | internal typealias AtomicLong = java.util.concurrent.atomic.AtomicLong 24 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/DesktopPlatform.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork 18 | 19 | internal enum class DesktopPlatform { 20 | Linux, 21 | Windows, 22 | MacOS, 23 | Unknown; 24 | 25 | companion object { 26 | /** 27 | * Identify OS on which the application is currently running. 28 | */ 29 | val Current: DesktopPlatform by lazy { 30 | val name = System.getProperty("os.name") 31 | when { 32 | name?.startsWith("Linux") == true -> Linux 33 | name?.startsWith("Win") == true -> Windows 34 | name == "Mac OS X" -> MacOS 35 | else -> Unknown 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/TempListUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork 18 | 19 | import kotlin.contracts.ExperimentalContracts 20 | import kotlin.contracts.contract 21 | 22 | // TODO: remove these when we can add new APIs to ui-util outside of beta cycle 23 | 24 | /** 25 | * Returns a list containing only elements matching the given [predicate]. 26 | */ 27 | @OptIn(ExperimentalContracts::class) 28 | internal inline fun List.fastFilter(predicate: (T) -> Boolean): List { 29 | contract { callsInPlace(predicate) } 30 | val target = ArrayList(size) 31 | fastForEach { 32 | if (predicate(it)) target += (it) 33 | } 34 | return target 35 | } 36 | 37 | /** 38 | * Accumulates value starting with [initial] value and applying [operation] from left to right 39 | * to current accumulator value and each element. 40 | * 41 | * Returns the specified [initial] value if the collection is empty. 42 | * 43 | * @param [operation] function that takes current accumulator value and an element, and calculates the next accumulator value. 44 | */ 45 | @OptIn(ExperimentalContracts::class) 46 | internal inline fun List.fastFold(initial: R, operation: (acc: R, T) -> R): R { 47 | contract { callsInPlace(operation) } 48 | var accumulator = initial 49 | fastForEach { e -> 50 | accumulator = operation(accumulator, e) 51 | } 52 | return accumulator 53 | } 54 | 55 | /** 56 | * Returns a list containing the results of applying the given [transform] function 57 | * to each element in the original collection. 58 | */ 59 | @OptIn(ExperimentalContracts::class) 60 | internal inline fun List.fastMapIndexedNotNull( 61 | transform: (index: Int, T) -> R? 62 | ): List { 63 | contract { callsInPlace(transform) } 64 | val target = ArrayList(size) 65 | fastForEachIndexed { index, e -> 66 | transform(index, e)?.let { target += it } 67 | } 68 | return target 69 | } 70 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/DesktopCursorHandle.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.geometry.Offset 22 | 23 | @Composable 24 | @Suppress("UNUSED_PARAMETER") 25 | internal fun CursorHandle( 26 | handlePosition: Offset, 27 | modifier: Modifier, 28 | content: @Composable (() -> Unit)? 29 | ) { 30 | /* Not implemented. */ 31 | } 32 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/KeyCommand.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | internal enum class KeyCommand( 20 | // Indicates, that this command is supposed to edit text so should be applied only to 21 | // editable text fields 22 | val editsText: Boolean 23 | ) { 24 | LEFT_CHAR(false), 25 | RIGHT_CHAR(false), 26 | 27 | RIGHT_WORD(false), 28 | LEFT_WORD(false), 29 | 30 | NEXT_PARAGRAPH(false), 31 | PREV_PARAGRAPH(false), 32 | 33 | LINE_START(false), 34 | LINE_END(false), 35 | LINE_LEFT(false), 36 | LINE_RIGHT(false), 37 | 38 | UP(false), 39 | DOWN(false), 40 | 41 | PAGE_UP(false), 42 | PAGE_DOWN(false), 43 | 44 | HOME(false), 45 | END(false), 46 | 47 | COPY(false), 48 | PASTE(true), 49 | CUT(true), 50 | 51 | DELETE_PREV_CHAR(true), 52 | DELETE_NEXT_CHAR(true), 53 | 54 | DELETE_PREV_WORD(true), 55 | DELETE_NEXT_WORD(true), 56 | 57 | DELETE_FROM_LINE_START(true), 58 | DELETE_TO_LINE_END(true), 59 | 60 | SELECT_ALL(false), 61 | 62 | SELECT_LEFT_CHAR(false), 63 | SELECT_RIGHT_CHAR(false), 64 | 65 | SELECT_UP(false), 66 | SELECT_DOWN(false), 67 | 68 | SELECT_PAGE_UP(false), 69 | SELECT_PAGE_DOWN(false), 70 | 71 | SELECT_HOME(false), 72 | SELECT_END(false), 73 | 74 | SELECT_LEFT_WORD(false), 75 | SELECT_RIGHT_WORD(false), 76 | SELECT_NEXT_PARAGRAPH(false), 77 | SELECT_PREV_PARAGRAPH(false), 78 | 79 | SELECT_LINE_START(false), 80 | SELECT_LINE_END(false), 81 | SELECT_LINE_LEFT(false), 82 | SELECT_LINE_RIGHT(false), 83 | 84 | DESELECT(false), 85 | 86 | NEW_LINE(true), 87 | TAB(true), 88 | 89 | UNDO(true), 90 | REDO(true), 91 | CHARACTER_PALETTE(true) 92 | } 93 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/KeyEventHelpers.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | import androidx.compose.ui.input.key.KeyEvent 20 | import org.jetbrains.skiko.orderEmojiAndSymbolsPopup 21 | 22 | internal fun KeyEvent.cancelsTextSelection(): Boolean = false 23 | 24 | internal fun showCharacterPalette() = orderEmojiAndSymbolsPopup() 25 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/KeyboardActionRunner.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | import androidx.compose.foundation.text.KeyboardActionScope 20 | import androidx.compose.foundation.text.KeyboardActions 21 | import androidx.compose.ui.focus.FocusDirection 22 | import androidx.compose.ui.focus.FocusManager 23 | import androidx.compose.ui.text.input.ImeAction 24 | import androidx.compose.ui.text.input.ImeAction.Companion.Default 25 | import androidx.compose.ui.text.input.ImeAction.Companion.None 26 | import androidx.compose.ui.text.input.ImeAction.Companion.Go 27 | import androidx.compose.ui.text.input.ImeAction.Companion.Search 28 | import androidx.compose.ui.text.input.ImeAction.Companion.Send 29 | import androidx.compose.ui.text.input.ImeAction.Companion.Previous 30 | import androidx.compose.ui.text.input.ImeAction.Companion.Next 31 | import androidx.compose.ui.text.input.ImeAction.Companion.Done 32 | 33 | /** 34 | * This class can be used to run keyboard actions when the user triggers an IME action. 35 | */ 36 | internal class KeyboardActionRunner : KeyboardActionScope { 37 | 38 | /** 39 | * The developer specified [KeyboardActions]. 40 | */ 41 | lateinit var keyboardActions: KeyboardActions 42 | 43 | /** 44 | * A reference to the [FocusManager] composition local. 45 | */ 46 | lateinit var focusManager: FocusManager 47 | 48 | /** 49 | * Run the keyboard action corresponding to the specified imeAction. If a keyboard action is 50 | * not specified, use the default implementation provided by [defaultKeyboardAction]. 51 | */ 52 | fun runAction(imeAction: ImeAction) { 53 | val keyboardAction = when (imeAction) { 54 | Done -> keyboardActions.onDone 55 | Go -> keyboardActions.onGo 56 | Next -> keyboardActions.onNext 57 | Previous -> keyboardActions.onPrevious 58 | Search -> keyboardActions.onSearch 59 | Send -> keyboardActions.onSend 60 | Default, None -> null 61 | else -> error("invalid ImeAction") 62 | } 63 | keyboardAction?.invoke(this) ?: defaultKeyboardAction(imeAction) 64 | } 65 | 66 | /** 67 | * Default implementations for [KeyboardActions]. 68 | */ 69 | override fun defaultKeyboardAction(imeAction: ImeAction) { 70 | when (imeAction) { 71 | Next -> focusManager.moveFocus(FocusDirection.Next) 72 | Previous -> focusManager.moveFocus(FocusDirection.Previous) 73 | // Note: Don't replace this with an else. These are specified explicitly so that we 74 | // don't forget to update this when statement when new imeActions are added. 75 | Done, Go, Search, Send, Default, None -> Unit // Do Nothing. 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/LongPressTextDragObserver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | import androidx.compose.foundation.gestures.detectDragGestures 20 | import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress 21 | import androidx.compose.ui.geometry.Offset 22 | import androidx.compose.ui.input.pointer.PointerInputScope 23 | 24 | internal interface TextDragObserver { 25 | fun onStart(startPoint: Offset) 26 | 27 | fun onDrag(delta: Offset) 28 | 29 | fun onStop() 30 | 31 | fun onCancel() 32 | } 33 | 34 | internal suspend fun PointerInputScope.detectDragGesturesAfterLongPressWithObserver( 35 | observer: TextDragObserver 36 | ) = detectDragGesturesAfterLongPress( 37 | onDragEnd = { observer.onStop() }, 38 | onDrag = { _, offset -> 39 | observer.onDrag(offset) 40 | }, 41 | onDragStart = { 42 | observer.onStart(it) 43 | }, 44 | onDragCancel = { observer.onCancel() } 45 | ) 46 | 47 | internal suspend fun PointerInputScope.detectDragGesturesWithObserver( 48 | observer: TextDragObserver 49 | ) = detectDragGestures( 50 | onDragEnd = { observer.onStop() }, 51 | onDrag = { _, offset -> 52 | observer.onDrag(offset) 53 | }, 54 | onDragStart = { 55 | observer.onStart(it) 56 | }, 57 | onDragCancel = { observer.onCancel() } 58 | ) 59 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/MaxLinesHeightModifier.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | import androidx.compose.foundation.layout.heightIn 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.composed 23 | import androidx.compose.ui.platform.LocalDensity 24 | import androidx.compose.ui.platform.LocalFontLoader 25 | import androidx.compose.ui.platform.LocalLayoutDirection 26 | import androidx.compose.ui.platform.debugInspectorInfo 27 | import androidx.compose.ui.text.TextStyle 28 | import androidx.compose.ui.text.resolveDefaults 29 | 30 | /** 31 | * Constraint the height of the text field so that it vertically occupies no more than [maxLines] 32 | * number of lines. 33 | */ 34 | internal fun Modifier.maxLinesHeight( 35 | /*@IntRange(from = 1)*/ 36 | maxLines: Int, 37 | textStyle: TextStyle 38 | ) = composed( 39 | inspectorInfo = debugInspectorInfo { 40 | name = "maxLinesHeight" 41 | properties["maxLines"] = maxLines 42 | properties["textStyle"] = textStyle 43 | } 44 | ) { 45 | require(maxLines > 0) { 46 | "maxLines must be greater than 0" 47 | } 48 | if (maxLines == Int.MAX_VALUE) return@composed Modifier 49 | 50 | val density = LocalDensity.current 51 | val resourceLoader = LocalFontLoader.current 52 | val layoutDirection = LocalLayoutDirection.current 53 | 54 | // Difference between the height of two lines paragraph and one line paragraph gives us 55 | // an approximation of height of one line 56 | val firstLineHeight = remember(density, resourceLoader, textStyle, layoutDirection) { 57 | computeSizeForDefaultText( 58 | style = resolveDefaults(textStyle, layoutDirection), 59 | density = density, 60 | resourceLoader = resourceLoader, 61 | text = EmptyTextReplacement, 62 | maxLines = 1 63 | ).height 64 | } 65 | val firstTwoLinesHeight = remember(density, resourceLoader, textStyle, layoutDirection) { 66 | val twoLines = EmptyTextReplacement + "\n" + EmptyTextReplacement 67 | computeSizeForDefaultText( 68 | style = resolveDefaults(textStyle, layoutDirection), 69 | density = density, 70 | resourceLoader = resourceLoader, 71 | text = twoLines, 72 | maxLines = 2 73 | ).height 74 | } 75 | val lineHeight = firstTwoLinesHeight - firstLineHeight 76 | val precomputedMaxLinesHeight = firstLineHeight + lineHeight * (maxLines - 1) 77 | 78 | Modifier.heightIn( 79 | max = with(density) { precomputedMaxLinesHeight.toDp() } 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/PointerIconModifier.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:OptIn(ExperimentalComposeUiApi::class) 18 | 19 | package io.github.n34t0.compose.fork.text 20 | 21 | import androidx.compose.ui.ExperimentalComposeUiApi 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.input.pointer.PointerIcon 24 | import androidx.compose.ui.input.pointer.pointerIcon 25 | 26 | internal fun Modifier.pointerTextIcon() = 27 | this.pointerIcon(PointerIcon.Text) 28 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/StringHelpers.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | import org.jetbrains.skija.BreakIterator 20 | 21 | internal fun String.findPrecedingBreak(index: Int): Int { 22 | val it = BreakIterator.makeCharacterInstance() 23 | it.setText(this) 24 | return it.preceding(index) 25 | } 26 | 27 | internal fun String.findFollowingBreak(index: Int): Int { 28 | val it = BreakIterator.makeCharacterInstance() 29 | it.setText(this) 30 | return it.following(index) 31 | } 32 | 33 | internal fun StringBuilder.appendCodePointX(codePoint: Int): StringBuilder = 34 | this.appendCodePoint(codePoint) 35 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/StringHelpers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | import androidx.compose.ui.text.TextRange 20 | 21 | internal fun CharSequence.findParagraphStart(startIndex: Int): Int { 22 | for (index in startIndex - 1 downTo 1) { 23 | if (this[index - 1] == '\n') { 24 | return index 25 | } 26 | } 27 | return 0 28 | } 29 | 30 | internal fun CharSequence.findParagraphEnd(startIndex: Int): Int { 31 | for (index in startIndex + 1 until this.length) { 32 | if (this[index] == '\n') { 33 | return index 34 | } 35 | } 36 | return this.length 37 | } 38 | 39 | /** 40 | * Returns the text range of the paragraph at the given character offset. 41 | * 42 | * Paragraphs are separated by Line Feed character (\n). 43 | */ 44 | internal fun CharSequence.getParagraphBoundary(index: Int): TextRange { 45 | return TextRange(findParagraphStart(index), findParagraphEnd(index)) 46 | } 47 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/TextFieldCursor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | import androidx.compose.animation.core.Animatable 20 | import androidx.compose.animation.core.AnimationSpec 21 | import androidx.compose.animation.core.infiniteRepeatable 22 | import androidx.compose.animation.core.keyframes 23 | import androidx.compose.runtime.LaunchedEffect 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.composed 27 | import androidx.compose.ui.draw.drawWithContent 28 | import androidx.compose.ui.geometry.Offset 29 | import androidx.compose.ui.geometry.Rect 30 | import androidx.compose.ui.graphics.Brush 31 | import androidx.compose.ui.graphics.SolidColor 32 | import androidx.compose.ui.graphics.isUnspecified 33 | import androidx.compose.ui.text.input.OffsetMapping 34 | import androidx.compose.ui.text.input.TextFieldValue 35 | import androidx.compose.ui.unit.dp 36 | 37 | @Suppress("ModifierInspectorInfo") 38 | internal fun Modifier.cursor( 39 | state: TextFieldState, 40 | value: TextFieldValue, 41 | offsetMapping: OffsetMapping, 42 | cursorBrush: Brush, 43 | enabled: Boolean 44 | ) = if (enabled) composed { 45 | val cursorAlpha = remember { Animatable(1f) } 46 | val isBrushSpecified = !(cursorBrush is SolidColor && cursorBrush.value.isUnspecified) 47 | if (state.hasFocus && value.selection.collapsed && isBrushSpecified) { 48 | LaunchedEffect(cursorBrush, value.annotatedString, value.selection) { 49 | cursorAlpha.animateTo(0f, cursorAnimationSpec) 50 | } 51 | drawWithContent { 52 | this.drawContent() 53 | val cursorAlphaValue = cursorAlpha.value.coerceIn(0f, 1f) 54 | if (cursorAlphaValue != 0f) { 55 | val transformedOffset = offsetMapping 56 | .originalToTransformed(value.selection.start) 57 | val cursorRect = state.layoutResult?.value?.getCursorRect(transformedOffset) 58 | ?: Rect(0f, 0f, 0f, 0f) 59 | val cursorWidth = DefaultCursorThickness.toPx() 60 | val cursorX = (cursorRect.left + cursorWidth / 2) 61 | .coerceAtMost(size.width - cursorWidth / 2) 62 | 63 | drawLine( 64 | cursorBrush, 65 | Offset(cursorX, cursorRect.top), 66 | Offset(cursorX, cursorRect.bottom), 67 | alpha = cursorAlphaValue, 68 | strokeWidth = cursorWidth 69 | ) 70 | } 71 | } 72 | } else { 73 | Modifier 74 | } 75 | } else this 76 | 77 | private val cursorAnimationSpec: AnimationSpec 78 | get() = infiniteRepeatable( 79 | animation = keyframes { 80 | durationMillis = 1000 81 | 1f at 0 82 | 1f at 499 83 | 0f at 500 84 | 0f at 999 85 | } 86 | ) 87 | 88 | internal val DefaultCursorThickness = 2.dp 89 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/TextFieldGestureModifiers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | import androidx.compose.foundation.focusable 20 | import androidx.compose.foundation.interaction.MutableInteractionSource 21 | import io.github.n34t0.compose.fork.text.selection.MouseSelectionObserver 22 | import io.github.n34t0.compose.fork.text.selection.mouseSelectionDetector 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.focus.FocusRequester 25 | import androidx.compose.ui.focus.FocusState 26 | import androidx.compose.ui.focus.focusRequester 27 | import androidx.compose.ui.focus.onFocusChanged 28 | import androidx.compose.ui.input.pointer.pointerInput 29 | 30 | // Touch selection 31 | internal fun Modifier.longPressDragGestureFilter( 32 | observer: TextDragObserver, 33 | enabled: Boolean 34 | ) = if (enabled) { 35 | this.pointerInput(observer) { detectDragGesturesAfterLongPressWithObserver(observer) } 36 | } else { 37 | this 38 | } 39 | 40 | // Focus modifiers 41 | internal fun Modifier.textFieldFocusModifier( 42 | enabled: Boolean, 43 | focusRequester: FocusRequester, 44 | interactionSource: MutableInteractionSource?, 45 | onFocusChanged: (FocusState) -> Unit 46 | ) = this 47 | .focusRequester(focusRequester) 48 | .onFocusChanged(onFocusChanged) 49 | .focusable(interactionSource = interactionSource, enabled = enabled) 50 | 51 | // Mouse 52 | internal fun Modifier.mouseDragGestureDetector( 53 | observer: MouseSelectionObserver, 54 | enabled: Boolean 55 | ) = if (enabled) Modifier.pointerInput(observer) { 56 | mouseSelectionDetector(observer) 57 | } else this 58 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/TextFieldKeyInput.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | import androidx.compose.ui.input.key.KeyEvent 20 | 21 | private fun Char.isPrintable(): Boolean { 22 | val block = Character.UnicodeBlock.of(this) 23 | return (!Character.isISOControl(this)) && 24 | this != java.awt.event.KeyEvent.CHAR_UNDEFINED && 25 | block != null && 26 | block != Character.UnicodeBlock.SPECIALS 27 | } 28 | 29 | val KeyEvent.isTypedEvent: Boolean 30 | get() = nativeKeyEvent.id == java.awt.event.KeyEvent.KEY_TYPED && 31 | nativeKeyEvent.keyChar.isPrintable() 32 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/TextFieldSize.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | import androidx.compose.foundation.layout.defaultMinSize 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.composed 23 | import androidx.compose.ui.layout.layout 24 | import androidx.compose.ui.platform.LocalDensity 25 | import androidx.compose.ui.platform.LocalFontLoader 26 | import androidx.compose.ui.platform.LocalLayoutDirection 27 | import androidx.compose.ui.text.TextStyle 28 | import androidx.compose.ui.text.font.Font 29 | import androidx.compose.ui.text.resolveDefaults 30 | import androidx.compose.ui.unit.Density 31 | import androidx.compose.ui.unit.IntSize 32 | import androidx.compose.ui.unit.LayoutDirection 33 | 34 | @Suppress("ModifierInspectorInfo") 35 | internal fun Modifier.textFieldMinSize(style: TextStyle) = composed { 36 | val density = LocalDensity.current 37 | val resourceLoader = LocalFontLoader.current 38 | val layoutDirection = LocalLayoutDirection.current 39 | 40 | val minSizeState = remember { TextFieldSize(layoutDirection, density, resourceLoader, style) } 41 | minSizeState.update(layoutDirection, density, resourceLoader, style) 42 | 43 | Modifier.layout { measurable, constraints -> 44 | Modifier.defaultMinSize() 45 | val minSize = minSizeState.minSize 46 | 47 | val childConstraints = constraints.copy( 48 | minWidth = minSize.width.coerceIn(constraints.minWidth, constraints.maxWidth), 49 | minHeight = minSize.height.coerceIn(constraints.minHeight, constraints.maxHeight) 50 | ) 51 | val measured = measurable.measure(childConstraints) 52 | layout(measured.width, measured.height) { 53 | measured.placeRelative(0, 0) 54 | } 55 | } 56 | } 57 | 58 | private class TextFieldSize( 59 | var layoutDirection: LayoutDirection, 60 | var density: Density, 61 | var resourceLoader: Font.ResourceLoader, 62 | var style: TextStyle 63 | ) { 64 | var minSize = computeMinSize() 65 | private set 66 | 67 | fun update( 68 | layoutDirection: LayoutDirection, 69 | density: Density, 70 | resourceLoader: Font.ResourceLoader, 71 | style: TextStyle 72 | ) { 73 | if (layoutDirection != this.layoutDirection || 74 | density != this.density || 75 | resourceLoader != this.resourceLoader || 76 | style != this.style 77 | ) { 78 | this.layoutDirection = layoutDirection 79 | this.density = density 80 | this.resourceLoader = resourceLoader 81 | this.style = style 82 | minSize = computeMinSize() 83 | } 84 | } 85 | 86 | private fun computeMinSize(): IntSize { 87 | return computeSizeForDefaultText( 88 | style = resolveDefaults(style, layoutDirection), 89 | density = density, 90 | resourceLoader = resourceLoader 91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/TouchMode.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | internal val isInTouchMode = false 20 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/UndoManager.jvm.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text 18 | 19 | internal fun timeNowMillis(): Long = System.currentTimeMillis() 20 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/selection/DesktopSelectionHandles.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text.selection 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.geometry.Offset 22 | import androidx.compose.ui.text.style.ResolvedTextDirection 23 | 24 | @Composable 25 | internal fun SelectionHandle( 26 | position: Offset, 27 | isStartHandle: Boolean, 28 | direction: ResolvedTextDirection, 29 | handlesCrossed: Boolean, 30 | modifier: Modifier, 31 | content: (@Composable () -> Unit)? 32 | ) { 33 | // TODO 34 | } 35 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/selection/Selection.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text.selection 18 | 19 | import androidx.compose.runtime.Immutable 20 | import androidx.compose.ui.text.TextRange 21 | import androidx.compose.ui.text.style.ResolvedTextDirection 22 | 23 | /** 24 | * Information about the current Selection. 25 | */ 26 | @Immutable 27 | internal data class Selection( 28 | /** 29 | * Information about the start of the selection. 30 | */ 31 | val start: AnchorInfo, 32 | 33 | /** 34 | * Information about the end of the selection. 35 | */ 36 | val end: AnchorInfo, 37 | /** 38 | * The flag to show that the selection handles are dragged across each other. After selection 39 | * is initialized, if user drags one handle to cross the other handle, this is true, otherwise 40 | * it's false. 41 | */ 42 | // If selection happens in single widget, checking [TextRange.start] > [TextRange.end] is 43 | // enough. 44 | // But when selection happens across multiple widgets, this value needs more complicated 45 | // calculation. To avoid repeated calculation, making it as a flag is cheaper. 46 | val handlesCrossed: Boolean = false 47 | ) { 48 | /** 49 | * Contains information about an anchor (start/end) of selection. 50 | */ 51 | @Immutable 52 | internal data class AnchorInfo( 53 | /** 54 | * Text direction of the character in selection edge. 55 | */ 56 | val direction: ResolvedTextDirection, 57 | 58 | /** 59 | * Character offset for the selection edge. This offset is within individual child text 60 | * composable. 61 | */ 62 | val offset: Int, 63 | 64 | /** 65 | * The id of the [Selectable] which contains this [Selection] Anchor. 66 | */ 67 | val selectableId: Long 68 | ) 69 | 70 | fun merge(other: Selection?): Selection { 71 | if (other == null) return this 72 | 73 | var selection = this 74 | selection = if (handlesCrossed) { 75 | selection.copy(start = other.start) 76 | } else { 77 | selection.copy(end = other.end) 78 | } 79 | 80 | return selection 81 | } 82 | 83 | /** 84 | * Returns the selection offset information as a [TextRange] 85 | */ 86 | fun toTextRange(): TextRange { 87 | return TextRange(start.offset, end.offset) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/selection/SelectionHandles.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text.selection 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.geometry.Offset 22 | import androidx.compose.ui.text.style.ResolvedTextDirection 23 | import androidx.compose.ui.unit.dp 24 | 25 | internal val HandleWidth = 25.dp 26 | internal val HandleHeight = 25.dp 27 | 28 | /** 29 | * Adjust coordinates for given text offset. 30 | * 31 | * Currently [android.text.Layout.getLineBottom] returns y coordinates of the next 32 | * line's top offset, which is not included in current line's hit area. To be able to 33 | * hit current line, move up this y coordinates by 1 pixel. 34 | */ 35 | internal fun getAdjustedCoordinates(position: Offset): Offset { 36 | return Offset(position.x, position.y - 1f) 37 | } 38 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/selection/SelectionManager.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text.selection 18 | 19 | import io.github.n34t0.compose.fork.DesktopPlatform 20 | import io.github.n34t0.compose.fork.text.MappedKeys 21 | import androidx.compose.ui.input.key.KeyEvent 22 | import androidx.compose.ui.input.key.isCtrlPressed 23 | import androidx.compose.ui.input.key.isMetaPressed 24 | import androidx.compose.ui.input.key.key 25 | 26 | // this doesn't sounds very sustainable 27 | // it would end up being a function for any conceptual keyevent (selectall, cut, copy, paste) 28 | // TODO(b/1564937) 29 | internal fun isCopyKeyEvent(keyEvent: KeyEvent) = 30 | keyEvent.key == MappedKeys.C && when (DesktopPlatform.Current) { 31 | DesktopPlatform.MacOS -> keyEvent.isMetaPressed 32 | else -> keyEvent.isCtrlPressed 33 | } || keyEvent.key == MappedKeys.Copy 34 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/selection/SimpleLayout.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text.selection 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.layout.Layout 22 | import io.github.n34t0.compose.fork.fastFold 23 | import io.github.n34t0.compose.fork.fastForEach 24 | import io.github.n34t0.compose.fork.fastMap 25 | import kotlin.math.max 26 | 27 | /** 28 | * Selection is transparent in terms of measurement and layout and passes the same constraints to 29 | * the children. 30 | */ 31 | @Composable 32 | internal fun SimpleLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) { 33 | Layout(modifier = modifier, content = content) { measurables, constraints -> 34 | val placeables = measurables.fastMap { measurable -> 35 | measurable.measure(constraints) 36 | } 37 | 38 | val width = placeables.fastFold(0) { maxWidth, placeable -> 39 | max(maxWidth, (placeable.width)) 40 | } 41 | 42 | val height = placeables.fastFold(0) { minWidth, placeable -> 43 | max(minWidth, (placeable.height)) 44 | } 45 | 46 | layout(width, height) { 47 | placeables.fastForEach { placeable -> 48 | placeable.place(0, 0) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/selection/TextFieldSelectionDelegate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text.selection 18 | 19 | import androidx.compose.ui.text.TextLayoutResult 20 | import androidx.compose.ui.text.TextRange 21 | 22 | /** 23 | * Return selection information for TextField. 24 | * 25 | * @param textLayoutResult a result of the text layout. 26 | * @param rawStartOffset unprocessed start offset calculated directly from input position 27 | * @param rawEndOffset unprocessed end offset calculated directly from input position 28 | * @param previousSelection previous selection result 29 | * @param isStartHandle true if the start handle is being dragged 30 | * @param adjustment selection is adjusted according to this param 31 | * 32 | * @return selected text range. 33 | */ 34 | internal fun getTextFieldSelection( 35 | textLayoutResult: TextLayoutResult?, 36 | rawStartOffset: Int, 37 | rawEndOffset: Int, 38 | previousSelection: TextRange?, 39 | isStartHandle: Boolean, 40 | adjustment: SelectionAdjustment 41 | ): TextRange { 42 | textLayoutResult?.let { 43 | val textRange = TextRange(rawStartOffset, rawEndOffset) 44 | 45 | // When the previous selection is null, it's allowed to have collapsed selection. 46 | // So we can ignore the SelectionAdjustment.Character. 47 | if (previousSelection == null && adjustment == SelectionAdjustment.Character) { 48 | return textRange 49 | } 50 | 51 | return adjustment.adjust( 52 | textLayoutResult = textLayoutResult, 53 | newRawSelectionRange = textRange, 54 | previousHandleOffset = -1, 55 | isStartHandle = isStartHandle, 56 | previousSelectionRange = previousSelection 57 | ) 58 | } 59 | return TextRange(0, 0) 60 | } 61 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/selection/TextFieldSelectionManager.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text.selection 18 | 19 | import androidx.compose.ui.input.pointer.PointerEvent 20 | 21 | internal val PointerEvent.isShiftPressed: Boolean 22 | get() = mouseEvent?.isShiftDown ?: false 23 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/fork/text/selection/TextSelectionDelegate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.n34t0.compose.fork.text.selection 18 | 19 | import androidx.compose.ui.geometry.Offset 20 | import androidx.compose.ui.text.TextLayoutResult 21 | import kotlin.math.max 22 | 23 | /** 24 | * This method returns the graphical position where the selection handle should be based on the 25 | * offset and other information. 26 | * 27 | * @param textLayoutResult a result of the text layout. 28 | * @param offset character offset to be calculated 29 | * @param isStart true if called for selection start handle 30 | * @param areHandlesCrossed true if the selection handles are crossed 31 | * 32 | * @return the graphical position where the selection handle should be. 33 | */ 34 | internal fun getSelectionHandleCoordinates( 35 | textLayoutResult: TextLayoutResult, 36 | offset: Int, 37 | isStart: Boolean, 38 | areHandlesCrossed: Boolean 39 | ): Offset { 40 | val line = textLayoutResult.getLineForOffset(offset) 41 | val offsetToCheck = 42 | if (isStart && !areHandlesCrossed || !isStart && areHandlesCrossed) offset 43 | else max(offset - 1, 0) 44 | val bidiRunDirection = textLayoutResult.getBidiRunDirection(offsetToCheck) 45 | val paragraphDirection = textLayoutResult.getParagraphDirection(offset) 46 | 47 | val x = textLayoutResult.getHorizontalPosition( 48 | offset = offset, 49 | usePrimaryDirection = bidiRunDirection == paragraphDirection 50 | ) 51 | val y = textLayoutResult.getLineBottom(line) 52 | 53 | return Offset(x, y) 54 | } 55 | -------------------------------------------------------------------------------- /component/editor/src/main/kotlin/io/github/n34t0/compose/stubs/PlatformStub.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.stubs 2 | 3 | import io.github.n34t0.platform.CodeCompletionElement 4 | import io.github.n34t0.platform.GotoDeclarationData 5 | import io.github.n34t0.platform.Platform 6 | import io.github.n34t0.platform.Project 7 | import io.github.n34t0.platform.impl.GTDData 8 | 9 | internal val isStub: Boolean = java.lang.Boolean.getBoolean("platform.stub") 10 | 11 | internal class PlatformStub : Platform { 12 | 13 | override fun init() {} 14 | 15 | override fun stop() {} 16 | 17 | override fun openProject(rootFolder: String): Project = ProjectStub() 18 | 19 | override fun openFile(filePath: String): Project = ProjectStub() 20 | 21 | } 22 | 23 | internal class ProjectStub : Project { 24 | 25 | override fun addLibraries(p0: MutableList) {} 26 | 27 | override fun synchronizeProjectDirectory() {} 28 | 29 | var count = 0 30 | var list = ArrayList() 31 | 32 | override fun getCodeCompletion(p0: String, p1: Int): List { 33 | repeat(3) { 34 | count++ 35 | list.add(CcElementStub("Sorted$count")) 36 | } 37 | return list 38 | } 39 | 40 | override fun gotoDeclaration(path: String?, caretOffset: Int): GotoDeclarationData = 41 | GTDData.NON_NAVIGATABLE 42 | 43 | override fun closeProject() {} 44 | 45 | } 46 | 47 | internal class CcElementStub( 48 | private val name: String = "Name", 49 | private val type: String = "Type", 50 | private val tail: String = "Tail" 51 | ) : CodeCompletionElement { 52 | 53 | override fun getName(): String = name 54 | 55 | override fun getType(): String = type 56 | 57 | override fun getTail(): String = tail 58 | 59 | override fun toString(): String = "$type $name $tail" 60 | 61 | } 62 | -------------------------------------------------------------------------------- /component/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 2 | kotlin.code.style=official 3 | 4 | platform.version=0.1.0 5 | editor.version=0.1.0 6 | #release=true 7 | -------------------------------------------------------------------------------- /component/gradle/project.properties: -------------------------------------------------------------------------------- 1 | intellijSdk.version=211.7442.40 2 | intellijSdk.platform=ideaIC 3 | deps.rootFolder=platform-build-dependencies 4 | deps.organization=platform.build 5 | -------------------------------------------------------------------------------- /component/gradle/projectProperties.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. 3 | * Modified by Alex Hosh (n34to0@gmail.com) 2021. 4 | * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 5 | */ 6 | 7 | import java.util.* 8 | import java.io.* 9 | 10 | val scriptDirectory: File = File(buildscript.sourceURI!!.rawPath).parentFile 11 | val propertiesFile: File = File(scriptDirectory, "project.properties") 12 | 13 | FileReader(propertiesFile).use { 14 | val properties = Properties() 15 | properties.load(it) 16 | properties.forEach { (k, v) -> 17 | extra[k.toString()] = v 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /component/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n34t0/compose-code-editor/8bc6edadd2a2b1eb20e15e265d3d6ad638b27db4/component/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /component/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /component/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 | -------------------------------------------------------------------------------- /component/platform/api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | `maven-publish` 4 | signing 5 | } 6 | 7 | tasks.jar { 8 | manifest { 9 | attributes( 10 | mapOf( 11 | "Implementation-Title" to "platform-api", 12 | "Implementation-Version" to project.version 13 | ) 14 | ) 15 | } 16 | } 17 | 18 | publishing { 19 | publications { 20 | create("mavenPlatformApi") { 21 | artifactId = "platform-api" 22 | 23 | from(components["java"]) 24 | 25 | pom { 26 | name.set("Code Editor Platform API") 27 | description.set("A library that provides an API for using code completion " + 28 | "and go-to-definition functions using the IntelliJ platform") 29 | addCommonPom() 30 | } 31 | } 32 | } 33 | } 34 | 35 | signing { 36 | val signingKeyId: String? by project 37 | val signingKey: String? by project 38 | val signingPassword: String? by project 39 | useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) 40 | sign(publishing.publications["mavenPlatformApi"]) 41 | } -------------------------------------------------------------------------------- /component/platform/api/src/main/java/io/github/n34t0/platform/CodeCompletionElement.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform; 2 | 3 | /** 4 | * The content returned by the methods depends on the object type. 5 | */ 6 | public interface CodeCompletionElement { 7 | 8 | /** 9 | * @return the string which will be inserted into the editor when this element is chosen. 10 | */ 11 | String getName(); 12 | 13 | /** 14 | * @return the type of the variable or the type returned by the method. Could be null. 15 | */ 16 | String getType(); 17 | 18 | /** 19 | * @return a list of method parameters. Could be null. 20 | */ 21 | String getTail(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /component/platform/api/src/main/java/io/github/n34t0/platform/GotoDeclarationData.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform; 2 | 3 | import java.util.Collection; 4 | 5 | public interface GotoDeclarationData { 6 | 7 | /** 8 | * @return true, if the index is not ready and it is impossible to get "go to declaration" data 9 | */ 10 | boolean isIndexNotReady(); 11 | 12 | /** 13 | * @return true, if navigation to the target is possible 14 | */ 15 | boolean canNavigate(); 16 | 17 | /** 18 | * @return true, if the initial element offset was successfully set 19 | */ 20 | boolean isInitialElementOffsetSet(); 21 | 22 | /** 23 | * @return start offset of the initial element 24 | */ 25 | int getInitialElementStartOffset(); 26 | 27 | /** 28 | * @return end offset of the initial element 29 | */ 30 | int getInitialElementEndOffset(); 31 | 32 | /** 33 | * @return collection of items containing coordinates of declarations 34 | */ 35 | Collection getTargets(); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /component/platform/api/src/main/java/io/github/n34t0/platform/GotoDeclarationTarget.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform; 2 | 3 | public interface GotoDeclarationTarget { 4 | 5 | /** 6 | * @return path to the file containing the target element 7 | */ 8 | String getPath(); 9 | 10 | /** 11 | * @return offset in the file to the target element 12 | */ 13 | int getOffset(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /component/platform/api/src/main/java/io/github/n34t0/platform/Platform.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform; 2 | 3 | /** 4 | * Only one initialized instance of this interface can exist at runtime. 5 | * The platform cannot be re-initialized after stopping. 6 | */ 7 | public interface Platform { 8 | 9 | /** 10 | * Initializes the platform. Must be called first. 11 | */ 12 | void init(); 13 | 14 | /** 15 | * Stops the platform, closes projects, and all open resources. 16 | */ 17 | void stop(); 18 | 19 | /** 20 | * Creates a new project. 21 | * @param rootFolder the root folder of the project, which contains all the source files. 22 | */ 23 | Project openProject(String rootFolder); 24 | 25 | /** 26 | * Creates a temporary project for only one file. 27 | * Allows to use code completion for this file. 28 | * @param filePath absolute path to the file 29 | */ 30 | Project openFile(String filePath); 31 | } 32 | -------------------------------------------------------------------------------- /component/platform/api/src/main/java/io/github/n34t0/platform/Project.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | public interface Project { 7 | 8 | /** 9 | * Loads libraries for the project. 10 | * 11 | * @param paths list of paths to libraries. 12 | * Each item can refer to a jar library file or to a directory. 13 | * If a directory is specified, all libraries from it and all its subdirectories will be loaded. 14 | */ 15 | default void addLibraries(String... paths) { 16 | addLibraries(Arrays.asList(paths)); 17 | } 18 | 19 | void addLibraries(List paths); 20 | 21 | /** 22 | * Refreshes information about the project directory structure. 23 | * Must be called after any manipulation in the project directory. 24 | */ 25 | void synchronizeProjectDirectory(); 26 | 27 | /** 28 | * Calls the code completion for the file at the offset. 29 | * 30 | * @param path path to a source file. 31 | * @param caretOffset - caret position offset 32 | */ 33 | List getCodeCompletion(String path, int caretOffset); 34 | 35 | /** 36 | * Returns the coordinates of the element declaration. 37 | * 38 | * @param path path to a source file. 39 | * @param caretOffset caret position offset 40 | */ 41 | GotoDeclarationData gotoDeclaration(String path, int caretOffset); 42 | 43 | /** 44 | * Closes the project, clears resources. 45 | */ 46 | void closeProject(); 47 | } 48 | -------------------------------------------------------------------------------- /component/platform/build.gradle.kts: -------------------------------------------------------------------------------- 1 | subprojects { 2 | version = getVersion("platform") 3 | } -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/com/intellij/codeInsight/daemon/impl/IpwStatusBarUpdater.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2016 JetBrains s.r.o. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * Modified by Alex Hosh (n34to0@gmail.com) 2021. 17 | */ 18 | package com.intellij.codeInsight.daemon.impl; 19 | 20 | import com.intellij.openapi.Disposable; 21 | import com.intellij.openapi.project.Project; 22 | 23 | public class IpwStatusBarUpdater implements Disposable { 24 | public IpwStatusBarUpdater(Project project) { 25 | } 26 | 27 | private void updateLater() { 28 | } 29 | 30 | @Override 31 | public void dispose() { 32 | } 33 | 34 | private void updateStatus() { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/com/intellij/ide/ui/laf/IpwLafManagerImpl.java: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 2 | // Modified by Alex Hosh (n34to0@gmail.com) 2021. 3 | package com.intellij.ide.ui.laf; 4 | 5 | import com.intellij.ide.ui.LafManager; 6 | import com.intellij.ide.ui.LafManagerListener; 7 | import com.intellij.openapi.Disposable; 8 | import com.intellij.ui.CollectionComboBoxModel; 9 | import javax.swing.JComponent; 10 | import javax.swing.ListCellRenderer; 11 | import javax.swing.UIManager; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | 15 | final class IpwLafManagerImpl extends LafManager { 16 | 17 | @Override 18 | public UIManager.LookAndFeelInfo[] getInstalledLookAndFeels() { 19 | return new UIManager.LookAndFeelInfo[0]; 20 | } 21 | 22 | @Override 23 | public UIManager.LookAndFeelInfo getCurrentLookAndFeel() { 24 | return null; 25 | } 26 | 27 | @Override 28 | public LafReference getLookAndFeelReference() { 29 | return null; 30 | } 31 | 32 | @Override 33 | public ListCellRenderer getLookAndFeelCellRenderer() { 34 | return null; 35 | } 36 | 37 | @Override 38 | @NotNull 39 | public JComponent getSettingsToolbar() { 40 | return new JComponent() {}; 41 | } 42 | 43 | @Override 44 | public void setCurrentLookAndFeel(UIManager.LookAndFeelInfo lookAndFeelInfo, boolean lockEditorScheme) { } 45 | 46 | @Override 47 | public CollectionComboBoxModel getLafComboBoxModel() { 48 | return new CollectionComboBoxModel<>(); 49 | } 50 | 51 | @Override 52 | public UIManager.LookAndFeelInfo findLaf(LafReference reference) { 53 | return null; 54 | } 55 | 56 | @Override 57 | public void updateUI() { } 58 | 59 | @Override 60 | public void repaintUI() { } 61 | 62 | @Override 63 | public boolean getAutodetect() { 64 | return false; 65 | } 66 | 67 | @Override 68 | public void setAutodetect(boolean value) {} 69 | 70 | @Override 71 | public boolean getAutodetectSupported() { 72 | return false; 73 | } 74 | 75 | @Override 76 | public void setPreferredDarkLaf(UIManager.LookAndFeelInfo myPreferredDarkLaf) { } 77 | 78 | @Override 79 | public void setPreferredLightLaf(UIManager.LookAndFeelInfo myPreferredLightLaf) { } 80 | 81 | @Override 82 | public void addLafManagerListener(@NotNull LafManagerListener listener) { } 83 | 84 | @Override 85 | public void addLafManagerListener(@NotNull LafManagerListener listener, @NotNull Disposable disposable) { } 86 | 87 | @Override 88 | public void removeLafManagerListener(@NotNull LafManagerListener listener) { } 89 | } 90 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/com/intellij/openapi/progress/util/IpwPotemkinProgress.java: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 2 | // Modified by Alex Hosh (n34to0@gmail.com) 2021. 3 | package com.intellij.openapi.progress.util; 4 | 5 | import com.intellij.ide.IpwEventQueue; 6 | import com.intellij.openapi.application.Application; 7 | import com.intellij.openapi.application.ApplicationManager; 8 | import com.intellij.openapi.progress.ProcessCanceledException; 9 | import com.intellij.openapi.progress.ProgressManager; 10 | import com.intellij.openapi.project.Project; 11 | import com.intellij.openapi.util.NlsContexts; 12 | import com.intellij.util.concurrency.AppExecutorUtil; 13 | import com.intellij.util.concurrency.Semaphore; 14 | import javax.swing.JComponent; 15 | import org.jetbrains.annotations.Nls; 16 | import org.jetbrains.annotations.NotNull; 17 | import org.jetbrains.annotations.Nullable; 18 | 19 | import java.awt.event.InvocationEvent; 20 | 21 | public final class IpwPotemkinProgress extends IpwProgressWindow implements PingProgress { 22 | private final Application myApp = ApplicationManager.getApplication(); 23 | 24 | public IpwPotemkinProgress(@NotNull @NlsContexts.ProgressTitle String title, @Nullable Project project, @Nullable JComponent parentComponent, 25 | @Nullable @Nls(capitalization = Nls.Capitalization.Title) String cancelText) { 26 | super(cancelText != null,false, project, parentComponent, cancelText); 27 | setTitle(title); 28 | myApp.assertIsDispatchThread(); 29 | } 30 | 31 | @Override 32 | public void interact() { 33 | } 34 | 35 | /** Executes the action in EDT, paints itself inside checkCanceled calls. */ 36 | public void runInSwingThread(@NotNull Runnable action) { 37 | myApp.assertIsDispatchThread(); 38 | try { 39 | ProgressManager.getInstance().runProcess(action, this); 40 | } 41 | catch (ProcessCanceledException ignore) { 42 | } 43 | } 44 | 45 | /** Executes the action in a background thread, block Swing thread, handles selected input events and paints itself periodically. */ 46 | public void runInBackground(@NotNull Runnable action) { 47 | myApp.assertIsDispatchThread(); 48 | enterModality(); 49 | 50 | try { 51 | ensureBackgroundThreadStarted(action); 52 | 53 | while (isRunning()) { 54 | } 55 | } 56 | finally { 57 | exitModality(); 58 | } 59 | } 60 | 61 | private void ensureBackgroundThreadStarted(@NotNull Runnable action) { 62 | Semaphore started = new Semaphore(); 63 | started.down(); 64 | AppExecutorUtil.getAppExecutorService().execute(() -> { 65 | ProgressManager.getInstance().runProcess(() -> { 66 | started.up(); 67 | action.run(); 68 | }, this); 69 | }); 70 | 71 | started.waitFor(); 72 | } 73 | 74 | public static void invokeLaterNotBlocking(Object source, Runnable runnable) { 75 | IpwEventQueue.getInstance().postEvent(new InvocationEvent(source, runnable)); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/com/intellij/ui/IpwEditorNotificationsImpl.java: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 2 | // Modified by Alex Hosh (n34to0@gmail.com) 2021. 3 | package com.intellij.ui; 4 | 5 | import com.intellij.openapi.util.Key; 6 | import com.intellij.openapi.vfs.VirtualFile; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | public class IpwEditorNotificationsImpl extends EditorNotifications { 11 | 12 | @Override 13 | public void updateNotifications(@NotNull VirtualFile file) { 14 | 15 | } 16 | 17 | @Override 18 | public void updateNotifications(@NotNull Provider provider) { 19 | 20 | } 21 | 22 | @Override 23 | public void updateAllNotifications() { 24 | 25 | } 26 | 27 | @Override 28 | public void logNotificationActionInvocation(@Nullable Key providerKey, @Nullable Class runnableClass) { 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/CcElement.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl; 2 | 3 | import io.github.n34t0.platform.CodeCompletionElement; 4 | import com.intellij.codeInsight.lookup.LookupElement; 5 | import com.intellij.codeInsight.lookup.LookupElementPresentation; 6 | 7 | public class CcElement implements CodeCompletionElement { 8 | private final String name; 9 | private final String type; 10 | private final String tail; 11 | private String cacheString; 12 | 13 | CcElement(LookupElement element) { 14 | var presentation = LookupElementPresentation.renderElement(element); 15 | name = element.getLookupString(); 16 | type = presentation.getTypeText(); 17 | tail = presentation.getTailText(); 18 | } 19 | 20 | @Override 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | @Override 26 | public String getType() { 27 | return type; 28 | } 29 | 30 | @Override 31 | public String getTail() { 32 | return tail; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | if (cacheString == null) { 38 | var sb = new StringBuilder(); 39 | if (type != null) sb.append(type).append(' '); 40 | if (name != null) sb.append(name); 41 | if (tail != null) sb.append(tail); 42 | cacheString = sb.toString(); 43 | } 44 | return cacheString; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/GTDData.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl; 2 | 3 | import io.github.n34t0.platform.GotoDeclarationData; 4 | import io.github.n34t0.platform.GotoDeclarationTarget; 5 | import com.intellij.openapi.util.TextRange; 6 | 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | 10 | public class GTDData implements GotoDeclarationData { 11 | public static final GTDData NON_NAVIGATABLE = new GTDData(false); 12 | 13 | private final boolean indexNotReady; 14 | private final TextRange initialElementRange; 15 | private final Collection targets; 16 | 17 | private GTDData(boolean indexNotReady, Collection targets, TextRange initialElementRange) { 18 | this.indexNotReady = indexNotReady; 19 | this.targets = Collections.unmodifiableCollection(targets); 20 | this.initialElementRange = initialElementRange; 21 | } 22 | 23 | private GTDData(boolean indexNotReady, TextRange initialElementRange) { 24 | this(indexNotReady, Collections.emptyList(), initialElementRange); 25 | } 26 | 27 | private GTDData(boolean indexNotReady) { 28 | this(indexNotReady, TextRange.EMPTY_RANGE); 29 | } 30 | 31 | public GTDData(Collection targets, TextRange initialElementRange) { 32 | this(false, targets, initialElementRange); 33 | } 34 | 35 | public static GTDData createIndexNotReady(TextRange initialElementRange) { 36 | return new GTDData(true, initialElementRange); 37 | } 38 | 39 | public static GTDData createNonNavigatable(TextRange initialElementRange) { 40 | return new GTDData(false, initialElementRange); 41 | } 42 | 43 | @Override 44 | public boolean isIndexNotReady() { 45 | return indexNotReady; 46 | } 47 | 48 | @Override 49 | public boolean canNavigate() { 50 | return !indexNotReady && !targets.isEmpty(); 51 | } 52 | 53 | @Override 54 | public boolean isInitialElementOffsetSet() { 55 | return !initialElementRange.equalsToRange(0, 0); 56 | } 57 | 58 | @Override 59 | public int getInitialElementStartOffset() { 60 | return initialElementRange.getStartOffset(); 61 | } 62 | 63 | @Override 64 | public int getInitialElementEndOffset() { 65 | return initialElementRange.getEndOffset(); 66 | } 67 | 68 | @Override 69 | public Collection getTargets() { 70 | return targets; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/GTDTarget.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl; 2 | 3 | import io.github.n34t0.platform.GotoDeclarationTarget; 4 | 5 | public class GTDTarget implements GotoDeclarationTarget { 6 | private final String path; 7 | private final int offset; 8 | 9 | public GTDTarget(String path, int offset) { 10 | this.path = path; 11 | this.offset = offset; 12 | } 13 | 14 | @Override 15 | public String getPath() { 16 | return path; 17 | } 18 | 19 | @Override 20 | public int getOffset() { 21 | return offset; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return "GTDTarget{" + 27 | "path='" + path + '\'' + 28 | ", offset=" + offset + 29 | '}'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/IntellijPlatformWrapper.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl; 2 | 3 | import io.github.n34t0.platform.impl.edt.EdtUtil; 4 | import io.github.n34t0.platform.impl.services.impl.ApplicationServiceImpl; 5 | import io.github.n34t0.platform.impl.services.impl.FileServiceImpl; 6 | import io.github.n34t0.platform.Project; 7 | import io.github.n34t0.platform.Platform; 8 | import io.github.n34t0.platform.impl.services.ApplicationService; 9 | import io.github.n34t0.platform.impl.services.FileService; 10 | import com.intellij.openapi.Disposable; 11 | import com.intellij.openapi.util.Disposer; 12 | 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | 16 | public class IntellijPlatformWrapper implements Platform { 17 | private final Disposable rootDisposable; 18 | private FileService fileService; 19 | private ApplicationService applicationService; 20 | private static boolean isInitialized; 21 | private static boolean wasInitialized; 22 | 23 | public IntellijPlatformWrapper() { 24 | rootDisposable = Disposer.newDisposable("IpwRootDisposable"); 25 | } 26 | 27 | @Override 28 | public void init() { 29 | if (wasInitialized) return; 30 | setSystemSettings(); 31 | fileService = new FileServiceImpl(); 32 | fileService.init(rootDisposable); 33 | 34 | applicationService = new ApplicationServiceImpl(); 35 | applicationService.init(fileService); 36 | 37 | wasInitialized = true; 38 | isInitialized = true; 39 | } 40 | 41 | @Override 42 | public void stop() { 43 | if (!isInitialized) return; 44 | EdtUtil.runInEdtAndWait(() -> Disposer.dispose(rootDisposable)); 45 | Disposer.assertIsEmpty(); 46 | isInitialized = false; 47 | } 48 | 49 | @Override 50 | public Project openProject(String rootFolder) { 51 | checkIsInit(); 52 | var path = Path.of(rootFolder); 53 | if (!Files.isDirectory(path)) { 54 | throw new IllegalArgumentException("Path " + rootFolder + " does not exist or is not a directory"); 55 | } 56 | return new IpwProject(path, rootDisposable, applicationService, fileService); 57 | } 58 | 59 | @Override 60 | public Project openFile(String filePath) { 61 | checkIsInit(); 62 | var path = Path.of(filePath); 63 | if (!Files.isRegularFile(path)) { 64 | throw new IllegalArgumentException("File " + filePath + " does not exist or is not a file"); 65 | } 66 | return new IpwProject(path, rootDisposable, applicationService, fileService); 67 | } 68 | 69 | private void checkIsInit() { 70 | if (!wasInitialized) { 71 | throw new IllegalStateException("the wrapper is not initialized"); 72 | } 73 | } 74 | 75 | private void setSystemSettings() { 76 | System.setProperty("java.awt.headless", "true"); 77 | System.setProperty("idea.platform.prefix", "Idea"); 78 | System.setProperty("idea.ignore.disabled.plugins", "true"); 79 | System.setProperty("jna.nosys", "true"); 80 | 81 | // command line Java applications need a way to launch without a Dock icon. 82 | System.setProperty("apple.awt.UIElement", "true"); 83 | 84 | // running disposer in debug mode 85 | System.setProperty("idea.disposer.debug", Boolean.getBoolean("ipw.debug") ? "on" : "off"); 86 | 87 | // disable storing stack traces when PSI elements are invalidated 88 | System.setProperty("psi.track.invalidation", "false"); 89 | 90 | System.setProperty("idea.use.native.fs.for.win", "false"); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/IpwProject.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl; 2 | 3 | import io.github.n34t0.platform.CodeCompletionElement; 4 | import io.github.n34t0.platform.GotoDeclarationData; 5 | import io.github.n34t0.platform.Project; 6 | import io.github.n34t0.platform.impl.services.ApplicationService; 7 | import io.github.n34t0.platform.impl.services.FileService; 8 | import io.github.n34t0.platform.impl.services.ProjectService; 9 | import io.github.n34t0.platform.impl.services.impl.ProjectServiceImpl; 10 | import io.github.n34t0.platform.impl.services.impl.TempProjectServiceImpl; 11 | import com.intellij.codeInsight.lookup.LookupElement; 12 | import com.intellij.openapi.Disposable; 13 | import com.intellij.openapi.application.ReadAction; 14 | import com.intellij.openapi.util.Disposer; 15 | import com.intellij.openapi.util.io.FileUtil; 16 | 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.util.List; 20 | import java.util.stream.Collectors; 21 | 22 | public class IpwProject implements Project, Disposable { 23 | 24 | private ProjectService projectService; 25 | private boolean isDisposed; 26 | 27 | IpwProject(Path path, Disposable rootDisposable, ApplicationService applicationService, FileService fileService) { 28 | if (Files.isDirectory(path)) { 29 | String projectName = path.getFileName().toString(); 30 | projectService = new ProjectServiceImpl(applicationService, fileService, projectName, path); 31 | } else { 32 | String projectName = FileUtil.getNameWithoutExtension(path.getFileName().toString()); 33 | projectService = new TempProjectServiceImpl(applicationService, fileService, projectName); 34 | } 35 | projectService.init(this); 36 | projectService.createProject(); 37 | Disposer.register(rootDisposable, this); 38 | } 39 | 40 | @Override 41 | public void dispose() { 42 | isDisposed = true; 43 | projectService = null; 44 | } 45 | 46 | @Override 47 | public void addLibraries(List paths) { 48 | projectService.addLibraries(paths); 49 | } 50 | 51 | @Override 52 | public void synchronizeProjectDirectory() { 53 | projectService.synchronizeProjectDir(); 54 | } 55 | 56 | @Override 57 | public List getCodeCompletion(String path, int caretOffset) { 58 | return mapLookupElements(projectService.getCodeCompletion(path, caretOffset)); 59 | } 60 | 61 | @Override 62 | public GotoDeclarationData gotoDeclaration(String path, int caretOffset) { 63 | return projectService.gotoDeclaration(path, caretOffset); 64 | } 65 | 66 | private List mapLookupElements(List elements) { 67 | return ReadAction.compute(() -> elements.stream() 68 | .map(CcElement::new) 69 | .collect(Collectors.toList())); 70 | } 71 | 72 | @Override 73 | public void closeProject() { 74 | if (isDisposed) { 75 | throw new IllegalStateException("The project is already closed"); 76 | } 77 | Disposer.dispose(this); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/edt/EdtAdapter.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.edt; 2 | 3 | import com.intellij.ide.IpwEventQueue; 4 | 5 | import java.lang.reflect.InvocationTargetException; 6 | 7 | public class EdtAdapter { 8 | 9 | private EdtAdapter() {} 10 | 11 | @SuppressWarnings("unused") 12 | public static boolean isEventDispatchThread() { 13 | return IpwEventQueue.isDispatchThread(); 14 | } 15 | 16 | @SuppressWarnings("unused") 17 | public static void invokeLater(Runnable runnable) { 18 | IpwEventQueue.invokeLater(runnable); 19 | } 20 | 21 | @SuppressWarnings("unused") 22 | public static void invokeAndWait(Runnable runnable) throws InterruptedException, InvocationTargetException { 23 | IpwEventQueue.invokeAndWait(runnable); 24 | } 25 | 26 | @SuppressWarnings("unused") 27 | public static void dispatchAllInvocationEvents() { 28 | IpwEventQueue.dispatchAllInvocationEventsInIdeEventQueue(); 29 | } 30 | 31 | @SuppressWarnings("unused") 32 | public static Thread getEventQueueThread() { 33 | return IpwEventQueue.getInstance().getDispatchThread(); 34 | } 35 | 36 | @SuppressWarnings("unused") 37 | public static void invokeLaterIfNeeded(Runnable runnable) { 38 | if (IpwEventQueue.isDispatchThread()) { 39 | runnable.run(); 40 | } else { 41 | IpwEventQueue.invokeLater(runnable); 42 | } 43 | } 44 | 45 | @SuppressWarnings("unused") 46 | public static void invokeAndWaitIfNeeded(Runnable runnable) { 47 | if (IpwEventQueue.isDispatchThread()) { 48 | runnable.run(); 49 | } else { 50 | try { 51 | IpwEventQueue.invokeAndWait(runnable); 52 | } catch (Exception e) { 53 | throw new RuntimeException(e); 54 | } 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/edt/EdtUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.edt; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.util.Ref; 5 | import com.intellij.util.ThrowableRunnable; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | 9 | public final class EdtUtil { 10 | 11 | private EdtUtil() {} 12 | 13 | public static void runInEdtAndWait(ThrowableRunnable runnable) throws T { 14 | var app = ApplicationManager.getApplication(); 15 | if (app != null ? app.isDispatchThread() : EdtAdapter.isEventDispatchThread()) { 16 | runnable.run(); 17 | return; 18 | } 19 | 20 | Ref exception = new Ref<>(); 21 | Runnable r = () -> { 22 | try { 23 | runnable.run(); 24 | } catch (Throwable e) { 25 | exception.set((T)e); 26 | } 27 | }; 28 | 29 | if (app != null) { 30 | app.invokeAndWait(r); 31 | } else { 32 | try { 33 | EdtAdapter.invokeAndWait(r); 34 | } catch (InterruptedException | InvocationTargetException e) { 35 | throw new RuntimeException(e); 36 | } 37 | } 38 | 39 | if (!exception.isNull()) { 40 | throw exception.get(); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/ApplicationService.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services; 2 | 3 | import com.intellij.openapi.actionSystem.DataProvider; 4 | import com.intellij.openapi.application.Application; 5 | import com.intellij.openapi.projectRoots.Sdk; 6 | 7 | public interface ApplicationService extends Service { 8 | 9 | Application getApplication(); 10 | 11 | Sdk getSdk(); 12 | 13 | void setDataProvider(DataProvider provider); 14 | 15 | void dispatchAllInvocationEvents(); 16 | } 17 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/EditorService.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services; 2 | 3 | import io.github.n34t0.platform.impl.GTDData; 4 | import com.intellij.codeInsight.lookup.LookupElement; 5 | import com.intellij.openapi.vfs.VirtualFile; 6 | 7 | import java.util.List; 8 | 9 | public interface EditorService extends Service { 10 | 11 | void clearFileAndEditor(); 12 | 13 | void cancelExecutions(); 14 | 15 | List getCodeCompletion(VirtualFile file, int caretOffset); 16 | 17 | GTDData gotoDeclaration(VirtualFile file, int caretOffset); 18 | } 19 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/FileService.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services; 2 | 3 | import com.intellij.openapi.vfs.VirtualFile; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Path; 7 | 8 | public interface FileService extends Service { 9 | 10 | Path getTempRoot(); 11 | 12 | Path generateTempPath(String fileName); 13 | 14 | Path generateTempPath(String fileName, Path root); 15 | 16 | /** 17 | * Creates in-memory directory temp:///some/path. 18 | * This method should be only called within write-action. 19 | */ 20 | VirtualFile createDirInMemory(String dirName); 21 | 22 | void clearDirInMemory(VirtualFile dir) throws IOException; 23 | 24 | void clearDir(VirtualFile dir) throws IOException; 25 | 26 | VirtualFile getFile(VirtualFile root, String path); 27 | 28 | VirtualFile createFile(VirtualFile root, String path); 29 | 30 | void copyDir(VirtualFile src, VirtualFile dst); 31 | 32 | VirtualFile synchronizeDirVfs(Path dir); 33 | 34 | void synchronizeDirVfs(VirtualFile dir); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/ModuleService.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services; 2 | 3 | import com.intellij.openapi.module.Module; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.vfs.VirtualFile; 6 | 7 | import java.nio.file.Path; 8 | import java.util.List; 9 | 10 | public interface ModuleService extends Service { 11 | 12 | void createModule(Project project, Path projectFolder); 13 | 14 | void closeModule(); 15 | 16 | Module getModule(); 17 | 18 | void addLibraries(List paths); 19 | 20 | VirtualFile getSourceRoot(); 21 | 22 | void synchronizeSourceRoot(); 23 | 24 | VirtualFile refreshFile(String path); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/ProjectService.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services; 2 | 3 | import io.github.n34t0.platform.impl.GTDData; 4 | import com.intellij.codeInsight.lookup.LookupElement; 5 | import com.intellij.openapi.project.Project; 6 | 7 | import java.util.List; 8 | 9 | public interface ProjectService extends Service { 10 | 11 | void createProject(); 12 | 13 | void closeProject(); 14 | 15 | Project getProject(); 16 | 17 | void addLibraries(List paths); 18 | 19 | void synchronizeProjectDir(); 20 | 21 | List getCodeCompletion(String path, int caretOffset); 22 | 23 | GTDData gotoDeclaration(String path, int caretOffset); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/Service.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services; 2 | 3 | import com.intellij.openapi.Disposable; 4 | 5 | public interface Service extends Disposable { 6 | 7 | void init(Disposable parentDisposable); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/impl/InspectionsUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services.impl; 2 | 3 | import com.intellij.codeInspection.InspectionProfileEntry; 4 | import com.intellij.codeInspection.LocalInspectionTool; 5 | import com.intellij.codeInspection.ex.InspectionProfileImpl; 6 | import com.intellij.codeInspection.ex.InspectionProfileKt; 7 | import com.intellij.codeInspection.ex.InspectionToolRegistrar; 8 | import com.intellij.codeInspection.ex.InspectionToolWrapper; 9 | import com.intellij.codeInspection.ex.InspectionToolsSupplier; 10 | import com.intellij.openapi.Disposable; 11 | import com.intellij.openapi.project.Project; 12 | import com.intellij.openapi.util.Disposer; 13 | import com.intellij.profile.codeInspection.BaseInspectionProfileManager; 14 | import com.intellij.profile.codeInspection.InspectionProfileManager; 15 | import com.intellij.profile.codeInspection.ProjectInspectionProfileManager; 16 | import com.intellij.util.ReflectionUtil; 17 | import com.intellij.util.containers.UtilKt; 18 | 19 | import java.lang.reflect.Field; 20 | import java.util.UUID; 21 | 22 | final class InspectionsUtil { 23 | 24 | private static final Field myToolField; 25 | 26 | static { 27 | try { 28 | myToolField = ReflectionUtil 29 | .findField(InspectionToolWrapper.class, InspectionProfileEntry.class, "myTool"); 30 | } catch (NoSuchFieldException e) { 31 | throw new RuntimeException(e); 32 | } 33 | } 34 | 35 | private InspectionsUtil() {} 36 | 37 | static void configureInspections(Project project, Disposable rootDisposable) { 38 | var tools = LocalInspectionTool.EMPTY_ARRAY; 39 | var toolSupplier = new InspectionToolsSupplier.Simple(UtilKt.mapSmart(tools, InspectionToolRegistrar::wrapTool)); 40 | Disposer.register(rootDisposable, toolSupplier); 41 | var profile = new InspectionProfileImpl(UUID.randomUUID().toString(), toolSupplier, 42 | ((BaseInspectionProfileManager)InspectionProfileManager.getInstance())); 43 | var profileManager = ProjectInspectionProfileManager.getInstance(project); 44 | Disposer.register(rootDisposable, () -> { 45 | profileManager.deleteProfile(profile); 46 | profileManager.setCurrentProfile(null); 47 | clearAllToolsIn(InspectionProfileKt.getBASE_PROFILE()); 48 | }); 49 | 50 | profileManager.addProfile(profile); 51 | profileManager.setCurrentProfile(profile); 52 | } 53 | 54 | private static void clearAllToolsIn(InspectionProfileImpl profile) { 55 | if (!profile.wasInitialized()) return; 56 | 57 | for (var state : profile.getAllTools()) { 58 | var wrapper = state.getTool(); 59 | if (wrapper.getExtension() != null) { 60 | ReflectionUtil.resetField(wrapper, myToolField); 61 | } 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/impl/IpwDataProvider.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services.impl; 2 | 3 | import com.intellij.openapi.actionSystem.CommonDataKeys; 4 | import com.intellij.openapi.actionSystem.DataContext; 5 | import com.intellij.openapi.actionSystem.DataProvider; 6 | import com.intellij.openapi.actionSystem.PlatformDataKeys; 7 | import com.intellij.openapi.editor.Editor; 8 | import com.intellij.openapi.editor.impl.EditorComponentImpl; 9 | import com.intellij.openapi.fileEditor.OpenFileDescriptor; 10 | import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; 11 | import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl; 12 | import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider; 13 | import com.intellij.openapi.project.Project; 14 | import javax.swing.JComponent; 15 | import org.jetbrains.annotations.NonNls; 16 | import org.jetbrains.annotations.NotNull; 17 | import org.jetbrains.annotations.Nullable; 18 | 19 | class IpwDataProvider implements DataProvider, DataContext { 20 | private final Project myProject; 21 | 22 | IpwDataProvider(Project project) { 23 | myProject = project; 24 | } 25 | 26 | @Override 27 | @Nullable 28 | public Object getData(@NotNull @NonNls String dataId) { 29 | if (myProject.isDisposed()) { 30 | throw new RuntimeException("IpwDataProvider is already disposed for " + myProject); 31 | } 32 | 33 | if (CommonDataKeys.PROJECT.is(dataId)) { 34 | return myProject; 35 | } 36 | FileEditorManagerEx manager = FileEditorManagerEx.getInstanceEx(myProject); 37 | if (manager == null) return null; 38 | 39 | if (CommonDataKeys.EDITOR.is(dataId) || OpenFileDescriptor.NAVIGATE_IN_EDITOR.is(dataId)) { 40 | return manager instanceof FileEditorManagerImpl 41 | ? ((FileEditorManagerImpl)manager).getSelectedTextEditor(true) 42 | : manager.getSelectedTextEditor(); 43 | } else if (PlatformDataKeys.FILE_EDITOR.is(dataId)) { 44 | Editor editor = manager.getSelectedTextEditor(); 45 | return editor == null ? null : TextEditorProvider.getInstance().getTextEditor(editor); 46 | } else { 47 | Editor editor = getData(CommonDataKeys.EDITOR); 48 | if (editor != null) { 49 | Object managerData = manager.getData(dataId, editor, editor.getCaretModel().getCurrentCaret()); 50 | if (managerData != null) { 51 | return managerData; 52 | } 53 | JComponent component = editor.getContentComponent(); 54 | if (component instanceof EditorComponentImpl) { 55 | return ((EditorComponentImpl)component).getData(dataId); 56 | } 57 | } 58 | } 59 | 60 | return null; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/impl/IpwService.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services.impl; 2 | 3 | import io.github.n34t0.platform.impl.services.Service; 4 | import com.intellij.openapi.Disposable; 5 | import com.intellij.openapi.util.Disposer; 6 | 7 | public abstract class IpwService implements Service { 8 | 9 | private volatile boolean isInitialized; 10 | private final Object lockObject = new Object(); 11 | 12 | @Override 13 | public void init(Disposable parentDisposable) { 14 | if (isInitialized) return; 15 | synchronized (lockObject) { 16 | if (isInitialized) return; 17 | Disposer.register(parentDisposable, this); 18 | doInit(); 19 | isInitialized = true; 20 | } 21 | } 22 | 23 | protected abstract void doInit(); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/impl/IpwUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services.impl; 2 | 3 | import io.github.n34t0.platform.impl.edt.EdtUtil; 4 | import com.intellij.openapi.application.ApplicationManager; 5 | import com.intellij.psi.impl.DocumentCommitProcessor; 6 | import com.intellij.psi.impl.DocumentCommitThread; 7 | import com.intellij.util.ReflectionUtil; 8 | import com.intellij.util.indexing.FileBasedIndex; 9 | import com.intellij.util.indexing.FileBasedIndexImpl; 10 | 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.util.Hashtable; 13 | import java.util.Map; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | final class IpwUtil { 17 | 18 | private IpwUtil() {} 19 | 20 | static void appWaitForThreads() { 21 | try { 22 | EdtUtil.runInEdtAndWait(() -> { 23 | var app = ApplicationManager.getApplication(); 24 | if (app != null && !app.isDisposed()) { 25 | var index = app.getServiceIfCreated(FileBasedIndex.class); 26 | if (index instanceof FileBasedIndexImpl) { 27 | ((FileBasedIndexImpl)index).getChangedFilesCollector() 28 | .waitForVfsEventsExecuted(10, TimeUnit.SECONDS); 29 | } 30 | 31 | var commitThread = ((DocumentCommitThread)app.getServiceIfCreated(DocumentCommitProcessor.class)); 32 | if (commitThread != null) { 33 | commitThread.waitForAllCommits(10, TimeUnit.SECONDS); 34 | } 35 | } 36 | }); 37 | } catch (Exception e) { 38 | throw new RuntimeException(e); 39 | } 40 | } 41 | 42 | static void cleanupSwingDataStructures() throws IllegalAccessException, InvocationTargetException, ClassNotFoundException { 43 | Object manager = ReflectionUtil.getDeclaredMethod(Class.forName("javax.swing.KeyboardManager"), "getCurrentManager").invoke(null); 44 | Map componentKeyStrokeMap = ReflectionUtil.getField(manager.getClass(), manager, Hashtable.class, "componentKeyStrokeMap"); 45 | componentKeyStrokeMap.clear(); 46 | Map containerMap = ReflectionUtil.getField(manager.getClass(), manager, Hashtable.class, "containerMap"); 47 | containerMap.clear(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/impl/ModuleServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services.impl; 2 | 3 | import io.github.n34t0.platform.impl.services.ApplicationService; 4 | import io.github.n34t0.platform.impl.services.FileService; 5 | import com.intellij.openapi.util.io.FileUtil; 6 | import com.intellij.openapi.vfs.VirtualFile; 7 | import com.intellij.util.io.PathKt; 8 | 9 | import java.nio.file.Path; 10 | 11 | public class ModuleServiceImpl extends AbstractModuleService { 12 | 13 | private Path myModuleFile; 14 | 15 | ModuleServiceImpl(ApplicationService applicationService, FileService fileService) { 16 | super(applicationService, fileService); 17 | } 18 | 19 | @Override 20 | protected Path getModuleFilePath() { 21 | if (myModuleFile == null) { 22 | myModuleFile = myFileService.generateTempPath(myModuleName + ".iml"); 23 | } 24 | return myModuleFile; 25 | } 26 | 27 | @Override 28 | protected void deleteModuleFiles() { 29 | if (myModuleFile != null) { 30 | PathKt.delete(myModuleFile); 31 | } 32 | } 33 | 34 | @Override 35 | public VirtualFile refreshFile(String path) { 36 | path = FileUtil.toSystemIndependentName(path); 37 | if (path.startsWith(mySourceRoot.getPath())) { 38 | path = path.substring(mySourceRoot.getPath().length()); 39 | } 40 | var vFile = findFileInSource(path); 41 | if (vFile == null) { 42 | throw new IllegalArgumentException("Path: " + path + " not found in project folder"); 43 | } 44 | vFile.refresh(false, false); 45 | return vFile; 46 | } 47 | 48 | @Override 49 | protected VirtualFile defineSourceRoot() { 50 | return myFileService.synchronizeDirVfs(myProjectFolder); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/impl/ProjectServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services.impl; 2 | 3 | import io.github.n34t0.platform.impl.services.ApplicationService; 4 | import io.github.n34t0.platform.impl.services.FileService; 5 | import io.github.n34t0.platform.impl.services.ModuleService; 6 | 7 | import java.nio.file.Path; 8 | 9 | public class ProjectServiceImpl extends AbstractProjectService { 10 | 11 | private final Path myProjectFolder; 12 | 13 | public ProjectServiceImpl(ApplicationService applicationService, FileService fileService, 14 | String projectName, Path projectFolder) { 15 | super(applicationService, fileService, projectName); 16 | myProjectFolder = projectFolder; 17 | } 18 | 19 | @Override 20 | protected Path getProjectFolder() { 21 | return myProjectFolder; 22 | } 23 | 24 | @Override 25 | protected ModuleService createModuleService() { 26 | return new ModuleServiceImpl(myApplicationService, myFileService); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/impl/TempModuleServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services.impl; 2 | 3 | import io.github.n34t0.platform.impl.services.ApplicationService; 4 | import io.github.n34t0.platform.impl.services.FileService; 5 | import com.intellij.openapi.application.WriteAction; 6 | import com.intellij.openapi.command.WriteCommandAction; 7 | import com.intellij.openapi.fileEditor.FileDocumentManager; 8 | import com.intellij.openapi.vfs.LocalFileSystem; 9 | import com.intellij.openapi.vfs.VirtualFile; 10 | 11 | import java.io.IOException; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | 15 | class TempModuleServiceImpl extends AbstractModuleService { 16 | 17 | private Path moduleFile; 18 | 19 | TempModuleServiceImpl(ApplicationService applicationService, FileService fileService) { 20 | super(applicationService, fileService); 21 | } 22 | 23 | @Override 24 | protected Path getModuleFilePath() { 25 | if (moduleFile == null) { 26 | moduleFile = Paths.get(myProjectFolder.toString(), myModuleName + ".iml"); 27 | } 28 | return moduleFile; 29 | } 30 | 31 | @Override 32 | protected void deleteModuleFiles() { 33 | if (myModule != null && mySourceRoot != null) { 34 | WriteCommandAction.runWriteCommandAction(myModule.getProject(), () -> { 35 | try { 36 | myFileService.clearDir(mySourceRoot); 37 | } catch (IOException e) { 38 | //noinspection CallToPrintStackTrace 39 | e.printStackTrace(); 40 | } 41 | }); 42 | } 43 | } 44 | 45 | @Override 46 | public VirtualFile refreshFile(String path) { 47 | var vFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(path); 48 | if (vFile == null) { 49 | throw new IllegalArgumentException("Path: " + path + " not found"); 50 | } 51 | vFile.refresh(false, true); 52 | return copyFileToSource(vFile); 53 | } 54 | 55 | private VirtualFile copyFileToSource(VirtualFile file) { 56 | String fileName = file.getName(); 57 | var targetFile = myFileService.getFile(mySourceRoot, fileName); 58 | if (targetFile == null) { 59 | targetFile = myFileService.createFile(mySourceRoot, fileName); 60 | } 61 | copyContent(file, targetFile); 62 | return targetFile; 63 | } 64 | 65 | private void copyContent(VirtualFile file, VirtualFile targetFile) { 66 | try { 67 | WriteAction.runAndWait(() -> { 68 | targetFile.setBinaryContent(file.contentsToByteArray()); 69 | FileDocumentManager.getInstance().reloadFiles(targetFile); 70 | }); 71 | } catch (IOException e) { 72 | throw new RuntimeException(e); 73 | } 74 | } 75 | 76 | @Override 77 | protected VirtualFile defineSourceRoot() { 78 | return myFileService.createDirInMemory(myModuleName); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/java/io/github/n34t0/platform/impl/services/impl/TempProjectServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform.impl.services.impl; 2 | 3 | import io.github.n34t0.platform.impl.services.ApplicationService; 4 | import io.github.n34t0.platform.impl.services.FileService; 5 | import io.github.n34t0.platform.impl.services.ModuleService; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.util.io.PathKt; 8 | 9 | import java.io.IOException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | 13 | public class TempProjectServiceImpl extends AbstractProjectService { 14 | 15 | private Path projectFolder; 16 | 17 | public TempProjectServiceImpl(ApplicationService applicationService, 18 | FileService fileService, String projectName) { 19 | super(applicationService, fileService, projectName); 20 | } 21 | 22 | @Override 23 | protected Path getProjectFolder() { 24 | if (projectFolder == null) { 25 | var pf = myFileService.generateTempPath(myProjectName); 26 | try { 27 | Files.createDirectories(pf); 28 | } catch (IOException e) { 29 | throw new RuntimeException(e); 30 | } 31 | projectFolder = pf; 32 | } 33 | return projectFolder; 34 | } 35 | 36 | @Override 37 | protected ModuleService createModuleService() { 38 | return new TempModuleServiceImpl(myApplicationService, myFileService); 39 | } 40 | 41 | @Override 42 | protected void deleteProjectFiles(Project project) { 43 | super.deleteProjectFiles(project); 44 | PathKt.delete(projectFolder); 45 | projectFolder = null; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /component/platform/lib/src/main/resources/META-INF/Editor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /component/platform/lib/src/test/java/io/github/n34t0/platform/AddLibFileTests.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | 7 | import java.util.List; 8 | 9 | import static io.github.n34t0.platform.WrapperExtension.assertCodeCompletion; 10 | import static io.github.n34t0.platform.WrapperExtension.platform; 11 | import static io.github.n34t0.platform.WrapperExtension.tempDir; 12 | 13 | @ExtendWith(WrapperExtension.class) 14 | class AddLibFileTests { 15 | 16 | @Test 17 | void severalPathsInOneCallTest() { 18 | var file = tempDir + "/files/LibTest.java"; 19 | Assertions.assertDoesNotThrow(() -> { 20 | var project = platform.openFile(file); 21 | project.addLibraries(tempDir + "/lib/api.jar", tempDir + "/lib/implA.jar"); 22 | assertCodeCompletion(project, file, List.of("ClassA"), List.of("ClassB", "ClassC")); 23 | project.closeProject(); 24 | }); 25 | } 26 | 27 | @Test 28 | void severalCallsTest() { 29 | var file = tempDir + "/files/LibTest.java"; 30 | Assertions.assertDoesNotThrow(() -> { 31 | var project = platform.openFile(file); 32 | project.addLibraries(tempDir + "/lib/api.jar"); 33 | project.addLibraries(tempDir + "/lib/implA.jar"); 34 | assertCodeCompletion(project, file, List.of("ClassA"), List.of("ClassB", "ClassC")); 35 | project.closeProject(); 36 | }); 37 | } 38 | 39 | @Test 40 | void oneFileAndOneDirInOneCallTest() { 41 | var file = tempDir + "/files/LibTest.java"; 42 | Assertions.assertDoesNotThrow(() -> { 43 | var project = platform.openFile(file); 44 | project.addLibraries(tempDir + "/lib/api.jar", tempDir + "/lib/sub"); 45 | assertCodeCompletion(project, file, List.of("ClassB", "ClassC"), List.of("ClassA")); 46 | project.closeProject(); 47 | }); 48 | } 49 | 50 | @Test 51 | void rootDirTest() { 52 | var file = tempDir + "/files/LibTest.java"; 53 | Assertions.assertDoesNotThrow(() -> { 54 | var project = platform.openFile(file); 55 | project.addLibraries(tempDir + "/lib"); 56 | assertCodeCompletion(project, file, List.of("ClassA", "ClassB", "ClassC")); 57 | project.closeProject(); 58 | }); 59 | } 60 | 61 | @Test 62 | void onlyApiTest() { 63 | var file = tempDir + "/files/LibTest.java"; 64 | Assertions.assertDoesNotThrow(() -> { 65 | var project = platform.openFile(file); 66 | project.addLibraries(tempDir + "/lib/api.jar"); 67 | assertCodeCompletion(project, file, List.of(), List.of("ClassA", "ClassB", "ClassC")); 68 | project.closeProject(); 69 | }); 70 | } 71 | 72 | @Test 73 | void addLibsBetweenCodeCompletionCallsTest() { 74 | var file = tempDir + "/files/LibTest.java"; 75 | Assertions.assertDoesNotThrow(() -> { 76 | var project = platform.openFile(file); 77 | 78 | project.addLibraries(tempDir + "/lib/api.jar"); 79 | assertCodeCompletion(project, file, List.of(), List.of("ClassA", "ClassB", "ClassC")); 80 | 81 | project.addLibraries(tempDir + "/lib/implA.jar"); 82 | assertCodeCompletion(project, file, List.of("ClassA"), List.of("ClassB", "ClassC")); 83 | 84 | project.addLibraries(tempDir + "/lib"); 85 | assertCodeCompletion(project, file, List.of("ClassA", "ClassB", "ClassC")); 86 | 87 | project.closeProject(); 88 | }); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /component/platform/lib/src/test/java/io/github/n34t0/platform/AddLibProjectTests.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | 7 | import java.util.List; 8 | 9 | import static io.github.n34t0.platform.WrapperExtension.assertCodeCompletion; 10 | import static io.github.n34t0.platform.WrapperExtension.platform; 11 | import static io.github.n34t0.platform.WrapperExtension.tempDir; 12 | 13 | @ExtendWith(WrapperExtension.class) 14 | class AddLibProjectTests { 15 | 16 | @Test 17 | void severalCallsTest() { 18 | var projectDir = tempDir + "/projects/libJavaProject"; 19 | var file = "org/example/Main.java"; 20 | Assertions.assertDoesNotThrow(() -> { 21 | var project = platform.openProject(projectDir); 22 | project.addLibraries(tempDir + "/lib/api.jar"); 23 | project.addLibraries(tempDir + "/lib/implA.jar"); 24 | assertCodeCompletion(project, file, projectDir, 25 | List.of("ClassA", "ClassD"), List.of("ClassB", "ClassC")); 26 | project.closeProject(); 27 | }); 28 | } 29 | 30 | @Test 31 | void oneFileAndOneDirInOneCallTest() { 32 | var projectDir = tempDir + "/projects/libJavaProject"; 33 | var file = "org/example/Main.java"; 34 | Assertions.assertDoesNotThrow(() -> { 35 | var project = platform.openProject(projectDir); 36 | project.addLibraries(tempDir + "/lib/api.jar", tempDir + "/lib/sub"); 37 | assertCodeCompletion(project, file, projectDir, 38 | List.of("ClassB", "ClassC", "ClassD"), List.of("ClassA")); 39 | project.closeProject(); 40 | }); 41 | } 42 | 43 | @Test 44 | void rootDirTest() { 45 | var projectDir = tempDir + "/projects/libJavaProject"; 46 | var file = "org/example/Main.java"; 47 | Assertions.assertDoesNotThrow(() -> { 48 | var project = platform.openProject(projectDir); 49 | project.addLibraries(tempDir + "/lib"); 50 | assertCodeCompletion(project, file, projectDir, 51 | List.of("ClassA", "ClassB", "ClassC", "ClassD")); 52 | project.closeProject(); 53 | }); 54 | } 55 | 56 | @Test 57 | void onlyApiTest() { 58 | var projectDir = tempDir + "/projects/libJavaProject"; 59 | var file = "org/example/Main.java"; 60 | Assertions.assertDoesNotThrow(() -> { 61 | var project = platform.openProject(projectDir); 62 | project.addLibraries(tempDir + "/lib/api.jar"); 63 | assertCodeCompletion(project, file, projectDir, 64 | List.of("ClassD"), List.of("ClassA", "ClassB", "ClassC")); 65 | project.closeProject(); 66 | }); 67 | } 68 | 69 | @Test 70 | void addLibsBetweenCodeCompletionCallsTest() { 71 | var projectDir = tempDir + "/projects/libJavaProject"; 72 | var file = "org/example/Main.java"; 73 | Assertions.assertDoesNotThrow(() -> { 74 | var project = platform.openProject(projectDir); 75 | 76 | project.addLibraries(tempDir + "/lib/api.jar"); 77 | assertCodeCompletion(project, file, projectDir, 78 | List.of("ClassD"), List.of("ClassA", "ClassB", "ClassC")); 79 | 80 | project.addLibraries(tempDir + "/lib/implA.jar"); 81 | assertCodeCompletion(project, file, projectDir, 82 | List.of("ClassA", "ClassD"), List.of("ClassB", "ClassC")); 83 | 84 | project.addLibraries(tempDir + "/lib"); 85 | assertCodeCompletion(project, file, projectDir, 86 | List.of("ClassA", "ClassB", "ClassC", "ClassD")); 87 | 88 | project.closeProject(); 89 | }); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /component/platform/lib/src/test/java/io/github/n34t0/platform/GTDTests.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | 7 | import static io.github.n34t0.platform.WrapperExtension.assertGotoDeclaration; 8 | import static io.github.n34t0.platform.WrapperExtension.assertLocalGotoDeclaration; 9 | import static io.github.n34t0.platform.WrapperExtension.platform; 10 | import static io.github.n34t0.platform.WrapperExtension.tempDir; 11 | 12 | @ExtendWith(WrapperExtension.class) 13 | class GTDTests { 14 | 15 | @Test 16 | void localJavaTest() { 17 | var file = tempDir + "/files/GTDTest.java"; 18 | Assertions.assertDoesNotThrow(() -> { 19 | var project = platform.openFile(file); 20 | assertLocalGotoDeclaration(project, file); 21 | project.closeProject(); 22 | }); 23 | } 24 | 25 | @Test 26 | void javaTest() { 27 | var projectDir = tempDir + "/projects/GTDJavaProject"; 28 | var file = "org/example/Main.java"; 29 | Assertions.assertDoesNotThrow(() -> { 30 | var project = platform.openProject(projectDir); 31 | assertGotoDeclaration(project, file, projectDir); 32 | project.closeProject(); 33 | }); 34 | } 35 | 36 | @Test 37 | void localKotlinTest() { 38 | var file = tempDir + "/files/GTDTest.kt"; 39 | Assertions.assertDoesNotThrow(() -> { 40 | var project = platform.openFile(file); 41 | assertLocalGotoDeclaration(project, file); 42 | project.closeProject(); 43 | }); 44 | } 45 | 46 | @Test 47 | void kotlinTest() { 48 | var projectDir = tempDir + "/projects/GTDKotlinProject"; 49 | var file = "org/example/main.kt"; 50 | Assertions.assertDoesNotThrow(() -> { 51 | var project = platform.openProject(projectDir); 52 | assertGotoDeclaration(project, file, projectDir); 53 | project.closeProject(); 54 | }); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /component/platform/lib/src/test/java/io/github/n34t0/platform/IsolationTests.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | 7 | import java.util.List; 8 | 9 | import static io.github.n34t0.platform.WrapperExtension.assertCodeCompletion; 10 | import static io.github.n34t0.platform.WrapperExtension.platform; 11 | import static io.github.n34t0.platform.WrapperExtension.tempDir; 12 | 13 | @ExtendWith(WrapperExtension.class) 14 | class IsolationTests { 15 | 16 | @Test 17 | void twoFilesWithImplementationsOfTheSameInterfaceTest() { 18 | var file1 = tempDir + "/files/IsolationTest1.java"; 19 | var file2 = tempDir + "/files/IsolationTest2.java"; 20 | Assertions.assertDoesNotThrow(() -> { 21 | var project1 = platform.openFile(file1); 22 | var project2 = platform.openFile(file2); 23 | assertCodeCompletion(project1, file1, List.of("IsolationTest1"), List.of("IsolationTest2")); 24 | assertCodeCompletion(project2, file2, List.of("IsolationTest2"), List.of("IsolationTest1")); 25 | project1.closeProject(); 26 | project2.closeProject(); 27 | }); 28 | } 29 | 30 | @Test 31 | void twoProjectsWithImplementationsOfTheSameInterfaceTest() { 32 | var projectDir1 = tempDir + "/projects/isolationProject1"; 33 | var projectDir2 = tempDir + "/projects/isolationProject2"; 34 | var file = "org/example/Main.java"; 35 | Assertions.assertDoesNotThrow(() -> { 36 | var project1 = platform.openProject(projectDir1); 37 | var project2 = platform.openProject(projectDir2); 38 | assertCodeCompletion(project1, file, projectDir1, List.of("IsoClass1"), List.of("IsoClass2")); 39 | assertCodeCompletion(project2, file, projectDir2, List.of("IsoClass2"), List.of("IsoClass1")); 40 | project1.closeProject(); 41 | project2.closeProject(); 42 | }); 43 | } 44 | 45 | @Test 46 | void twoProjectsWithImplementationsOfTheSameLibTest() { 47 | var projectDir1 = tempDir + "/projects/isolationLibProject1"; 48 | var projectDir2 = tempDir + "/projects/isolationLibProject2"; 49 | var file = "org/example/Main.java"; 50 | Assertions.assertDoesNotThrow(() -> { 51 | var project1 = platform.openProject(projectDir1); 52 | var project2 = platform.openProject(projectDir2); 53 | project1.addLibraries(tempDir + "/lib/api.jar"); 54 | project2.addLibraries(tempDir + "/lib/api.jar"); 55 | assertCodeCompletion(project1, file, projectDir1, List.of("Impl1"), List.of("Impl2")); 56 | assertCodeCompletion(project2, file, projectDir2, List.of("Impl2"), List.of("Impl1")); 57 | project1.closeProject(); 58 | project2.closeProject(); 59 | }); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /component/platform/lib/src/test/java/io/github/n34t0/platform/ProjectFileOperationsTests.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; 5 | import org.junit.jupiter.api.Order; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.TestMethodOrder; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.StandardOpenOption; 13 | import java.util.List; 14 | 15 | import static io.github.n34t0.platform.WrapperExtension.assertCodeCompletion; 16 | import static io.github.n34t0.platform.WrapperExtension.platform; 17 | import static io.github.n34t0.platform.WrapperExtension.replaceInFile; 18 | import static io.github.n34t0.platform.WrapperExtension.tempDir; 19 | 20 | @ExtendWith(WrapperExtension.class) 21 | @TestMethodOrder(OrderAnnotation.class) 22 | class ProjectFileOperationsTests { 23 | 24 | @Test 25 | @Order(1) 26 | void newFileTest() { 27 | var projectDir = tempDir + "/projects/fileOperationsProject"; 28 | var file = projectDir + "/org/example/Main.java"; 29 | var newFile = projectDir + "/org/example/Test.java"; 30 | var sourceFile = tempDir + "/projects/simpleJavaProject/Test.java"; 31 | Assertions.assertDoesNotThrow(() -> { 32 | var project = platform.openProject(projectDir); 33 | assertCodeCompletion(project, file, List.of("ImplA")); 34 | 35 | var code = Files.readString(Path.of(sourceFile)); 36 | Files.writeString(Path.of(newFile), code, StandardOpenOption.CREATE_NEW); 37 | assertCodeCompletion(project, newFile, List.of("ArrayList")); 38 | 39 | project.closeProject(); 40 | }); 41 | } 42 | 43 | 44 | 45 | @Test 46 | @Order(2) 47 | void addFileTest() { 48 | var projectDir = tempDir + "/projects/fileOperationsProject"; 49 | var file = projectDir + "/org/example/Main.java"; 50 | var implA = projectDir + "/org/example/a/ImplA.java"; 51 | var implB = projectDir + "/org/example/a/ImplB.java"; 52 | Assertions.assertDoesNotThrow(() -> { 53 | var project = platform.openProject(projectDir); 54 | assertCodeCompletion(project, file, List.of("ImplA"), List.of("ImplB")); 55 | 56 | Files.copy(Path.of(implA), Path.of(implB)); 57 | replaceInFile(implB, "ImplA", "ImplB"); 58 | project.synchronizeProjectDirectory(); 59 | assertCodeCompletion(project, file, List.of("ImplA", "ImplB")); 60 | 61 | project.closeProject(); 62 | }); 63 | } 64 | 65 | @Test 66 | @Order(3) 67 | void deleteFileTest() { 68 | var projectDir = tempDir + "/projects/fileOperationsProject"; 69 | var file = projectDir + "/org/example/Main.java"; 70 | var implB = projectDir + "/org/example/a/ImplB.java"; 71 | Assertions.assertDoesNotThrow(() -> { 72 | var project = platform.openProject(projectDir); 73 | assertCodeCompletion(project, file, List.of("ImplA", "ImplB")); 74 | 75 | Files.delete(Path.of(implB)); 76 | project.synchronizeProjectDirectory(); 77 | assertCodeCompletion(project, file, List.of("ImplA"), List.of("ImplB")); 78 | 79 | project.closeProject(); 80 | }); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /component/platform/lib/src/test/java/io/github/n34t0/platform/UpdateTests.java: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.platform; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; 5 | import org.junit.jupiter.api.Order; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.TestMethodOrder; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | 10 | import java.util.List; 11 | 12 | import static io.github.n34t0.platform.WrapperExtension.assertCodeCompletion; 13 | import static io.github.n34t0.platform.WrapperExtension.platform; 14 | import static io.github.n34t0.platform.WrapperExtension.replaceInFile; 15 | import static io.github.n34t0.platform.WrapperExtension.tempDir; 16 | 17 | @ExtendWith(WrapperExtension.class) 18 | @TestMethodOrder(OrderAnnotation.class) 19 | class UpdateTests { 20 | 21 | @Test 22 | @Order(1) 23 | void javaProjectImportedFileUpdateTest() { 24 | var projectDir = tempDir + "/projects/updateJavaProject"; 25 | var file = projectDir + "/org/example/Main.java"; 26 | var importedFile = projectDir + "/org/example/a/ClassA.java"; 27 | Assertions.assertDoesNotThrow(() -> { 28 | var project = platform.openProject(projectDir); 29 | assertCodeCompletion(project, file, List.of("getJ"), List.of("getI")); 30 | replaceInFile(importedFile, 31 | "getJ() {\n return j;", 32 | "getI() {\n return i;"); 33 | project.synchronizeProjectDirectory(); 34 | assertCodeCompletion(project, file, List.of("getI"), List.of("getJ")); 35 | project.closeProject(); 36 | }); 37 | } 38 | 39 | @Test 40 | @Order(2) 41 | void javaProjectUpdateTest() { 42 | var projectDir = tempDir + "/projects/updateJavaProject"; 43 | var file = projectDir + "/org/example/Main.java"; 44 | Assertions.assertDoesNotThrow(() -> { 45 | var project = platform.openProject(projectDir); 46 | assertCodeCompletion(project, file, List.of("getI"), List.of("setI")); 47 | replaceInFile(file, "classA.g", "classA.s"); 48 | assertCodeCompletion(project, file, List.of("setI"), List.of("getI")); 49 | project.closeProject(); 50 | }); 51 | } 52 | 53 | @Test 54 | @Order(3) 55 | void javaOneFileUpdateTest() { 56 | var file = tempDir + "/files/UpdateTest.java"; 57 | Assertions.assertDoesNotThrow(() -> { 58 | var project = platform.openFile(file); 59 | assertCodeCompletion(project, file, List.of("SortedMap"), List.of("ArrayList")); 60 | replaceInFile(file, "new So", "new Ar"); 61 | assertCodeCompletion(project, file, List.of("ArrayList"), List.of("SortedMap")); 62 | project.closeProject(); 63 | }); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /component/platform/lib/src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=DEBUG, CONSOLE 2 | log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 3 | log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout 4 | log4j.appender.CONSOLE.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n 5 | -------------------------------------------------------------------------------- /component/platform/lib/testData/files/GTDTest.java: -------------------------------------------------------------------------------- 1 | public class GTDTest { 2 | public static final int CONST_FIELD = 4; 3 | 4 | private int field; 5 | 6 | GTDTest() {} 7 | 8 | public static void main(String[] args) { 9 | GTDTest test = new GTDTest(); 10 | test.method(5); 11 | int i = test.field; 12 | i = CONST_FIELD; 13 | staticMethod(); 14 | test.staticMethod(); 15 | GTDTest.staticMethod(); 16 | 17 | InnerClass innerClass = new InnerClass(); 18 | i = innerClass.field; 19 | innerClass.method(); 20 | 21 | SomeClass someClass = new SomeClass(); 22 | i = someClass.field; 23 | someClass.method(); 24 | 25 | for(int k = 5; k > 0; k--) { 26 | if (k > 2) continue; 27 | break; 28 | } 29 | } 30 | 31 | private void method(int parameter) { 32 | int localVariable = parameter; 33 | field = localVariable; 34 | } 35 | 36 | static void staticMethod() {} 37 | 38 | private class InnerClass { 39 | int field = 4; 40 | 41 | void method() {} 42 | } 43 | } 44 | 45 | class SomeClass { 46 | int field = 5; 47 | void method() {} 48 | } 49 | 50 | // (class self) 20 0 (13 20) 51 | // (method self main) 138 0 (134 138) 52 | // (class type) 168 13 (164 171) 53 | // (local variable self test) 174 0 (172 176) 54 | // (local variable self test) 176 0 (172 176) 55 | // (constructor) 186 97 (183 190) 56 | // (local variable test) 204 172 (202 206) 57 | // (method) 209 695 (207 213) 58 | // (local variable test) 236 172 (234 238) 59 | // (class field) 242 85 (239 244) 60 | // (local variable i) 255 230 (254 255) 61 | // (const) 262 51 (258 269) 62 | // (static method) 285 812 (279 291) 63 | // (local variable test) 305 172 (303 307) 64 | // (static method) 311 812 (308 320) 65 | // (class type) 339 13 (332 339) 66 | // (static method) 351 812 (340 352) 67 | // (inner class) 371 849 (365 375) 68 | // (inner class default constructor) 398 849 (393 403) 69 | // (local variable i) 415 230 (415 416) 70 | // (local variable innerClass) 424 376 (419 429) 71 | // (inner class field) 432 874 (430 435) 72 | // (local variable innerClass) 445 376 (445 455) 73 | // (inner class method) 460 899 (456 462) 74 | // (some class) 480 926 (475 484) 75 | // (some class constructor) 505 926 (501 510) 76 | // (local variable i) 523 230 (522 523) 77 | // (local variable someClass) 531 485 (526 535) 78 | // (some class field) 538 946 (536 541) 79 | // (local variable someClass) 556 485 (551 560) 80 | // (some class method) 564 966 (561 567) 81 | // (for variable) 595 588 (595 596) 82 | // (for variable) 602 588 (602 603) 83 | // (for variable) 625 588 (625 626) 84 | // (continue) 637 580 (632 640) 85 | // (break) 656 670 (654 659) 86 | // (method parameter) 750 706 (747 756) 87 | // (class field) 768 85 (766 771) 88 | // (local variable) 780 731 (774 787) 89 | 90 | -------------------------------------------------------------------------------- /component/platform/lib/testData/files/GTDTest.kt: -------------------------------------------------------------------------------- 1 | class Sum(val sum: Int) { 2 | val add = 4 3 | fun add(a: Int): Sum = Sum(add + a) 4 | } 5 | 6 | fun sum(a: Int, b: Int) = Sum(a + b) 7 | 8 | fun main() { 9 | val localVal = 4 10 | var localVar = 5 11 | 12 | localVar = localVal 13 | 14 | val sum1 = sum(localVal, localVar) 15 | val sum2 = Sum(4) 16 | val sum3 = sum1.add(sum2.sum + sum1.add) 17 | 18 | val op1 = Ops() 19 | val op2 = Ops() 20 | op1..op2 21 | op1[0] 22 | op1(localVal) 23 | } 24 | 25 | class Ops { 26 | operator fun rangeTo(o: Ops) {} 27 | operator fun get(i: Int) {} 28 | operator fun invoke(i: Int) {} 29 | } 30 | 31 | // (class self) 9 0 (6 9) 32 | // (class) 64 6 (63 66) 33 | // (class constructor) 70 9 (69 72) 34 | // (class val) 74 34 (73 76) 35 | // (class fun parameter) 80 54 (79 80) 36 | // (fun self sum) 92 0 (89 92) 37 | // (class constructor) 112 9 (111 114) 38 | // (fun parameter) 116 93 (115 116) 39 | // (fun parameter) 120 101 (119 120) 40 | // (fun self main) 131 0 (127 131) 41 | // (loval val self) 149 0 (144 152) 42 | // (loval val self) 152 0 (144 152) 43 | // (local var) 191 165 (183 191) 44 | // (local val) 200 144 (194 202) 45 | // (fun) 221 89 (219 222) 46 | // (local val) 226 144 (223 231) 47 | // (local var) 238 165 (233 241) 48 | // (class constructor) 259 9 (258 261) 49 | // (local sum1) 282 212 (280 284) 50 | // (class fun) 287 50 (285 288) 51 | // (local sum2) 291 251 (289 293) 52 | // (class val) 295 14 (294 297) 53 | // (local sum1) 302 212 (300 304) 54 | // (class val) 307 34 (305 308) 55 | // (local op1) 358 319 (355 358) 56 | // (op range) 359 425 (358 360) 57 | // (local op2) 360 339 (360 363) 58 | // (local op1) 371 319 (368 371) 59 | // (op get) 374 461 (373 374) 60 | // (local op1) 382 319 (379 382) 61 | // (local val) 383 144 (383 391) 62 | // (local val) 391 144 (383 391) 63 | // (op invoke) 392 493 (391 392) 64 | -------------------------------------------------------------------------------- /component/platform/lib/testData/files/IsolationTest1.java: -------------------------------------------------------------------------------- 1 | public class IsolationTest1 implements Runnable { 2 | 3 | public static void main(String[] args) { 4 | Runnable runnable = new IsolationTest 5 | } 6 | 7 | @Override 8 | public void run() {} 9 | 10 | } 11 | 12 | // caret: 141 13 | -------------------------------------------------------------------------------- /component/platform/lib/testData/files/IsolationTest2.java: -------------------------------------------------------------------------------- 1 | public class IsolationTest2 implements Runnable { 2 | 3 | public static void main(String[] args) { 4 | Runnable runnable = new IsolationTest 5 | } 6 | 7 | @Override 8 | public void run() {} 9 | 10 | } 11 | 12 | // caret: 141 13 | -------------------------------------------------------------------------------- /component/platform/lib/testData/files/LibTest.java: -------------------------------------------------------------------------------- 1 | import api.Api; 2 | 3 | public class LibTest { 4 | 5 | public static void main(String[] args) { 6 | Api api = new Class 7 | } 8 | } 9 | 10 | // caret: 113 11 | -------------------------------------------------------------------------------- /component/platform/lib/testData/files/LibTest2.java: -------------------------------------------------------------------------------- 1 | import api.Api; 2 | 3 | public class LibTest { 4 | 5 | public static void main(String[] args) { 6 | Api api = new Class 7 | } 8 | } 9 | 10 | // caret: 113 11 | -------------------------------------------------------------------------------- /component/platform/lib/testData/files/ListTest.kt: -------------------------------------------------------------------------------- 1 | import java.util.* 2 | 3 | fun main() { 4 | val list = ArrayList() 5 | list. 6 | } 7 | 8 | // caret: 77 9 | -------------------------------------------------------------------------------- /component/platform/lib/testData/files/TestAr.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | public class TestAr { 4 | 5 | public static void main(String[] args) { 6 | var a = new Ar 7 | } 8 | 9 | } 10 | 11 | // caret: 111 12 | -------------------------------------------------------------------------------- /component/platform/lib/testData/files/TestSo.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | public class TestSo { 4 | 5 | public static void main(String[] args) { 6 | var a = new So 7 | } 8 | 9 | } 10 | 11 | // caret: 111 12 | -------------------------------------------------------------------------------- /component/platform/lib/testData/files/UpdateTest.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | public class UpdateTest { 4 | 5 | public static void main(String[] args) { 6 | var a = new So 7 | } 8 | 9 | } 10 | 11 | // caret: 115 12 | -------------------------------------------------------------------------------- /component/platform/lib/testData/files/test.kt: -------------------------------------------------------------------------------- 1 | package Tests 2 | 3 | import java.util.* 4 | 5 | fun hello() { 6 | val a = So 7 | } 8 | 9 | // caret: 63 10 | -------------------------------------------------------------------------------- /component/platform/lib/testData/lib/api.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n34t0/compose-code-editor/8bc6edadd2a2b1eb20e15e265d3d6ad638b27db4/component/platform/lib/testData/lib/api.jar -------------------------------------------------------------------------------- /component/platform/lib/testData/lib/implA.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n34t0/compose-code-editor/8bc6edadd2a2b1eb20e15e265d3d6ad638b27db4/component/platform/lib/testData/lib/implA.jar -------------------------------------------------------------------------------- /component/platform/lib/testData/lib/sub/implB.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n34t0/compose-code-editor/8bc6edadd2a2b1eb20e15e265d3d6ad638b27db4/component/platform/lib/testData/lib/sub/implB.jar -------------------------------------------------------------------------------- /component/platform/lib/testData/lib/sub/implC.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n34t0/compose-code-editor/8bc6edadd2a2b1eb20e15e265d3d6ad638b27db4/component/platform/lib/testData/lib/sub/implC.jar -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/GTDJavaProject/org/example/Main.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import org.example.a.ClassA; 4 | 5 | public class Main { 6 | 7 | private static ClassA classA; 8 | 9 | public static void main(String[] args) { 10 | classA = new ClassA(); 11 | classA.set(5); 12 | System.out.println(classA.field); 13 | } 14 | } 15 | 16 | // (class type) 94 (ClassA.java) 37 17 | // (local field) 165 (Main.java) 99 18 | // (constructor) 177 (ClassA.java) 58 19 | // (local field) 195 (Main.java) 99 20 | // (method) 201 (ClassA.java) 166 21 | // (local field) 240 (Main.java) 99 22 | // (field) 244 (ClassA.java) 86 23 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/GTDJavaProject/org/example/a/ClassA.java: -------------------------------------------------------------------------------- 1 | package org.example.a; 2 | 3 | public class ClassA { 4 | 5 | public ClassA() {} 6 | 7 | public int field = 5; 8 | 9 | public int get() { 10 | return field; 11 | } 12 | 13 | public void set(int i) { 14 | field = i; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/GTDKotlinProject/org/example/a/Sum.kt: -------------------------------------------------------------------------------- 1 | class Sum(val sum: Int) { 2 | val add = 4 3 | fun add(a: Int, b: (Int) -> Int): Sum = Sum(add + b(a)) 4 | } 5 | 6 | fun sum(a: Int, b: Int) = Sum(a + b) 7 | 8 | val d: (Int) -> Int = { b -> 2 * b } 9 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/GTDKotlinProject/org/example/main.kt: -------------------------------------------------------------------------------- 1 | package org.example 2 | 3 | import Sum 4 | import d 5 | import sum 6 | 7 | fun main() { 8 | val sum1 = sum(1, 2) 9 | val sum2: Sum = Sum(4) 10 | val sum3 = sum1.add(sum2.sum + sum1.add) { b -> 11 | d(b) 12 | } 13 | } 14 | 15 | // (import Sum) 29 (Sum.kt) 6 16 | // (import d) 39 (Sum.kt) 147 17 | // (import sum) 49 (Sum.kt) 109 18 | // (local val self sum1) 75 () 0 19 | // (fun sum) 83 (Sum.kt) 109 20 | // (class) 106 (Sum.kt) 6 21 | // (class constructor) 112 (Sum.kt) 9 22 | // (local val sum1) 135 (main.kt) 74 23 | // (class fun add) 140 (Sum.kt) 50 24 | // (local val sum2) 145 (main.kt) 99 25 | // (class val sum) 148 (Sum.kt) 14 26 | // (local val sum1) 155 (main.kt) 74 27 | // (class val add) 160 (Sum.kt) 34 28 | // (fun d) 178 (Sum.kt) 147 29 | // (closure parameter b) 180 (main.kt) 165 30 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/fileOperationsProject/org/example/Main.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import org.example.a.*; 4 | 5 | public class Main { 6 | 7 | public static void main(String[] args) { 8 | Api api = new Impl 9 | } 10 | } 11 | 12 | // caret: 139 13 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/fileOperationsProject/org/example/a/Api.java: -------------------------------------------------------------------------------- 1 | package org.example.a; 2 | 3 | public interface Api { 4 | 5 | void calc(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/fileOperationsProject/org/example/a/ImplA.java: -------------------------------------------------------------------------------- 1 | package org.example.a; 2 | 3 | public class ImplA implements Api { 4 | 5 | public void calc() { 6 | System.out.println("ok"); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/isolationLibProject1/org/example/Impl1.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import api.Api; 4 | 5 | public class Impl1 implements Api { 6 | 7 | @Override 8 | public boolean getB() { 9 | return false; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/isolationLibProject1/org/example/Main.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import api.Api; 4 | 5 | public class Main { 6 | 7 | public static void main(String[] args) { 8 | Api api = new Impl 9 | } 10 | } 11 | 12 | // caret: 131 13 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/isolationLibProject2/org/example/Impl2.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import api.Api; 4 | 5 | public class Impl2 implements Api { 6 | 7 | @Override 8 | public boolean getB() { 9 | return false; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/isolationLibProject2/org/example/Main.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import api.Api; 4 | 5 | public class Main { 6 | 7 | public static void main(String[] args) { 8 | Api api = new Impl 9 | } 10 | } 11 | 12 | // caret: 131 13 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/isolationProject1/org/example/IsoClass1.java: -------------------------------------------------------------------------------- 1 | public class IsoClass1 implements Runnable { 2 | 3 | @Override 4 | public void run() {} 5 | 6 | } 7 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/isolationProject1/org/example/Main.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | public class Main { 4 | 5 | public static void main(String[] args) { 6 | Runnable runnable = new IsoClass 7 | } 8 | } 9 | 10 | // caret: 128 11 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/isolationProject2/org/example/IsoClass2.java: -------------------------------------------------------------------------------- 1 | public class IsoClass2 implements Runnable { 2 | 3 | @Override 4 | public void run() {} 5 | 6 | } 7 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/isolationProject2/org/example/Main.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | public class Main { 4 | 5 | public static void main(String[] args) { 6 | Runnable runnable = new IsoClass 7 | } 8 | } 9 | 10 | // caret: 128 11 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/javaProject/org/example/Main.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import org.example.a.ClassA; 4 | 5 | public class Main { 6 | 7 | public static void main(String[] args) { 8 | ClassA classA = new ClassA(); 9 | classA.g 10 | } 11 | } 12 | 13 | // caret: 172 14 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/javaProject/org/example/a/ClassA.java: -------------------------------------------------------------------------------- 1 | package org.example.a; 2 | 3 | public class ClassA { 4 | 5 | private int i = 5; 6 | 7 | public int getI() { 8 | return i; 9 | } 10 | 11 | public void setI(int i) { 12 | this.i = i; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/libJavaProject/org/example/ClassD.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import api.Api; 4 | 5 | public class ClassD implements Api { 6 | 7 | @Override 8 | public boolean getB() { 9 | return false; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/libJavaProject/org/example/Main.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import api.Api; 4 | 5 | public class Main { 6 | 7 | public static void main(String[] args) { 8 | Api api = new Class 9 | } 10 | } 11 | 12 | // caret: 132 13 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/libJavaProject2/org/example/ClassD.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import api.Api; 4 | 5 | public class ClassD implements Api { 6 | 7 | @Override 8 | public boolean getB() { 9 | return false; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/libJavaProject2/org/example/Main.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import api.Api; 4 | 5 | public class Main { 6 | 7 | public static void main(String[] args) { 8 | Api api = new Class 9 | } 10 | } 11 | 12 | // caret: 132 13 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/simpleJavaProject/Test.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | public class Test { 4 | 5 | public static void main(String[] args) { 6 | var a = new Ar 7 | } 8 | 9 | } 10 | 11 | // caret: 109 12 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/simpleKotlinProject/test.kt: -------------------------------------------------------------------------------- 1 | package Tests 2 | 3 | import java.util.* 4 | 5 | fun hello() { 6 | val a = So 7 | } 8 | 9 | // caret: 63 10 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/updateJavaProject/org/example/Main.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import org.example.a.ClassA; 4 | 5 | public class Main { 6 | 7 | public static void main(String[] args) { 8 | ClassA classA = new ClassA(); 9 | classA.g 10 | } 11 | } 12 | 13 | // caret: 172 14 | -------------------------------------------------------------------------------- /component/platform/lib/testData/projects/updateJavaProject/org/example/a/ClassA.java: -------------------------------------------------------------------------------- 1 | package org.example.a; 2 | 3 | public class ClassA { 4 | 5 | private int i = 5; 6 | private int j = 6; 7 | 8 | public int getJ() { 9 | return j; 10 | } 11 | 12 | public void setI(int i) { 13 | this.i = i; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /component/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 5 | } 6 | } 7 | 8 | include(":platform:api") 9 | include(":platform:lib") 10 | include(":editor") 11 | -------------------------------------------------------------------------------- /demo/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.compose 2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 3 | 4 | plugins { 5 | kotlin("jvm") version "1.5.30" 6 | id("org.jetbrains.compose") version "1.0.0-alpha4-build331" 7 | } 8 | 9 | repositories { 10 | mavenLocal() 11 | mavenCentral() 12 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 13 | google() 14 | } 15 | 16 | dependencies { 17 | implementation(compose.desktop.currentOs) 18 | implementation(compose.materialIconsExtended) 19 | implementation("io.github.n34t0.compose:code-editor:0.1.0") 20 | } 21 | 22 | tasks.withType() { 23 | kotlinOptions.jvmTarget = "11" 24 | } 25 | 26 | compose.desktop { 27 | application { 28 | mainClass = "io.github.n34t0.compose.codeEditor.demo.MainKt" 29 | jvmArgs("-Xmx2048m") 30 | // jvmArgs("-Xmx2048m", "-Dplatform.stub=true") 31 | 32 | nativeDistributions { 33 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 34 | packageName = "sample" 35 | packageVersion = "1.0.0" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /demo/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 2 | kotlin.code.style=official -------------------------------------------------------------------------------- /demo/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n34t0/compose-code-editor/8bc6edadd2a2b1eb20e15e265d3d6ad638b27db4/demo/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /demo/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /demo/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 | -------------------------------------------------------------------------------- /demo/jars/kotlin-stdlib.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n34t0/compose-code-editor/8bc6edadd2a2b1eb20e15e265d3d6ad638b27db4/demo/jars/kotlin-stdlib.jar -------------------------------------------------------------------------------- /demo/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 5 | } 6 | } -------------------------------------------------------------------------------- /demo/src/main/kotlin/io/github/n34t0/compose/codeEditor/demo/diagnostics.kt: -------------------------------------------------------------------------------- 1 | package io.github.n34t0.compose.codeEditor.demo 2 | 3 | import io.github.n34t0.compose.codeEditor.diagnostics.DiagnosticElement 4 | import io.github.n34t0.compose.codeEditor.diagnostics.Severity 5 | import java.util.regex.Pattern 6 | 7 | private val diagnosticsLinePatterns: List> = listOf( 8 | Triple(Pattern.compile("\\bfun (foo)\\(\\)"), Severity.INFO, "Function \"foo\" is never used"), 9 | Triple(Pattern.compile("\\bval (str)\\b"), Severity.INFO, "Variable \"str\" is never used"), 10 | Triple(Pattern.compile("\\b(vl)\\b"), Severity.ERROR, "Unresolved reference: vl"), 11 | Triple(Pattern.compile("\\b(list)\\b"), Severity.ERROR, "Unresolved reference: list"), 12 | Triple(Pattern.compile("\\bif \\((true)\\)"), Severity.INFO, "Condition is always \"true\""), 13 | ) 14 | 15 | private val unusedLambdaPattern = Pattern.compile("\\{\\s*val str = \"[^\"]*\"\\s*}", Pattern.MULTILINE) 16 | 17 | fun updateDiagnostics(code: String, list: MutableList) { 18 | list.clear() 19 | code.lineSequence().forEachIndexed { index, line -> 20 | diagnosticsLinePatterns.forEach { pattern -> 21 | pattern.first.matcher(line).results().forEach { result -> 22 | list.add(DiagnosticElement( 23 | startLine = index + 1, 24 | startCharacter = result.start(1), 25 | endLine = index + 1, 26 | endCharacter = result.end(1), 27 | message = pattern.third, 28 | severity = pattern.second 29 | )) 30 | } 31 | } 32 | } 33 | val matcher = unusedLambdaPattern.matcher(code) 34 | if (matcher.find()) { 35 | matcher.toMatchResult()?.let { 36 | val start = it.start() 37 | val end = it.end() 38 | val subSequenceBefore = code.subSequence(0, start) 39 | val startLine = subSequenceBefore.count { it == '\n' } + 1 40 | val endLine = startLine + code.subSequence(start, end).count { it == '\n' } 41 | var lastIndexOf = subSequenceBefore.lastIndexOf('\n', start) 42 | val startCharacter = if (lastIndexOf != -1) start - lastIndexOf - 1 else start 43 | lastIndexOf = code.subSequence(0, end).lastIndexOf('\n', end) 44 | val endCharacter = if (lastIndexOf != -1) end - lastIndexOf - 1 else end 45 | list.add(DiagnosticElement( 46 | startLine = startLine, 47 | startCharacter = startCharacter, 48 | endLine = endLine, 49 | endCharacter = endCharacter, 50 | message = "The lambda expression is unused. If you mean a block, you can use 'run { ... }'", 51 | severity = Severity.WARNING 52 | )) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /screenshots/codeEditor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n34t0/compose-code-editor/8bc6edadd2a2b1eb20e15e265d3d6ad638b27db4/screenshots/codeEditor.gif --------------------------------------------------------------------------------