[space] Extract Space plugin sources from Community

GitOrigin-RevId: e872d745c76e5ebcb66cdd9ac0cf228bdead4998
This commit is contained in:
Nikolay Rykunov
2021-03-19 18:04:40 +00:00
committed by intellij-monorepo-bot
parent eb4c713239
commit a2ddafffd7
221 changed files with 1 additions and 16213 deletions
-5
View File
@@ -51,11 +51,6 @@
<option name="name" value="36870030-7a21-4052-b93a-010aa4c33366" />
<option name="url" value="https://cache-redirector.jetbrains.com/download.jetbrains.com/teamcity-repository" />
</remote-repository>
<remote-repository>
<option name="id" value="JetBrainsSpacePublicMaven" />
<option name="name" value="JetBrains Space Public Maven" />
<option name="url" value="https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/public/p/space/maven" />
</remote-repository>
<remote-repository>
<option name="id" value="caeb020e-2153-45d1-b752-f56d66968d08" />
<option name="name" value="caeb020e-2153-45d1-b752-f56d66968d08" />
-17
View File
@@ -1,17 +0,0 @@
<component name="libraryTable">
<library name="jackson-datatype-joda" type="repository">
<properties maven-id="com.fasterxml.jackson.datatype:jackson-datatype-joda:2.12.2">
<exclude>
<dependency maven-id="com.fasterxml.jackson.core:jackson-annotations" />
<dependency maven-id="com.fasterxml.jackson.core:jackson-core" />
<dependency maven-id="com.fasterxml.jackson.core:jackson-databind" />
</exclude>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/datatype/jackson-datatype-joda/2.12.2/jackson-datatype-joda-2.12.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/joda-time/joda-time/2.10.8/joda-time-2.10.8.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
-33
View File
@@ -1,33 +0,0 @@
<component name="libraryTable">
<library name="ktor-websockets" type="repository">
<properties maven-id="io.ktor:ktor-websockets:1.5.0">
<exclude>
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-core-common" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-core" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-jdk8" />
<dependency maven-id="org.slf4j:slf4j-api" />
<dependency maven-id="org.jetbrains:annotations" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-reflect" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-common" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk7" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib" />
<dependency maven-id="com.typesafe:config" />
<dependency maven-id="io.ktor:ktor-http-cio-jvm" />
<dependency maven-id="io.ktor:ktor-http-jvm" />
<dependency maven-id="io.ktor:ktor-io-jvm" />
<dependency maven-id="io.ktor:ktor-network" />
<dependency maven-id="io.ktor:ktor-server-core" />
<dependency maven-id="io.ktor:ktor-utils-jvm" />
<dependency maven-id="org.jetbrains.kotlinx:atomicfu" />
</exclude>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-websockets/1.5.0/ktor-websockets-1.5.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-websockets/1.5.0/ktor-websockets-1.5.0-sources.jar!/" />
</SOURCES>
</library>
</component>
-43
View File
@@ -1,43 +0,0 @@
<component name="libraryTable">
<library name="space-idea-sdk" type="repository">
<properties maven-id="com.jetbrains:space-idea-sdk:1.1.66161-pre">
<exclude>
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-reflect" />
<dependency maven-id="com.fasterxml.jackson.core:jackson-core" />
<dependency maven-id="com.fasterxml.jackson.core:jackson-databind" />
<dependency maven-id="com.fasterxml.jackson.dataformat:jackson-dataformat-xml" />
<dependency maven-id="com.fasterxml.jackson.module:jackson-module-kotlin" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-core" />
<dependency maven-id="org.jetbrains:kotlin-css" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-common" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk7" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-slf4j" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-io-jvm" />
<dependency maven-id="commons-io:commons-io" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-jdk8" />
<dependency maven-id="com.squareup.okhttp3:okhttp" />
<dependency maven-id="io.ktor:ktor-client-core" />
<dependency maven-id="io.ktor:ktor-client-apache" />
<dependency maven-id="io.ktor:ktor-http-jvm" />
<dependency maven-id="joda-time:joda-time" />
<dependency maven-id="org.reflections:reflections" />
<dependency maven-id="ch.qos.logback:logback-classic" />
<dependency maven-id="org.apache.logging.log4j:log4j-to-slf4j" />
<dependency maven-id="com.fasterxml.jackson.datatype:jackson-datatype-joda" />
<dependency maven-id="com.google.guava:guava" />
<dependency maven-id="org.slf4j:jul-to-slf4j" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8" />
<dependency maven-id="org.apache.ivy:ivy" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-scripting-dependencies" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-scripting-common" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-scripting-jvm" />
</exclude>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/jetbrains/space-idea-sdk/1.1.66161-pre/space-idea-sdk-1.1.66161-pre.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
-2
View File
@@ -530,7 +530,6 @@
<module fileurl="file://$PROJECT_DIR$/community-guitests/intellij.idea.community.guiTests.iml" filepath="$PROJECT_DIR$/community-guitests/intellij.idea.community.guiTests.iml" />
<module fileurl="file://$PROJECT_DIR$/intellij.idea.community.main.iml" filepath="$PROJECT_DIR$/intellij.idea.community.main.iml" />
<module fileurl="file://$PROJECT_DIR$/intellij.idea.community.main.android.iml" filepath="$PROJECT_DIR$/intellij.idea.community.main.android.iml" />
<module fileurl="file://$PROJECT_DIR$/intellij.idea.community.main.space.iml" filepath="$PROJECT_DIR$/intellij.idea.community.main.space.iml" />
<module fileurl="file://$PROJECT_DIR$/community-resources/intellij.idea.community.resources.iml" filepath="$PROJECT_DIR$/community-resources/intellij.idea.community.resources.iml" />
<module fileurl="file://$PROJECT_DIR$/build/launch/intellij.idea.tools.launch.iml" filepath="$PROJECT_DIR$/build/launch/intellij.idea.tools.launch.iml" />
<module fileurl="file://$PROJECT_DIR$/java/openapi/intellij.java.iml" filepath="$PROJECT_DIR$/java/openapi/intellij.java.iml" />
@@ -819,7 +818,6 @@
<module fileurl="file://$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.iml" filepath="$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.tests.iml" filepath="$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/sh/intellij.sh.iml" filepath="$PROJECT_DIR$/plugins/sh/intellij.sh.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/space/intellij.space.iml" filepath="$PROJECT_DIR$/plugins/space/intellij.space.iml" />
<module fileurl="file://$PROJECT_DIR$/spellchecker/intellij.spellchecker.iml" filepath="$PROJECT_DIR$/spellchecker/intellij.spellchecker.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/stats-collector/intellij.statsCollector.iml" filepath="$PROJECT_DIR$/plugins/stats-collector/intellij.statsCollector.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/stats-collector/intellij.statsCollector.tests.iml" filepath="$PROJECT_DIR$/plugins/stats-collector/intellij.statsCollector.tests.iml" />
@@ -1,4 +1,4 @@
// 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.
// 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.
package org.jetbrains.intellij.build
import groovy.transform.CompileStatic
@@ -85,7 +85,6 @@ abstract class BaseIdeaProperties extends JetBrainsProductProperties {
"intellij.webp",
"intellij.grazie",
"intellij.featuresTrainer",
"intellij.space",
"intellij.lombok"
]
-1
View File
@@ -142,7 +142,6 @@
<orderEntry type="module" module-name="intellij.filePrediction" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.textmate" scope="TEST" />
<orderEntry type="module" module-name="intellij.webp" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.space" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.packageSearch" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.packageSearch.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.emojipicker" scope="RUNTIME" />
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module relativePaths="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.idea.community.main" scope="RUNTIME" />
<orderEntry type="library" scope="RUNTIME" name="KotlinPlugin" level="project" />
</component>
</module>
@@ -336,9 +336,6 @@ class CommunityLibraryLicenses {
new LibraryLicense(name: "Jackson", libraryName: "jackson", url: "https://github.com/FasterXML/jackson").apache(),
new LibraryLicense(name: "Jackson Databind", libraryName: "jackson-databind",
url: "https://github.com/FasterXML/jackson-databind").apache(),
new LibraryLicense(name: "Jackson Datatype Joda",
libraryName: "jackson-datatype-joda",
url: "https://github.com/FasterXML/jackson-datatype-joda").apache(),
new LibraryLicense(name: "Jackson Module Kotlin", libraryName: "jackson-module-kotlin",
url: "https://github.com/FasterXML/jackson-module-kotlin").apache(),
new LibraryLicense(name: "Jakarta ORO", libraryName: "OroMatcher",
@@ -449,15 +446,6 @@ class CommunityLibraryLicenses {
new LibraryLicense(name: "Kryo", libraryName: "Kryo",
url: "https://github.com/EsotericSoftware/kryo",
licenseUrl: "https://github.com/EsotericSoftware/kryo/blob/master/LICENSE.md").newBsd(),
new LibraryLicense(name: "ktor-client-apache",
libraryName: "space-ktor-client-apache",
url: "https://github.com/ktorio/ktor").apache(),
new LibraryLicense(name: "ktor-server-netty",
libraryName: "space-ktor-server-netty",
url: "https://github.com/ktorio/ktor").apache(),
new LibraryLicense(name: "ktor-websockets",
libraryName: "ktor-websockets",
url: "https://github.com/ktorio/ktor").apache(),
new LibraryLicense(name: "kXML2", libraryName: "kxml2", license: "BSD", url: "http://sourceforge.net/projects/kxml/"),
new LibraryLicense(name: "Language Tool", libraryName: "org.languagetool:languagetool-core:5.2",
license: "LGPL 2.1",
@@ -693,7 +681,6 @@ class CommunityLibraryLicenses {
jetbrainsLibrary("rd-framework"),
jetbrainsLibrary("rd-swing"),
jetbrainsLibrary("rd-text"),
jetbrainsLibrary("space-idea-sdk"),
jetbrainsLibrary("tcServiceMessages"),
jetbrainsLibrary("tips-idea-ce"),
jetbrainsLibrary("tips-pycharm-community"),
@@ -268,10 +268,6 @@ final class CommunityRepositoryModules {
bundlingRestrictions.includeInEapOnly = true
},
plugin("intellij.jps.cache"),
plugin("intellij.space") {
withProjectLibrary("space-idea-sdk")
withProjectLibrary("jackson-datatype-joda")
},
plugin("intellij.lombok") {
withModule("intellij.lombok.generated")
},
-235
View File
@@ -1,235 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/kotlin" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/kotlin" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.vcs.git" />
<orderEntry type="module" module-name="intellij.platform.vcs" />
<orderEntry type="module" module-name="intellij.platform.vcs.impl" />
<orderEntry type="module" module-name="intellij.platform.vcs.log" />
<orderEntry type="module" module-name="intellij.platform.vcs.log.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.lang" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.platform.externalSystem" />
<orderEntry type="module" module-name="intellij.platform.execution.impl" />
<orderEntry type="module" module-name="intellij.platform.ide" />
<orderEntry type="module" module-name="intellij.platform.diff.impl" />
<orderEntry type="module" module-name="intellij.platform.statistics" />
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
<orderEntry type="library" name="Guava" level="project" />
<orderEntry type="library" name="kotlin-stdlib-jdk8" level="project" />
<orderEntry type="library" name="kotlinx-coroutines-jdk8" level="project" />
<orderEntry type="library" name="kotlin-reflect" level="project" />
<orderEntry type="library" scope="TEST" name="kotlin-test" level="project" />
<orderEntry type="library" name="miglayout-swing" level="project" />
<orderEntry type="module-library">
<library name="kotlinx-coroutines-slf4j" type="repository">
<properties include-transitive-deps="false" maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.3.4" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-slf4j/1.3.4/kotlinx-coroutines-slf4j-1.3.4.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-slf4j/1.3.4/kotlinx-coroutines-slf4j-1.3.4-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-slf4j/1.3.4/kotlinx-coroutines-slf4j-1.3.4-sources.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="library" name="ktor-websockets" level="project" />
<orderEntry type="module-library">
<library name="space-ktor-client-apache" type="repository">
<properties maven-id="io.ktor:ktor-client-apache:1.5.2">
<exclude>
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib" />
<dependency maven-id="org.slf4j:slf4j-api" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm" />
<dependency maven-id="io.ktor:ktor-http-jvm" />
<dependency maven-id="io.ktor:ktor-http-cio-jvm" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-common" />
<dependency maven-id="org.apache.httpcomponents:httpclient" />
<dependency maven-id="org.apache.httpcomponents:httpcore" />
<dependency maven-id="commons-logging:commons-logging" />
</exclude>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-client-apache/1.5.2/ktor-client-apache-1.5.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-client-core-jvm/1.5.2/ktor-client-core-jvm-1.5.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/apache/httpcomponents/httpasyncclient/4.1.4/httpasyncclient-4.1.4.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/apache/httpcomponents/httpcore-nio/4.4.13/httpcore-nio-4.4.13.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-client-apache/1.5.2/ktor-client-apache-1.5.2-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-client-core-jvm/1.5.2/ktor-client-core-jvm-1.5.2-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/apache/httpcomponents/httpasyncclient/4.1.4/httpasyncclient-4.1.4-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/apache/httpcomponents/httpcore-nio/4.4.13/httpcore-nio-4.4.13-sources.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="module-library" scope="PROVIDED">
<library name="kotlin-scripting" type="repository">
<properties maven-id="org.jetbrains.kotlin:scripting:212-1.4.21-release-IJ22">
<exclude>
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8" />
</exclude>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/scripting/212-1.4.21-release-IJ22/scripting-212-1.4.21-release-IJ22.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-scripting-common/212-1.4.21-release-IJ22/kotlin-scripting-common-212-1.4.21-release-IJ22.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library" scope="PROVIDED">
<library name="kotlin-scripting-common" type="repository">
<properties maven-id="org.jetbrains.kotlin:kotlin-scripting-common:1.4.21" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-scripting-common/1.4.21/kotlin-scripting-common-1.4.21.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.4.21/kotlin-stdlib-1.4.21.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.4.21/kotlin-stdlib-common-1.4.21.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.3.7/kotlinx-coroutines-core-1.3.7.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="library" exported="" name="space-idea-sdk" level="project" />
<orderEntry type="library" scope="RUNTIME" name="jackson" level="project" />
<orderEntry type="library" scope="RUNTIME" name="jackson-databind" level="project" />
<orderEntry type="library" scope="RUNTIME" name="jackson-datatype-joda" level="project" />
<orderEntry type="module-library" scope="RUNTIME">
<library name="okhttp3" type="repository">
<properties maven-id="com.squareup.okhttp3:okhttp:3.12.10" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/squareup/okhttp3/okhttp/3.12.10/okhttp-3.12.10.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/squareup/okio/okio/1.15.0/okio-1.15.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/squareup/okhttp3/okhttp/3.12.10/okhttp-3.12.10-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/squareup/okio/okio/1.15.0/okio-1.15.0-sources.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="library" name="Slf4j" level="project" />
<orderEntry type="module" module-name="intellij.platform.vcs.codeReview" />
<orderEntry type="module-library">
<library name="org.apache.ivy" type="repository">
<properties maven-id="org.apache.ivy:ivy:2.5.0" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/ivy/ivy/2.5.0/ivy-2.5.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/ivy/ivy/2.5.0/ivy-2.5.0-sources.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.util.ui" />
<orderEntry type="library" name="intellij-markdown" level="project" />
<orderEntry type="module-library">
<library name="kotlin-scripting-dependencies" type="repository">
<properties maven-id="org.jetbrains.kotlin:kotlin-scripting-dependencies:1.4.21">
<exclude>
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-core" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-common" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-scripting-common" />
<dependency maven-id="org.jetbrains:annotations" />
</exclude>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-scripting-dependencies/1.4.21/kotlin-scripting-dependencies-1.4.21.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library" scope="PROVIDED">
<library name="kotlin-scripting-jvm" type="repository">
<properties maven-id="org.jetbrains.kotlin:kotlin-scripting-jvm:1.4.21">
<exclude>
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-core" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-common" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-script-runtime" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-scripting-common" />
<dependency maven-id="org.jetbrains:annotations" />
</exclude>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-scripting-jvm/1.4.21/kotlin-scripting-jvm-1.4.21.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-scripting-jvm/1.4.21/kotlin-scripting-jvm-1.4.21-sources.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="module-library">
<library name="space-ktor-server-netty" type="repository">
<properties maven-id="io.ktor:ktor-server-netty:1.5.2">
<exclude>
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk7" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8" />
<dependency maven-id="org.slf4j:slf4j-api" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-jdk8" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-reflect" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm" />
<dependency maven-id="io.netty:netty-codec-http2" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-common" />
<dependency maven-id="io.netty:netty-buffer" />
<dependency maven-id="io.netty:netty-common" />
<dependency maven-id="io.netty:netty-resolver" />
<dependency maven-id="io.netty:netty-transport" />
</exclude>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-server-netty/1.5.2/ktor-server-netty-1.5.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-server-host-common/1.5.2/ktor-server-host-common-1.5.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-server-core/1.5.2/ktor-server-core-1.5.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-utils-jvm/1.5.2/ktor-utils-jvm-1.5.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-io-jvm/1.5.2/ktor-io-jvm-1.5.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-http-jvm/1.5.2/ktor-http-jvm-1.5.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/typesafe/config/1.3.1/config-1.3.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-http-cio-jvm/1.5.2/ktor-http-cio-jvm-1.5.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-network-jvm/1.5.2/ktor-network-jvm-1.5.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/alpn/alpn-api/1.1.3.v20160715/alpn-api-1.1.3.v20160715.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-transport-native-kqueue/4.1.54.Final/netty-transport-native-kqueue-4.1.54.Final.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-transport-native-unix-common/4.1.54.Final/netty-transport-native-unix-common-4.1.54.Final.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-transport-native-epoll/4.1.54.Final/netty-transport-native-epoll-4.1.54.Final.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-server-netty/1.5.2/ktor-server-netty-1.5.2-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-server-host-common/1.5.2/ktor-server-host-common-1.5.2-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-server-core/1.5.2/ktor-server-core-1.5.2-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-utils-jvm/1.5.2/ktor-utils-jvm-1.5.2-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-io-jvm/1.5.2/ktor-io-jvm-1.5.2-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-http-jvm/1.5.2/ktor-http-jvm-1.5.2-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/typesafe/config/1.3.1/config-1.3.1-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-http-cio-jvm/1.5.2/ktor-http-cio-jvm-1.5.2-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/ktor/ktor-network-jvm/1.5.2/ktor-network-jvm-1.5.2-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/alpn/alpn-api/1.1.3.v20160715/alpn-api-1.1.3.v20160715-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-transport-native-kqueue/4.1.54.Final/netty-transport-native-kqueue-4.1.54.Final-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-transport-native-unix-common/4.1.54.Final/netty-transport-native-unix-common-4.1.54.Final-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-transport-native-epoll/4.1.54.Final/netty-transport-native-epoll-4.1.54.Final-sources.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="library" name="gson" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide.util.io" />
</component>
</module>
@@ -1,8 +0,0 @@
// 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.
package com.intellij.space
import circlet.platform.client.KCircletClient
interface SpaceVmWithClient {
val client: KCircletClient
}
@@ -1,12 +0,0 @@
// 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.
package com.intellij.space.actions
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnActionEvent
import icons.SpaceIcons
object SpaceActionUtils {
fun showIconInActionSearch(e: AnActionEvent) {
e.presentation.icon = if (e.place == ActionPlaces.ACTION_SEARCH) SpaceIcons.Main else null
}
}
@@ -1,18 +0,0 @@
// 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.
package com.intellij.space.actions
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.space.components.SpaceWorkspaceComponent
import com.intellij.space.settings.SpaceSettingsPanel
class SpaceLoginAction : DumbAwareAction() {
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = SpaceWorkspaceComponent.getInstance().workspace.value == null
SpaceActionUtils.showIconInActionSearch(e)
}
override fun actionPerformed(e: AnActionEvent) {
SpaceSettingsPanel.openSettings(e.project)
}
}
@@ -1,116 +0,0 @@
// 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.
package com.intellij.space.actions
import com.intellij.ide.BrowserUtil
import com.intellij.ide.ui.fullRow
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.SystemInfo
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.promo.*
import com.intellij.space.settings.SpaceLoginState
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.space.ui.LoginComponents.errorText
import com.intellij.space.ui.LoginComponents.loginButton
import com.intellij.space.ui.LoginComponents.organizationUrlTextField
import com.intellij.ui.ComponentUtil
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.BrowserLink
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.panels.Wrapper
import com.intellij.ui.layout.*
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import javax.swing.JButton
import javax.swing.JComponent
internal fun prettyBorder() = JBUI.Borders.empty(16, 20, 4, 20)
internal fun buildLoginPanelWithPromo(
state: SpaceLoginState.Disconnected,
statsExplorePlace: SpaceStatsCounterCollector.ExplorePlace,
statsLoginPlace: SpaceStatsCounterCollector.LoginPlace,
packParent: () -> Unit,
loginAction: (String) -> Unit
): JComponent {
val wrapper = Wrapper().apply {
val loginPanel = loginPanel(statsLoginPlace, state, loginAction).apply {
border = prettyBorder()
}
val promoPanel = promoPanel(statsExplorePlace) {
SpaceStatsCounterCollector.ADV_LOG_IN_LINK.log()
setContent(loginPanel)
packParent()
repaint()
}.apply {
border = prettyBorder()
}
val content = if (state.server.isNotBlank() || state.error?.isNotBlank() == true) loginPanel else promoPanel
setContent(content)
repaint()
}
val borderLayoutPanel = BorderLayoutPanel().addToCenter(wrapper)
toolbarPromoBanner()?.let { borderLayoutPanel.addToTop(it) }
return borderLayoutPanel
}
private fun promoPanel(statsExplorePlace: SpaceStatsCounterCollector.ExplorePlace, loginAction: () -> Unit) = panel {
fullRow { createSpaceByJetbrainsLabel()() }
fullRow { promoText(if (SystemInfo.isMac) 60 else 52)() }
fullRow {
JButton(SpaceBundle.message("space.promo.explore.space.button")).apply {
addActionListener {
SpaceStatsCounterCollector.EXPLORE_SPACE.log(statsExplorePlace)
BrowserUtil.browse(EXPLORE_SPACE_PROMO_URL)
}
}.apply {
ComponentUtil.putClientProperty(this, DarculaButtonUI.DEFAULT_STYLE_KEY, true)
}()
}
fullRow {
grayTextLabel(SpaceBundle.message("login.already.have.account.label"))()
loginLabel(loginAction)()
}
}
private fun loginPanel(
statsLoginPlace: SpaceStatsCounterCollector.LoginPlace,
state: SpaceLoginState.Disconnected,
loginAction: (String) -> Unit
) = panel {
val serverField = organizationUrlTextField(state.server)
val loginButton = loginButton(statsLoginPlace, default = true) {
it.isEnabled = false
loginAction(serverField.text.trim())
}
fullRow { label(SpaceBundle.message("login.panel.organization.url.label")) }
fullRow { serverField() }
fullRow { loginButton() }
state.error?.let {
val errorText = errorText(it)
fullRow { errorText() }
}
fullRow {
grayTextLabel(SpaceBundle.message("login.dont.have.account.label"))()
singUpLabel()()
}
}
private fun grayTextLabel(@NlsContexts.Label text: String) = JBLabel(text).apply {
foreground = SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES.fgColor
}
private fun loginLabel(loginAction: () -> Unit) = ActionLink(SpaceBundle.message("login.panel.log.in.button")) {
loginAction()
}
private fun singUpLabel(): BrowserLink = BrowserLink(SpaceBundle.message("login.sign.up"), SIGN_UP_SPACE_URL).apply {
addActionListener {
SpaceStatsCounterCollector.SIGN_UP_LINK.log()
}
}
@@ -1,18 +0,0 @@
// 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.
package com.intellij.space.actions
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.space.components.SpaceWorkspaceComponent
import com.intellij.space.stats.SpaceStatsCounterCollector
class SpaceLogoutAction : DumbAwareAction() {
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = SpaceWorkspaceComponent.getInstance().workspace.value != null
SpaceActionUtils.showIconInActionSearch(e)
}
override fun actionPerformed(e: AnActionEvent) {
SpaceWorkspaceComponent.getInstance().signOut(SpaceStatsCounterCollector.LogoutPlace.ACTION)
}
}
@@ -1,157 +0,0 @@
// 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.
package com.intellij.space.actions
import circlet.client.api.englishFullName
import circlet.platform.client.ConnectionStatus
import circlet.workspaces.Workspace
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.RightAlignedToolbarAction
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.space.components.SpaceUserAvatarProvider
import com.intellij.space.components.SpaceWorkspaceComponent
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.settings.SpaceLoginState
import com.intellij.space.settings.SpaceSettings
import com.intellij.space.settings.SpaceSettingsPanel
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.space.ui.*
import com.intellij.space.ui.LoginComponents.buildConnectingPanel
import com.intellij.space.utils.SpaceUrls
import com.intellij.space.vcs.SpaceProjectContext
import com.intellij.space.vcs.clone.SpaceCloneAction
import com.intellij.space.vcs.review.SpaceShowReviewsAction
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.components.panels.Wrapper
import com.intellij.util.ui.cloneDialog.VcsCloneDialogUiSpec
import libraries.coroutines.extra.Lifetime
import runtime.reactive.view
import java.awt.Component
import java.awt.Point
import javax.swing.Icon
import javax.swing.JComponent
class SpaceMainToolBarAction : DumbAwareAction(), RightAlignedToolbarAction {
private val settings = SpaceSettings.getInstance()
override fun update(e: AnActionEvent) {
val isOnNavBar = e.place == ActionPlaces.NAVIGATION_BAR_TOOLBAR
val isOnMainBar = e.place == ActionPlaces.MAIN_TOOLBAR
if (!isOnNavBar && !isOnMainBar) {
e.presentation.isEnabledAndVisible = false
return
}
e.presentation.isEnabledAndVisible = true
val space = SpaceWorkspaceComponent.getInstance()
val avatars = SpaceUserAvatarProvider.getInstance().avatars.value
val isOnline = space.workspace.value?.client?.connectionStatus?.value is ConnectionStatus.Connected
val isConnecting = space.loginState.value is SpaceLoginState.Connecting
e.presentation.icon = when {
isOnline -> avatars.online
isConnecting -> AnimatedIcon.Default.INSTANCE
else -> avatars.offline
}
}
override fun actionPerformed(e: AnActionEvent) {
val component = e.inputEvent.component
val space = SpaceWorkspaceComponent.getInstance()
val workspace = space.workspace.value
SpaceStatsCounterCollector.OPEN_MAIN_TOOLBAR_POPUP.log(SpaceStatsCounterCollector.LoginState.convert(space.loginState.value))
if (workspace != null) {
buildMenu(workspace, SpaceUserAvatarProvider.getInstance().avatars.value.circle, e.project!!)
.showUnderneathOf(component)
}
else {
val wrapper = Wrapper()
val popup = JBPopupFactory.getInstance().createComponentPopupBuilder(wrapper, wrapper)
.setRequestFocus(true)
.setFocusable(true)
.createPopup()
space.loginState.view(space.lifetime) { _: Lifetime, st: SpaceLoginState ->
val view = createView(st, component) { popup.pack(true, true) }
if (view == null) {
popup.cancel()
return@view
}
wrapper.setContent(view)
wrapper.repaint()
if (st is SpaceLoginState.Disconnected) {
popup.pack(true, true)
}
}
popup.show(RelativePoint(component, Point(-wrapper.preferredSize.width + component.width, component.height)))
}
}
private fun createView(st: SpaceLoginState, component: Component, packPopup: () -> Unit): JComponent? {
return when (st) {
is SpaceLoginState.Connected -> null
is SpaceLoginState.Connecting -> buildConnectingPanel(st, SpaceStatsCounterCollector.LoginPlace.MAIN_TOOLBAR, prettyBorder()) {
st.cancel()
}
is SpaceLoginState.Disconnected -> buildLoginPanelWithPromo(
st,
SpaceStatsCounterCollector.ExplorePlace.MAIN_TOOLBAR,
SpaceStatsCounterCollector.LoginPlace.MAIN_TOOLBAR,
packPopup
) { serverName ->
val space = SpaceWorkspaceComponent.getInstance()
space.signInManually(serverName, space.lifetime, component)
}
}
}
private fun buildMenu(workspace: Workspace, icon: Icon, project: Project): AccountsMenuListPopup {
val host = settings.serverSettings.server
val serverUrl = cleanupUrl(host)
val menuItems: MutableList<AccountMenuItem> = mutableListOf()
menuItems += AccountMenuItem.Account(
workspace.me.value.englishFullName(), // NON-NLS
serverUrl,
resizeIcon(icon, VcsCloneDialogUiSpec.Components.popupMenuAvatarSize),
listOf(browseAction(SpaceBundle.message("main.toolbar.open.server", serverUrl), host, true)))
menuItems += AccountMenuItem.Action(SpaceBundle.message("action.com.intellij.space.vcs.clone.SpaceCloneAction.text"),
{ SpaceCloneAction.runClone(project) },
showSeparatorAbove = true)
val projectContext = SpaceProjectContext.getInstance(project)
val context = projectContext.currentContext
if (context.isAssociatedWithSpaceRepository) {
val descriptions = context.reposInProject.keys
menuItems += AccountMenuItem.Action(SpaceBundle.message("action.show.code.reviews.text"), {
SpaceShowReviewsAction.showCodeReviews(project)
})
if (descriptions.size > 1) {
menuItems += AccountMenuItem.Group(SpaceBundle.message("open.in.browser.group.checklists"), descriptions.map {
val checklistsUrl = SpaceUrls.checklists(it.key)
browseAction(SpaceBundle.message("open.in.browser.open.for.project.action", it.project.name), checklistsUrl)
}.toList())
menuItems += AccountMenuItem.Group(SpaceBundle.message("open.in.browser.group.issues"), descriptions.map {
val issuesUrl = SpaceUrls.issues(it.key)
browseAction(SpaceBundle.message("open.in.browser.open.for.project.action", it.project.name), issuesUrl)
}.toList())
}
else if (descriptions.isNotEmpty()) {
val projectKey = descriptions.first().key
menuItems += browseAction(SpaceBundle.message("main.toolbar.checklists.action"), SpaceUrls.checklists(projectKey))
menuItems += browseAction(SpaceBundle.message("main.toolbar.issues.action"), SpaceUrls.issues(projectKey))
}
}
menuItems += AccountMenuItem.Action(SpaceBundle.message("main.toolbar.settings.action"),
{ SpaceSettingsPanel.openSettings(project) },
showSeparatorAbove = true)
menuItems += AccountMenuItem.Action(SpaceBundle.message("main.toolbar.log.out.action"), {
SpaceWorkspaceComponent.getInstance().signOut(SpaceStatsCounterCollector.LogoutPlace.MAIN_TOOLBAR)
})
return AccountsMenuListPopup(project, AccountMenuPopupStep(menuItems))
}
}
@@ -1,43 +0,0 @@
// 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.
package com.intellij.space.actions.internal
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.space.components.SpaceWorkspaceComponent
import com.intellij.space.messages.SpaceBundle
import libraries.klogging.logger
internal class SpaceReceivedPacketsAction : DumbAwareAction() {
companion object {
private val LOG = logger<SpaceReceivedPacketsAction>()
}
override fun update(e: AnActionEvent) {
val presentation = e.presentation
val workspace = SpaceWorkspaceComponent.getInstance().workspace.value
if (workspace == null) {
presentation.isEnabledAndVisible = false
return
}
presentation.isEnabledAndVisible = true
if (workspace.client.logReceivedPackets) {
presentation.text = SpaceBundle.message("action.internal.stop.log.packets.text")
}
else {
presentation.text = SpaceBundle.message("action.internal.start.log.packets.text")
}
}
override fun actionPerformed(e: AnActionEvent) {
val client = SpaceWorkspaceComponent.getInstance().workspace.value?.client ?: return
if (client.logReceivedPackets) {
LOG.info { "Logging packets is stopped" }
client.logReceivedPackets = false
}
else {
LOG.info { "Logging packets is started" }
client.logReceivedPackets = true
}
}
}
@@ -1,20 +0,0 @@
// 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.
package com.intellij.space.auth
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.messages.SpaceBundleExtensions
import com.intellij.space.settings.SpaceSettingsPanel
import com.intellij.space.utils.notify
internal object SpaceAuthNotifier {
fun authFailed() {
val tryAgainAction = object : DumbAwareAction(SpaceBundleExtensions.messagePointer("auth.notification.failed.login.again.action")) {
override fun actionPerformed(e: AnActionEvent) {
SpaceSettingsPanel.openSettings(null)
}
}
notify(SpaceBundle.message("auth.notification.failed.message"), listOf(tryAgainAction))
}
}
@@ -1,72 +0,0 @@
// 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.
package com.intellij.space.auth
import com.intellij.openapi.application.ApplicationManager
import com.intellij.util.concurrency.AppExecutorUtil
import io.ktor.application.call
import io.ktor.http.ContentType
import io.ktor.request.uri
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.asCoroutineDispatcher
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.launch
import java.io.IOException
private val ioDispatcher = AppExecutorUtil.getAppExecutorService().asCoroutineDispatcher()
internal suspend fun startRedirectHandling(lifetime: Lifetime, server: String, ports: Collection<Int>): SpaceRedirectHandlingInfo? {
val info = CompletableDeferred<SpaceRedirectHandlingInfo?>()
launch(lifetime, ioDispatcher) {
for (port in ports.distinct().shuffled()) {
try {
val redirectUrl = startAuthServerAsync(lifetime, server, port)
info.complete(SpaceRedirectHandlingInfo(port, redirectUrl))
break
}
catch (e: IOException) {
// check next port
}
}
info.complete(null)
}
return info.await()
}
private fun startAuthServerAsync(lifetime: Lifetime, serverUrl: String, port: Int): Deferred<String> {
val redirectUrl = CompletableDeferred<String>()
val server = embeddedServer(Netty, port = port, host = "localhost") {
routing {
get("/auth") {
call.respondText(
createAuthPage(serverUrl),
contentType = ContentType.Text.Html
)
redirectUrl.complete(call.request.uri)
}
}
}
server.start(wait = false)
fun stopServer() {
ApplicationManager.getApplication().executeOnPooledThread(Runnable {
server.stop(1000, 1000)
})
}
lifetime.addOrCallImmediately {
stopServer()
}
redirectUrl.invokeOnCompletion {
stopServer()
}
return redirectUrl
}
internal data class SpaceRedirectHandlingInfo(val port: Int, val redirectUrl: Deferred<String>)
@@ -1,62 +0,0 @@
// 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.
package com.intellij.space.auth
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.text.HtmlChunk.*
import com.intellij.space.messages.SpaceBundle
import com.intellij.ui.AppUIUtil
import java.io.File
internal fun createAuthPage(server: String): String = html()
.child(
head()
.child(styleTag(loadAuthResourceFileContent("spaceAuthPage.css")))
)
.child(
body().child(
div().attr("class", "central-div")
.child(
div().attr("class", "central-div-header")
.child(
div().attr("class", "central-div-header-icon").addRaw(loadAuthResourceFileContent("spaceAuthIcon.svg"))
)
.child(
div().attr("class", "central-div-header-text").addText(SpaceBundle.message("auth.page.jetbrains.space.header.text"))
)
)
.child(
div().attr("class", "central-div-content")
.child(
div().attr("class", "central-div-content-icon").addRaw(loadProductSvg() ?: "")
)
.child(
div().attr("class", "central-div-content-text").addText(SpaceBundle.message("auth.page.content.text"))
)
)
.child(
link(server, SpaceBundle.message("auth.page.go.to.space.link")).attr("class", "central-div-content-link")
)
)
)
.toString()
@NlsSafe
private fun loadAuthResourceFileContent(fileName: String): String {
val plugin = PluginManagerCore.getPlugin(PluginId.getId("com.jetbrains.space"))
?: throw IllegalStateException("Couldn't find space plugin descriptor")
val loader = plugin.pluginClassLoader
return loader.getResourceAsStream("auth/$fileName")!!.bufferedReader(Charsets.UTF_8).readText()
}
@NlsSafe
private fun loadProductSvg(): String? {
AppUIUtil.findIcon().takeIf { it?.endsWith(".svg") ?: false }?.let { iconPath ->
val iconFile = File(iconPath)
if (iconFile.exists() && iconFile.isFile) {
return iconFile.readText(Charsets.UTF_8)
}
}
return null
}
@@ -1,56 +0,0 @@
// 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.
package com.intellij.space.chat.editor
import com.intellij.diff.util.FileEditorBase
import com.intellij.openapi.Disposable
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.fileEditor.FileEditorPolicy
import com.intellij.openapi.fileEditor.FileEditorProvider
import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.space.chat.ui.SpaceChatPanel
import com.intellij.space.messages.SpaceBundle
import libraries.coroutines.extra.LifetimeSource
import javax.swing.JComponent
private class SpaceChatEditor(private val project: Project, private val spaceChatFile: SpaceChatFile) : FileEditorBase() {
private val editorLifetime = LifetimeSource()
init {
Disposer.register(this, Disposable { editorLifetime.terminate() })
}
val component by lazy {
val headerDetails = spaceChatFile.headerDetailsBuilder(editorLifetime)
SpaceChatPanel(project, editorLifetime, this, spaceChatFile.channelsVm, spaceChatFile.chatRecord, headerDetails)
}
override fun getComponent(): JComponent = component
override fun getPreferredFocusedComponent(): JComponent? = null
override fun getName(): String = SpaceBundle.message("chat.editor.name")
override fun getFile() = spaceChatFile
}
internal class SpaceChatEditorProvider : FileEditorProvider, DumbAware {
override fun accept(project: Project, file: VirtualFile): Boolean = file is SpaceChatFile
override fun createEditor(project: Project, file: VirtualFile): FileEditor = SpaceChatEditor(project, file as SpaceChatFile)
override fun getEditorTypeId(): String = "SpaceChatEditor"
override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.HIDE_DEFAULT_EDITOR
override fun disposeEditor(editor: FileEditor) {
if (editor.file?.getUserData(FileEditorManagerImpl.CLOSING_TO_REOPEN) != true) {
Disposer.dispose(editor)
}
super.disposeEditor(editor)
}
}
@@ -1,103 +0,0 @@
// 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.
package com.intellij.space.chat.editor
import circlet.client.api.M2ChannelRecord
import circlet.m2.ChannelsVm
import circlet.platform.api.Ref
import com.intellij.ide.FileIconProvider
import com.intellij.ide.actions.SplitAction
import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFilePathWrapper
import com.intellij.space.chat.model.api.SpaceChatHeaderDetails
import com.intellij.space.editor.SpaceChatComplexPathVirtualFileSystem
import com.intellij.space.messages.SpaceBundle
import com.intellij.testFramework.LightVirtualFile
import icons.SpaceIcons
import libraries.coroutines.extra.Lifetime
import javax.swing.Icon
/**
* @param id is used only for equals and hashcode to avoid multiple editor tabs the same [SpaceChatFile]
*/
internal class SpaceChatFile(
private val sessionId: String,
private val projectHash: String,
@NlsSafe val id: String,
@NlsSafe path: String,
@NlsContexts.TabTitle val displayName: String,
@NlsContexts.Tooltip val tabTooltip: String,
val channelsVm: ChannelsVm,
val chatRecord: Ref<M2ChannelRecord>,
val headerDetailsBuilder: (Lifetime) -> SpaceChatHeaderDetails
) : LightVirtualFile(path, SpaceChatFileType.instance, ""), VirtualFilePathWrapper {
init {
putUserData(SplitAction.FORBID_TAB_SPLIT, true)
isWritable = false
}
override fun getFileSystem() = SpaceChatComplexPathVirtualFileSystem.getInstance()
override fun getPath(): String = try {
fileSystem.getPath(sessionId, projectHash, id)
}
catch (e: Exception) {
name
}
override fun enforcePresentableName(): Boolean = true
override fun getPresentablePath(): String = name
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SpaceChatFile
if (id != other.id) return false
return true
}
override fun hashCode(): Int = id.hashCode()
}
internal class SpaceChatFileTabTitleProvider : EditorTabTitleProvider {
override fun getEditorTabTitle(project: Project, file: VirtualFile): String? = (file as? SpaceChatFile)?.displayName
override fun getEditorTabTooltipText(project: Project, virtualFile: VirtualFile): String? = (virtualFile as? SpaceChatFile)?.tabTooltip
}
internal class SpaceChatIconProvider : FileIconProvider {
override fun getIcon(file: VirtualFile, flags: Int, project: Project?): Icon? {
if (file !is SpaceChatFile) {
return null
}
return SpaceIcons.Main
}
}
private class SpaceChatFileType private constructor(): FileType {
override fun getName(): String = "SpaceChat"
override fun getDescription(): String = SpaceBundle.message("filetype.chat.description")
override fun getDefaultExtension(): String = ""
override fun getIcon(): Icon = SpaceIcons.Main
override fun isBinary(): Boolean = true
override fun isReadOnly(): Boolean = true
companion object {
val instance = SpaceChatFileType()
}
}
@@ -1,78 +0,0 @@
// 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.
package com.intellij.space.chat.markdown
import org.intellij.markdown.IElementType
import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.flavours.gfm.GFMElementTypes
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import org.intellij.markdown.flavours.gfm.GFMTokenTypes
import org.intellij.markdown.flavours.gfm.StrikeThroughParser
import org.intellij.markdown.html.GeneratingProvider
import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.html.SimpleInlineTagProvider
import org.intellij.markdown.parser.LinkMap
import org.intellij.markdown.parser.MarkdownParser
import org.intellij.markdown.parser.sequentialparsers.RangesListBuilder
import org.intellij.markdown.parser.sequentialparsers.SequentialParser
import org.intellij.markdown.parser.sequentialparsers.SequentialParserManager
import org.intellij.markdown.parser.sequentialparsers.TokensCache
import org.intellij.markdown.parser.sequentialparsers.impl.*
import org.jetbrains.annotations.Contract
import java.net.URI
@Contract(pure = true)
internal fun convertToHtml(markdownText: String, server: String? = null): String {
val flavour = SpaceFlavourDescriptor()
val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(markdownText)
val providers = flavour.createHtmlGeneratingProviders(
linkMap = LinkMap.buildLinkMap(parsedTree, markdownText),
baseURI = server?.let { URI(it) }
)
val htmlText = HtmlGenerator(markdownText, parsedTree, providers, false).generateHtml()
return htmlText.removePrefix("<body>").removeSuffix("</body>")
}
private class SpaceFlavourDescriptor : GFMFlavourDescriptor() {
override val sequentialParserManager = object : SequentialParserManager() {
override fun getParserSequence(): List<SequentialParser> = listOf(
AutolinkParser(listOf(MarkdownTokenTypes.AUTOLINK, GFMTokenTypes.GFM_AUTOLINK)),
BacktickParser(),
ImageParser(),
InlineLinkParser(),
ReferenceLinkParser(),
LineBreaksParser(), // difference with super
StrikeThroughParser(),
EmphStrongParser()
)
}
override fun createHtmlGeneratingProviders(linkMap: LinkMap, baseURI: URI?): Map<IElementType, GeneratingProvider> {
val parentProviders = super.createHtmlGeneratingProviders(linkMap, baseURI).toMutableMap()
parentProviders[MarkdownElementTypes.EMPH] = SimpleInlineTagProvider("i", 1, -1)
parentProviders[MarkdownElementTypes.STRONG] = SimpleInlineTagProvider("b", 2, -2)
parentProviders[GFMElementTypes.STRIKETHROUGH] = SimpleInlineTagProvider("strike", 2, -2)
return parentProviders
}
}
private class LineBreaksParser : SequentialParser {
override fun parse(tokens: TokensCache, rangesToGlue: List<IntRange>): SequentialParser.ParsingResult {
val result = SequentialParser.ParsingResultBuilder()
val delegateIndices = RangesListBuilder()
var iterator = tokens.RangesListIterator(rangesToGlue)
while (iterator.type != null) {
if (iterator.type == MarkdownTokenTypes.EOL) {
result.withNode(SequentialParser.Node(iterator.index..iterator.index + 1, MarkdownTokenTypes.HARD_LINE_BREAK))
}
else {
delegateIndices.put(iterator.index)
}
iterator = iterator.advance()
}
return result.withFurtherProcessing(delegateIndices.get())
}
}
@@ -1,25 +0,0 @@
// 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.
package com.intellij.space.chat.markdown
import circlet.client.api.UnfurlAttachment
import circlet.client.api.UnfurlDetailsDateTime
import circlet.client.api.UnfurlDetailsProfile
import circlet.client.api.fullName
import circlet.platform.client.resolve
import com.intellij.openapi.util.text.HtmlChunk.link
import com.intellij.space.utils.formatPrettyDateTime
import platform.common.Unfurl
internal fun processUnfurls(markdown: String, unfurls: List<UnfurlAttachment>): String {
var result = markdown
unfurls.map { it.unfurl }.forEach { unfurl ->
result = result.replace(unfurl.link, unfurl.createNewLink())
}
return result
}
private fun Unfurl.createNewLink(): String = when (val unfurlDetails = details) {
is UnfurlDetailsProfile -> link(link, unfurlDetails.profile.resolve().name.fullName()).toString() // NON-NLS
is UnfurlDetailsDateTime -> unfurlDetails.utcMilliseconds.formatPrettyDateTime()
else -> link
}
@@ -1,4 +0,0 @@
// 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.
package com.intellij.space.chat.model.api
internal interface SpaceChatHeaderDetails
@@ -1,74 +0,0 @@
// 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.
package com.intellij.space.chat.model.api
import circlet.client.api.AttachmentInfo
import circlet.client.api.CPrincipal
import circlet.client.api.ChannelItemRecord
import circlet.client.api.M2ChannelRecord
import circlet.m2.M2MessageEditingVm
import circlet.m2.channel.M2ChannelVm
import circlet.m2.threads.M2ThreadPreviewVm
import circlet.platform.api.KDateTime
import circlet.platform.api.Ref
import circlet.platform.api.TID
import com.intellij.openapi.util.NlsSafe
import com.intellij.util.ui.codereview.timeline.TimelineItem
import kotlinx.coroutines.CompletableDeferred
import libraries.coroutines.extra.Lifetime
import runtime.reactive.MutableProperty
import runtime.reactive.Property
internal interface SpaceChatItem : TimelineItem {
val id: TID
val chat: M2ChannelVm
val author: CPrincipal
val text: @NlsSafe String
val created: KDateTime
val type: SpaceChatItemType
val thread: Ref<M2ChannelRecord>?
/**
* hack to prevent threads collapsing when SpaceChatItem is recreated
*/
val loadingThread: MutableProperty<CompletableDeferred<M2ChannelVm>?>
val delivered: Boolean
val link: String?
val canDelete: Boolean
val isEdited: Boolean
val editingVm: Property<M2MessageEditingVm?>
val isEditing: Property<Boolean>
val canEdit: Boolean
val startThreadVm: SpaceChatStartThreadVm
val pending: Boolean?
val attachments: List<AttachmentInfo>
val additionalFeatures: Set<SpaceChatItemAdditionalFeature>
val projectedItem: Property<ChannelItemRecord>?
val threadPreview: M2ThreadPreviewVm?
fun startEditing()
fun stopEditing()
suspend fun delete()
suspend fun loadThread(lifetime: Lifetime): M2ChannelVm?
}
@@ -1,8 +0,0 @@
// 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.
package com.intellij.space.chat.model.api
import runtime.reactive.Property
internal sealed class SpaceChatItemAdditionalFeature {
class ShowResolvedState(val resolved: Property<Boolean>) : SpaceChatItemAdditionalFeature()
}
@@ -1,50 +0,0 @@
// 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.
package com.intellij.space.chat.model.api
import circlet.client.api.M2ItemContentDetails
import circlet.client.api.M2TextItemContent
import circlet.client.api.TD_MemberProfile
import circlet.client.api.mc.ChatMessage
import circlet.client.api.mc.toMessage
import circlet.code.api.*
import circlet.platform.api.Ref
import com.intellij.space.chat.model.api.SpaceChatItemType.*
import com.intellij.space.chat.model.impl.toMCMessageContent
internal fun M2ItemContentDetails?.toType(): SpaceChatItemType = when (this) {
is CodeDiscussionAddedFeedEvent -> CodeDiscussion(codeDiscussion)
is M2TextItemContent -> SimpleText
is ReviewCompletionStateChangedEvent -> ReviewCompletionStateChanged
is ReviewerChangedEvent -> ReviewerChanged(changeType, uid)
is MergeRequestMergedEvent -> MergeRequestMerged(sourceBranch, targetBranch)
is MergeRequestBranchDeletedEvent -> MergeRequestBranchDeleted(branch)
is ReviewTitleChangedEvent -> ReviewTitleChanged(oldTitle, newTitle)
is circlet.client.api.mc.MCMessage -> when (val chatMessage = toMessage()) {
is ChatMessage.Text -> SimpleText
is ChatMessage.Block -> MCMessage(chatMessage.toMCMessageContent())
}
null -> Deleted
else -> Unknown
}
internal sealed class SpaceChatItemType {
class CodeDiscussion(val discussion: Ref<CodeDiscussionRecord>) : SpaceChatItemType()
object SimpleText : SpaceChatItemType()
object ReviewCompletionStateChanged : SpaceChatItemType()
class ReviewerChanged(val changeType: ReviewerChangedType, val uid: Ref<TD_MemberProfile>) : SpaceChatItemType()
class MergeRequestMerged(val sourceBranch: String, val targetBranch: String) : SpaceChatItemType()
class MergeRequestBranchDeleted(val branch: String) : SpaceChatItemType()
class ReviewTitleChanged(val oldTitle: String, val newTitle: String) : SpaceChatItemType()
class MCMessage(val content: SpaceMCMessageContent) : SpaceChatItemType()
object Deleted : SpaceChatItemType()
object Unknown : SpaceChatItemType()
}
@@ -1,16 +0,0 @@
// 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.
package com.intellij.space.chat.model.api
import runtime.reactive.Property
internal interface SpaceChatStartThreadVm {
val canStartThread: Property<Boolean>
val isWritingFirstMessage: Property<Boolean>
fun startWritingFirstMessage()
fun stopWritingFirstMessage()
suspend fun startThread(message: String)
}
@@ -1,24 +0,0 @@
// 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.
package com.intellij.space.chat.model.api
import circlet.client.api.mc.MessageOutline
import circlet.client.api.mc.MessageStyle
import javax.swing.Icon
internal interface SpaceMCMessageContent {
val outline: MessageOutline?
val sections: List<SpaceMCMessageSection>
val style: MessageStyle
}
internal sealed class SpaceMCMessageSection {
class Section(val header: String?, val elements: List<SpaceMCMessageElement>, val footer: String?) : SpaceMCMessageSection()
object Divider : SpaceMCMessageSection()
}
internal sealed class SpaceMCMessageElement {
class Text(val icon: Icon?, val content: String) : SpaceMCMessageElement()
object MessageDivider : SpaceMCMessageElement()
}
@@ -1,106 +0,0 @@
// 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.
package com.intellij.space.chat.model.impl
import circlet.m2.M2MessageVm
import circlet.m2.channel.M2ChannelVm
import circlet.platform.client.property
import com.intellij.space.chat.model.api.SpaceChatItem
import com.intellij.space.chat.model.api.SpaceChatItemAdditionalFeature
import com.intellij.space.chat.model.api.toType
import com.intellij.space.chat.ui.awaitFullLoad
import kotlinx.coroutines.CompletableDeferred
import libraries.coroutines.extra.Lifetime
import runtime.reactive.mutableProperty
internal class SpaceChatItemImpl private constructor(
private val messageVm: M2MessageVm,
override val link: String?,
override val additionalFeatures: Set<SpaceChatItemAdditionalFeature> = setOf()
) : SpaceChatItem {
private val record = messageVm.message
override val id = record.id
override val thread = messageVm.message.thread
override val loadingThread = mutableProperty<CompletableDeferred<M2ChannelVm>?>(null)
override val threadPreview = messageVm.thread
override val chat = messageVm.channelVm
override val author = record.author
override val created = record.created
override val type = record.details.toType()
override val delivered = messageVm.delivered
override val canDelete = messageVm.canDelete
override val text = record.text
override val editingVm = messageVm.editingVm
override val isEditing = messageVm.isEditing
override val canEdit = messageVm.canEdit
override val isEdited = record.edited != null
override val startThreadVm = SpaceChatStartThreadVmImpl(messageVm, threadPreview, loadingThread)
override val pending = record.pending
override val attachments = record.attachments ?: listOf()
override val projectedItem = record.projectedItem?.property()
override fun startEditing() {
messageVm.startEditing()
}
override fun stopEditing() {
messageVm.stopEditing()
}
override suspend fun delete() {
chat.deleteCurrentOrOriginalMessage(id)
}
override suspend fun loadThread(lifetime: Lifetime): M2ChannelVm? {
threadPreview ?: return null
val loadingThreadDeferred = CompletableDeferred<M2ChannelVm>()
loadingThread.value = loadingThreadDeferred
return chat.channelsVm.thread(lifetime, chat, threadPreview, null).also {
it.awaitFullLoad(lifetime)
loadingThreadDeferred.complete(it)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (other !is SpaceChatItemImpl) {
return false
}
if (record != other.record) {
return false
}
return true
}
override fun hashCode(): Int = record.hashCode()
companion object {
internal fun M2MessageVm.convertToChatItem(
link: String?,
additionalFeatures: Set<SpaceChatItemAdditionalFeature> = setOf()
): SpaceChatItem = SpaceChatItemImpl(this, link, additionalFeatures = additionalFeatures)
}
}
@@ -1,16 +0,0 @@
// 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.
package com.intellij.space.chat.model.impl
import circlet.code.api.CodeReviewState
import com.intellij.openapi.util.NlsSafe
import com.intellij.space.chat.model.api.SpaceChatHeaderDetails
import com.intellij.space.vcs.SpaceProjectInfo
import org.jetbrains.annotations.Nls
import runtime.reactive.Property
internal class SpaceChatReviewHeaderDetails(
val spaceProject: SpaceProjectInfo,
val state: Property<CodeReviewState>,
@Nls val title: Property<String>,
@NlsSafe val reviewKey: String?,
) : SpaceChatHeaderDetails
@@ -1,56 +0,0 @@
// 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.
package com.intellij.space.chat.model.impl
import circlet.m2.M2MessageVm
import circlet.m2.channel.M2ChannelVm
import circlet.m2.threads.M2ThreadPreviewVm
import com.intellij.space.chat.model.api.SpaceChatStartThreadVm
import com.intellij.space.chat.ui.awaitFullLoad
import kotlinx.coroutines.CompletableDeferred
import runtime.reactive.MutableProperty
import runtime.reactive.mutableProperty
import runtime.reactive.property
import runtime.reactive.property.map
internal class SpaceChatStartThreadVmImpl(
private val messageVm: M2MessageVm,
private val threadPreview: M2ThreadPreviewVm?,
private val loadingThread: MutableProperty<CompletableDeferred<M2ChannelVm>?>
) : SpaceChatStartThreadVm {
private val lifetime = messageVm.lifetime
override val isWritingFirstMessage = mutableProperty(false)
override val canStartThread = lifetime.map(
messageVm.canOpenThread,
isWritingFirstMessage,
threadPreview?.messageCount ?: property(null)
) { canOpenThread, writingFirstMessage, messagesCount ->
canOpenThread && !writingFirstMessage && (messagesCount == null || messagesCount == 0)
}
override fun startWritingFirstMessage() {
isWritingFirstMessage.value = true
}
override fun stopWritingFirstMessage() {
isWritingFirstMessage.value = false
}
override suspend fun startThread(message: String) {
val threadChannelDeferred = CompletableDeferred<M2ChannelVm>()
loadingThread.value = threadChannelDeferred
val newThreadPreview = threadPreview ?: messageVm.makeThreadPreview()
val channelsVm = messageVm.channelsVm
val viewerLifetime = messageVm.lifetime
val threadChannel = channelsVm.thread(viewerLifetime, messageVm.channelVm, newThreadPreview, null)
threadChannel.awaitFullLoad(viewerLifetime)
threadChannel.sendMessage(message)
threadChannelDeferred.complete(threadChannel)
stopWritingFirstMessage()
}
}
@@ -1,38 +0,0 @@
// 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.
package com.intellij.space.chat.model.impl
import circlet.client.api.mc.*
import com.intellij.icons.AllIcons
import com.intellij.space.chat.model.api.SpaceMCMessageContent
import com.intellij.space.chat.model.api.SpaceMCMessageElement
import com.intellij.space.chat.model.api.SpaceMCMessageSection
import javax.swing.Icon
internal fun ChatMessage.Block.toMCMessageContent(): SpaceMCMessageContent =
SpaceMCMessageContentImpl(outline, sections.mapNotNull { it.convert() }, style ?: MessageStyle.PRIMARY)
private fun MessageSectionElement.convert(): SpaceMCMessageSection? = when (this) {
is MessageSection -> SpaceMCMessageSection.Section(header, elements.mapNotNull { it.convert() }, footer)
is MessageDivider -> SpaceMCMessageSection.Divider
else -> null
}
private fun MessageElement.convert(): SpaceMCMessageElement? = when (this) {
is MessageText -> {
val icon = (accessory as? MessageIcon)?.icon?.toIcon()
SpaceMCMessageElement.Text(icon, content)
}
is MessageDivider -> SpaceMCMessageElement.MessageDivider
else -> null
}
private fun ApiIcon.toIcon(): Icon? = when (icon) {
"commits", "commits-small" -> AllIcons.Vcs.CommitNode
else -> null
}
private class SpaceMCMessageContentImpl(
override val outline: MessageOutline?,
override val sections: List<SpaceMCMessageSection>,
override val style: MessageStyle
) : SpaceMCMessageContent
@@ -1,8 +0,0 @@
// 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.
package com.intellij.space.chat.ui
import javax.swing.JPanel
internal abstract class HoverableJPanel : JPanel(null) {
abstract fun hoverStateChanged(isHovered: Boolean)
}
@@ -1,9 +0,0 @@
// 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.
package com.intellij.space.chat.ui
import com.intellij.util.ui.JBValue
internal enum class SpaceChatAvatarType(val size: JBValue) {
MAIN_CHAT(JBValue.UIInteger("space.chat.avatar.size", 30)),
THREAD(JBValue.UIInteger("space.chat.thread.avatar.size", 20))
}
@@ -1,97 +0,0 @@
// 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.
package com.intellij.space.chat.ui
import circlet.client.api.M2ChannelRecord
import circlet.client.api.TD_MemberProfile
import circlet.client.api.fullName
import circlet.m2.ChannelsVm
import circlet.m2.M2MessageVm
import circlet.m2.channel.M2ChannelVm
import circlet.platform.api.Ref
import circlet.platform.api.isTemporary
import com.intellij.ide.plugins.newui.VerticalLayout
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.space.chat.model.impl.SpaceChatItemImpl.Companion.convertToChatItem
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.space.ui.SpaceAvatarProvider
import com.intellij.space.utils.SpaceUrls
import com.intellij.ui.ColorUtil
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.codereview.timeline.TimelineComponent
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.delay
import org.jetbrains.annotations.Nls
import runtime.reactive.awaitTrue
import javax.swing.JPanel
internal class SpaceChatContentPanel(
private val project: Project,
private val lifetime: Lifetime,
parent: Disposable,
channelsVm: ChannelsVm,
chatRecord: Ref<M2ChannelRecord>
) : SpaceChatContentPanelBase(lifetime, parent, channelsVm, chatRecord) {
private val server = channelsVm.client.server
override fun onChatLoad(chatVm: M2ChannelVm) {
val itemsListModel = SpaceChatItemListModel()
val contentPanel = createContentPanel(itemsListModel, chatVm)
chatVm.mvms.forEach(lifetime) { messagesViewModel ->
val chatItems = messagesViewModel.messages.map { messageViewModel ->
messageViewModel.convertToChatItem(messageViewModel.getLink())
}
itemsListModel.messageListUpdated(chatItems)
stopLoadingContent(contentPanel)
}
}
private fun createContentPanel(model: SpaceChatItemListModel, chatVM: M2ChannelVm): JPanel {
val avatarSize = SpaceChatAvatarType.MAIN_CHAT.size
val avatarProvider = SpaceAvatarProvider(lifetime, this, avatarSize)
val itemComponentFactory = SpaceChatItemComponentFactory(project, lifetime, server, avatarProvider)
val timeline = TimelineComponent(model, itemComponentFactory, offset = 0).apply {
border = JBUI.Borders.emptyBottom(10)
}
return JPanel(VerticalLayout(0)).apply {
isOpaque = false
add(timeline, VerticalLayout.FILL_HORIZONTAL)
add(createNewMessageField(chatVM, SpaceChatAvatarType.MAIN_CHAT, SpaceStatsCounterCollector.SendMessagePlace.MAIN_CHAT),
VerticalLayout.FILL_HORIZONTAL)
}
}
}
internal suspend fun M2ChannelVm.awaitFullLoad(lifetime: Lifetime) {
ready.awaitTrue(lifetime)
val messages = mvms.prop
while (messages.value.hasPrev) {
// TODO: remove this temporary fix when it will be fixed in platform
delay(1)
loadPrev()
}
while (messages.value.hasNext) {
delay(1)
loadNext()
}
}
internal fun M2MessageVm.getLink(): String? =
if (canCopyLink && !message.isTemporary()) {
SpaceUrls.message(channelVm.key.value, message)
}
else {
null
}
@Nls
internal fun TD_MemberProfile.link(): HtmlChunk =
HtmlChunk.link(SpaceUrls.member(username), name.fullName()) // NON-NLS
@NlsSafe
internal fun getGrayTextHtml(@Nls text: String): String =
HtmlChunk.span("color: ${ColorUtil.toHtmlColor(UIUtil.getContextHelpForeground())}").addRaw(text).toString()
@@ -1,92 +0,0 @@
// 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.
package com.intellij.space.chat.ui
import circlet.client.api.M2ChannelRecord
import circlet.m2.ChannelsVm
import circlet.m2.channel.M2ChannelVm
import circlet.platform.api.Ref
import com.intellij.openapi.Disposable
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.ui.ComponentUtil
import com.intellij.ui.components.JBLoadingPanel
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.components.BorderLayoutPanel
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.launch
import org.jetbrains.annotations.Nls
import runtime.Ui
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.event.MouseEvent
import java.awt.event.MouseMotionAdapter
import javax.swing.JComponent
internal abstract class SpaceChatContentPanelBase(
private val lifetime: Lifetime,
parent: Disposable,
private val channelsVm: ChannelsVm,
private val chatRecord: Ref<M2ChannelRecord>
) : BorderLayoutPanel() {
private val loadingPanel = object : JBLoadingPanel(BorderLayout(), parent) {
init {
startLoading()
isOpaque = false
}
override fun getPreferredSize(): Dimension {
val size = super.getPreferredSize()
return Dimension(size.width, size.height.takeIf { it != 0 } ?: JBUI.scale(80))
}
}
init {
isOpaque = true
background = EditorColorsManager.getInstance().globalScheme.defaultBackground
addToCenter(loadingPanel)
addHoveringSupport()
launch(lifetime, Ui) {
val chatVM = loadChatVM()
onChatLoad(chatVM)
}
}
private fun addHoveringSupport() {
var lastHoveredMessagePanel: HoverableJPanel? = null
addMouseMotionListener(object : MouseMotionAdapter() {
override fun mouseMoved(e: MouseEvent?) {
e ?: return
val component = UIUtil.getDeepestComponentAt(this@SpaceChatContentPanelBase, e.x, e.y) ?: return
val messageComponent = ComponentUtil.getParentOfType(HoverableJPanel::class.java, component)
if (messageComponent != lastHoveredMessagePanel) {
lastHoveredMessagePanel?.hoverStateChanged(false)
lastHoveredMessagePanel = messageComponent
messageComponent?.hoverStateChanged(true)
}
}
})
}
private suspend fun loadChatVM(): M2ChannelVm = channelsVm.channel(lifetime, chatRecord).also {
it.awaitFullLoad(lifetime)
}
protected abstract fun onChatLoad(chatVm: M2ChannelVm)
@RequiresEdt
protected fun stopLoadingContent(contentComponent: JComponent) {
if (loadingPanel.isLoading) {
loadingPanel.add(contentComponent, BorderLayout.CENTER)
loadingPanel.stopLoading()
loadingPanel.revalidate()
loadingPanel.repaint()
}
}
fun setLoadingText(@Nls text: String) {
loadingPanel.setLoadingText(text)
}
}
@@ -1,75 +0,0 @@
// 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.
package com.intellij.space.chat.ui
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.project.Project
import com.intellij.space.chat.model.api.SpaceChatItem
import com.intellij.space.components.SpaceWorkspaceComponent
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.util.ui.codereview.SingleValueModel
import com.intellij.util.ui.codereview.SingleValueModelImpl
import com.intellij.util.ui.codereview.ToggleableContainer
import com.intellij.util.ui.codereview.timeline.comment.SubmittableTextField
import com.intellij.util.ui.codereview.timeline.comment.SubmittableTextFieldModelBase
import com.intellij.util.ui.components.BorderLayoutPanel
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.launch
import runtime.Ui
import runtime.reactive.awaitLoaded
import javax.swing.JComponent
internal class SpaceChatEditableComponent(
project: Project,
lifetime: Lifetime,
content: JComponent,
message: SpaceChatItem
) : BorderLayoutPanel() {
val editingModel: SingleValueModel<Boolean> = SingleValueModelImpl(false)
init {
isOpaque = false
val submittableModel = object : SubmittableTextFieldModelBase("") {
override fun submit() {
val editingVm = message.editingVm.value
val newText = document.text
if (editingVm != null) {
val id = editingVm.message.id
launch(lifetime, Ui) {
val chat = editingVm.channel.awaitLoaded(lifetime)
if (newText.isBlank()) {
SpaceStatsCounterCollector.SEND_EDIT_MESSAGE.log(project, true)
chat?.deleteMessage(id)
}
else {
SpaceStatsCounterCollector.SEND_EDIT_MESSAGE.log(project, false)
chat?.alterMessage(id, newText)
}
}
}
message.stopEditing()
}
}
message.editingVm.forEach(lifetime) { editingVm ->
if (editingVm == null) {
editingModel.value = false
return@forEach
}
val workspace = SpaceWorkspaceComponent.getInstance().workspace.value ?: return@forEach
runWriteAction {
submittableModel.document.setText(workspace.completion.editable(editingVm.message.text))
}
editingModel.value = true
SpaceStatsCounterCollector.START_EDIT_MESSAGE.log(project)
}
addToCenter(
ToggleableContainer.create(editingModel, { content }, {
SubmittableTextField(SpaceBundle.message("chat.message.edit.action.text"), submittableModel, onCancel = {
SpaceStatsCounterCollector.DISCARD_EDIT_MESSAGE.log(project)
message.stopEditing()
})
})
)
}
}
@@ -1,262 +0,0 @@
// 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.
package com.intellij.space.chat.ui
import circlet.code.api.ReviewerChangedType
import circlet.platform.client.resolve
import circlet.principals.asUser
import com.intellij.icons.AllIcons
import com.intellij.ide.plugins.newui.VerticalLayout
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.openapi.util.text.HtmlChunk.html
import com.intellij.space.chat.model.api.SpaceChatItem
import com.intellij.space.chat.model.api.SpaceChatItemType.*
import com.intellij.space.chat.ui.discussion.SpaceChatCodeDiscussionComponentFactory
import com.intellij.space.chat.ui.message.MessageTitleComponent
import com.intellij.space.chat.ui.message.SpaceChatMessagePendingHeader
import com.intellij.space.chat.ui.message.SpaceMCMessageComponent
import com.intellij.space.chat.ui.message.SpaceStyledMessageComponent
import com.intellij.space.chat.ui.thread.createCollapsedThreadComponent
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.space.ui.SpaceAvatarProvider
import com.intellij.space.ui.resizeIcon
import com.intellij.ui.components.JBLabel
import com.intellij.ui.scale.JBUIScale
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.codereview.SingleValueModelImpl
import com.intellij.util.ui.codereview.ToggleableContainer
import com.intellij.util.ui.codereview.timeline.TimelineItemComponentFactory
import com.intellij.util.ui.codereview.timeline.comment.SubmittableTextFieldModelBase
import com.intellij.util.ui.components.BorderLayoutPanel
import icons.SpaceIcons
import icons.VcsCodeReviewIcons
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.launch
import runtime.Ui
import javax.swing.*
internal class SpaceChatItemComponentFactory(
private val project: Project,
private val lifetime: Lifetime,
private val server: String,
private val avatarProvider: SpaceAvatarProvider
) : TimelineItemComponentFactory<SpaceChatItem> {
private val codeDiscussionComponentFactory = SpaceChatCodeDiscussionComponentFactory(project, lifetime, server)
/**
* Method should return [HoverableJPanel] because it is used to implement hovering properly
*
* @see SpaceChatPanel
*/
override fun createComponent(item: SpaceChatItem): HoverableJPanel {
val component =
when (val type = item.type) {
is CodeDiscussion ->
codeDiscussionComponentFactory.createComponent(item, type.discussion) ?: createUnsupportedMessageTypePanel(item.link)
is SimpleText -> SpaceChatEditableComponent(project, lifetime, createSimpleMessageComponent(item), item)
is ReviewCompletionStateChanged -> SpaceStyledMessageComponent(createSimpleMessageComponent(item))
is ReviewerChanged -> {
val user = type.uid.resolve().link()
val text = when (type.changeType) {
ReviewerChangedType.Joined -> SpaceBundle.message("chat.reviewer.added", user)
ReviewerChangedType.Left -> SpaceBundle.message("chat.reviewer.removed", user)
}
SpaceStyledMessageComponent(SpaceChatMarkdownTextComponent(server, text))
}
is MergeRequestMerged -> SpaceStyledMessageComponent(
SpaceChatMarkdownTextComponent(
server,
HtmlChunk.raw(
SpaceBundle.message(
"chat.review.merged",
HtmlChunk.text(type.sourceBranch).bold(), // NON-NLS
HtmlChunk.text(type.targetBranch).bold() // NON-NLS
)
).wrapWith(html()).toString()
)
)
is MergeRequestBranchDeleted -> SpaceStyledMessageComponent(
SpaceChatMarkdownTextComponent(
server,
HtmlChunk.raw(
SpaceBundle.message("chat.review.deleted.branch", HtmlChunk.text(type.branch).bold()) // NON-NLS
).wrapWith(html()).toString()
)
)
is ReviewTitleChanged -> SpaceStyledMessageComponent(
SpaceChatMarkdownTextComponent(
server,
HtmlChunk.raw(
SpaceBundle.message(
"chat.review.title.changed",
HtmlChunk.text(type.oldTitle).bold(), // NON-NLS
HtmlChunk.text(type.newTitle).bold() // NON-NLS
)
).wrapWith(html()).toString()
)
)
is MCMessage -> SpaceMCMessageComponent(server, type.content, item.attachments)
is Deleted -> SpaceChatMarkdownTextComponent(server, getGrayTextHtml(SpaceBundle.message("chat.message.removed.text")))
is Unknown -> createUnsupportedMessageTypePanel(item.link)
}
return Item(
createAvatarIcon(item),
createTitleComponent(item),
SpaceChatMessagePendingHeader(item),
component.addThreadComponentIfNeeded(item).addStartThreadField(item)
)
}
private fun createTitleComponent(item: SpaceChatItem): MessageTitleComponent? {
if (item.type == Deleted) {
return null
}
return MessageTitleComponent(project, lifetime, item)
}
private fun createAvatarIcon(item: SpaceChatItem): Icon {
val user = item.author.asUser
if (user != null) {
return avatarProvider.getIcon(user)
}
if (item.type == Deleted) {
return resizeIcon(VcsCodeReviewIcons.Delete)
}
return resizeIcon(SpaceIcons.Main)
}
private fun resizeIcon(icon: Icon) = resizeIcon(icon, avatarProvider.iconSize.get())
private fun createUnsupportedMessageTypePanel(messageLink: String?): JComponent {
val description = if (messageLink != null) {
SpaceBundle.message("chat.unsupported.message.type.with.link", messageLink)
}
else {
SpaceBundle.message("chat.unsupported.message.type")
}
return JBLabel(description, AllIcons.General.Warning, SwingConstants.LEFT).setCopyable(true)
}
private fun JComponent.addThreadComponentIfNeeded(message: SpaceChatItem): JComponent {
return if (message.type is CodeDiscussion) {
this
}
else {
val threadComponent = createCollapsedThreadComponent(project, lifetime, message, null)
JPanel(VerticalLayout(0)).apply {
isOpaque = false
add(this@addThreadComponentIfNeeded, VerticalLayout.FILL_HORIZONTAL)
add(threadComponent, VerticalLayout.FILL_HORIZONTAL)
}
}
}
private fun JComponent.addStartThreadField(message: SpaceChatItem): JComponent {
val startThreadVm = message.startThreadVm
val isWritingFirstMessageModel = SingleValueModelImpl(false)
startThreadVm.isWritingFirstMessage.forEach(lifetime) {
isWritingFirstMessageModel.value = it
}
val emptyPanel = JPanel().apply {
isOpaque = false
border = JBUI.Borders.empty()
}
val statsPlace = SpaceStatsCounterCollector.SendMessagePlace.NEW_THREAD
val firstThreadMessageField = ToggleableContainer.create(isWritingFirstMessageModel, { emptyPanel }, {
val submittableModel = object : SubmittableTextFieldModelBase("") {
override fun submit() {
isBusy = true
SpaceStatsCounterCollector.SEND_MESSAGE.log(statsPlace, false)
launch(lifetime, Ui) {
startThreadVm.startThread(document.text)
isBusy = false
}
}
}
val newMessageComponent = SpaceChatNewMessageWithAvatarComponent(
lifetime,
SpaceChatAvatarType.THREAD,
submittableModel,
statsPlace,
onCancel = { startThreadVm.stopWritingFirstMessage() }
)
BorderLayoutPanel().apply {
isOpaque = false
border = JBUI.Borders.emptyTop(10)
addToCenter(newMessageComponent)
}
})
return JPanel(VerticalLayout(0)).apply {
isOpaque = false
border = JBUI.Borders.empty()
add(this@addStartThreadField, VerticalLayout.FILL_HORIZONTAL)
add(firstThreadMessageField, VerticalLayout.FILL_HORIZONTAL)
}
}
private fun createSimpleMessageComponent(message: SpaceChatItem): SpaceChatMarkdownTextComponent =
SpaceChatMarkdownTextComponent(server, message.text, message.isEdited)
internal class Item(
avatar: Icon,
private val title: MessageTitleComponent?,
header: JComponent,
content: JComponent
) : HoverableJPanel() {
companion object {
val AVATAR_GAP: Int
get() = JBUIScale.scale(8)
}
init {
val headerPart = BorderLayoutPanel().apply {
isOpaque = false
border = JBUI.Borders.emptyLeft(avatar.iconWidth + AVATAR_GAP)
addToCenter(header)
}
val avatarPanel = BorderLayoutPanel().apply {
isOpaque = false
border = JBUI.Borders.empty()
addToTop(userAvatar(avatar))
}
val rightPart = JPanel(VerticalLayout(JBUI.scale(5))).apply {
isOpaque = false
border = JBUI.Borders.emptyLeft(AVATAR_GAP)
title?.let { add(it, VerticalLayout.FILL_HORIZONTAL) }
val contentWrapper = BorderLayoutPanel().apply {
isOpaque = false
if (title == null) {
border = JBUI.Borders.emptyTop(avatar.iconHeight / 4)
}
addToCenter(content)
}
add(contentWrapper, VerticalLayout.FILL_HORIZONTAL)
}
val messagePanel = BorderLayoutPanel().apply {
isOpaque = false
addToLeft(avatarPanel)
addToCenter(rightPart)
}
layout = VerticalLayout(JBUI.scale(3))
isOpaque = false
border = JBUI.Borders.emptyTop(15)
add(headerPart, VerticalLayout.FILL_HORIZONTAL)
add(messagePanel, VerticalLayout.FILL_HORIZONTAL)
}
private fun userAvatar(avatar: Icon) = JLabel(avatar)
override fun hoverStateChanged(isHovered: Boolean) {
title ?: return
title.actionsPanel.isVisible = isHovered
title.revalidate()
title.repaint()
}
}
}
@@ -1,53 +0,0 @@
// 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.
package com.intellij.space.chat.ui
import com.intellij.space.chat.model.api.SpaceChatItem
import javax.swing.AbstractListModel
import kotlin.math.min
internal class SpaceChatItemListModel : AbstractListModel<SpaceChatItem>() {
private val items = mutableListOf<SpaceChatItem>()
override fun getElementAt(index: Int): SpaceChatItem = items[index]
override fun getSize() = items.size
fun messageListUpdated(new: List<SpaceChatItem>) {
val newItems = keepOldItemsState(new)
val oldItems = ArrayList(items)
items.clear()
items.addAll(newItems)
for (i in 0 until min(oldItems.size, newItems.size)) {
if (oldItems[i] != newItems[i]) {
fireContentsChanged(this, i, i)
}
}
if (newItems.size < oldItems.size) {
fireIntervalRemoved(this, newItems.size, oldItems.size - 1)
}
if (newItems.size > oldItems.size) {
fireIntervalAdded(this, oldItems.size, newItems.size - 1)
}
}
private fun keepOldItemsState(new: List<SpaceChatItem>): List<SpaceChatItem> {
val oldItems = items.associateBy { it.id }
return List(new.size) { i ->
val newItem = new[i]
val oldItem = oldItems[newItem.id]
if (oldItem == newItem) {
// if nothing changed, keep old reference
oldItem
}
else {
if (oldItem != null) {
// if smth changed, keep loadingThread state
newItem.loadingThread.value = oldItem.loadingThread.value
}
newItem
}
}
}
}
@@ -1,46 +0,0 @@
// 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.
package com.intellij.space.chat.ui
import circlet.client.api.UnfurlAttachment
import circlet.completion.mentions.MentionConverter
import com.intellij.openapi.util.NlsSafe
import com.intellij.space.chat.markdown.convertToHtml
import com.intellij.space.chat.markdown.processUnfurls
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.vcs.review.HtmlEditorPane
import com.intellij.util.ui.UIUtil
import org.jetbrains.annotations.NonNls
internal class SpaceChatMarkdownTextComponent(
private val server: @NonNls String,
initialText: @NlsSafe String? = null,
initialIsEdited: Boolean = false,
initialUnfurls: List<UnfurlAttachment> = emptyList()
) : HtmlEditorPane() {
init {
putClientProperty(UIUtil.HIDE_EDITOR_FROM_DATA_CONTEXT_PROPERTY, true)
initialText?.let { setMarkdownText(it, initialIsEdited, initialUnfurls) }
}
fun setMarkdownText(text: @NlsSafe String, isEdited: Boolean, unfurls: List<UnfurlAttachment> = emptyList()) {
setBody(processItemText(server, text, isEdited, unfurls))
}
}
@NlsSafe
private fun processItemText(
server: @NonNls String,
@NlsSafe text: String,
isEdited: Boolean,
unfurls: List<UnfurlAttachment>
): String {
val markdown = MentionConverter.markdown(text, server).let {
if (isEdited) {
it + " " + getGrayTextHtml(SpaceBundle.message("chat.message.edited.text"))
}
else {
it
}
}
return convertToHtml(processUnfurls(markdown, unfurls), server)
}
@@ -1,70 +0,0 @@
// 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.
package com.intellij.space.chat.ui
import circlet.client.api.M2ChannelRecord
import circlet.m2.ChannelsVm
import circlet.platform.api.Ref
import com.intellij.openapi.Disposable
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.project.Project
import com.intellij.space.chat.model.api.SpaceChatHeaderDetails
import com.intellij.space.chat.ui.header.createComponent
import com.intellij.space.messages.SpaceBundle
import com.intellij.ui.ScrollPaneFactory
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import libraries.coroutines.extra.Lifetime
import net.miginfocom.layout.AC
import net.miginfocom.layout.CC
import net.miginfocom.layout.LC
import net.miginfocom.swing.MigLayout
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.ScrollPaneConstants
internal class SpaceChatPanel(
private val project: Project,
private val lifetime: Lifetime,
private val parent: Disposable,
private val channelsVm: ChannelsVm,
private val chatRecord: Ref<M2ChannelRecord>,
private val headerDetails: SpaceChatHeaderDetails
) : BorderLayoutPanel() {
init {
isOpaque = true
background = EditorColorsManager.getInstance().globalScheme.defaultBackground
val contentComponent = createContentComponent()
val scrollableContentPanel = ScrollPaneFactory.createScrollPane(
contentComponent,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
).apply {
border = JBUI.Borders.empty()
isOpaque = false
viewport.isOpaque = false
}
addToCenter(scrollableContentPanel)
}
private fun createContentComponent(): JComponent {
val contentPanel = SpaceChatContentPanel(project, lifetime, parent, channelsVm, chatRecord).apply {
setLoadingText(SpaceBundle.message("chat.loading.text"))
}
return JPanel(null).apply {
isOpaque = false
border = JBUI.Borders.empty(5, 20)
val maxWidth = JBUI.scale(600)
layout = MigLayout(LC().gridGap("0", "${JBUI.scale(10)}")
.insets("0", "0", "0", "0")
.flowY(),
AC().size(":$maxWidth:$maxWidth").gap("push"))
add(headerDetails.createComponent(lifetime), CC().growX().minWidth(""))
add(contentPanel, CC().grow().push().minWidth(""))
}
}
}
@@ -1,84 +0,0 @@
// 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.
package com.intellij.space.chat.ui
import circlet.m2.channel.M2ChannelVm
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.space.components.SpaceWorkspaceComponent
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.space.ui.SpaceAvatarProvider
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.codereview.timeline.comment.SubmittableTextField
import com.intellij.util.ui.codereview.timeline.comment.SubmittableTextFieldModel
import com.intellij.util.ui.codereview.timeline.comment.SubmittableTextFieldModelBase
import libraries.coroutines.extra.Lifetime
import net.miginfocom.layout.CC
import net.miginfocom.layout.LC
import net.miginfocom.swing.MigLayout
import javax.swing.JComponent
import javax.swing.JPanel
internal fun createNewMessageField(
chatVM: M2ChannelVm,
avatarType: SpaceChatAvatarType,
statsPlace: SpaceStatsCounterCollector.SendMessagePlace,
pendingStateProvider: () -> Boolean = { false },
onCancel: (() -> Unit)? = null,
onSend: (() -> Unit)? = null
): JComponent {
val submittableModel = object : SubmittableTextFieldModelBase("") {
override fun submit() {
val isPending = pendingStateProvider()
SpaceStatsCounterCollector.SEND_MESSAGE.log(statsPlace, isPending)
chatVM.sendMessage(document.text, pending = isPending)
runWriteAction {
document.setText("")
}
onSend?.invoke()
}
}
return SpaceChatNewMessageWithAvatarComponent(chatVM.lifetime, avatarType, submittableModel, statsPlace, onCancel)
}
internal class SpaceChatNewMessageWithAvatarComponent(
avatarProviderLifetime: Lifetime,
avatarType: SpaceChatAvatarType,
submittableModel: SubmittableTextFieldModel,
statsPlace: SpaceStatsCounterCollector.SendMessagePlace,
onCancel: (() -> Unit)?
) : JPanel(null) {
init {
isOpaque = false
layout = MigLayout(LC().gridGap("0", "0")
.insets("0", "0", "0", "0")
.fill()).apply {
columnConstraints = "[][]"
}
val avatarProvider = SpaceAvatarProvider(avatarProviderLifetime, this, avatarType.size)
val avatar = avatarProvider.getIcon(SpaceWorkspaceComponent.getInstance().workspace.value!!.me.value)
val avatarComponent = JBUI.Panels.simplePanel(JBLabel(avatar)).apply {
isOpaque = false
border = JBUI.Borders.emptyRight(SpaceChatItemComponentFactory.Item.AVATAR_GAP)
}
val submittableTextField = SubmittableTextField(
SpaceBundle.message("chat.comment.action.text"),
submittableModel,
onCancel = {
if (
submittableModel.document.text.isBlank() ||
MessageDialogBuilder.yesNo(
SpaceBundle.message("chat.message.new.discard.changes.title"),
SpaceBundle.message("chat.message.new.discard.changes.text")
).ask(this)
) {
SpaceStatsCounterCollector.DISCARD_SEND_MESSAGE.log(statsPlace)
onCancel?.invoke()
}
}
)
add(avatarComponent, CC().pushY())
add(submittableTextField, CC().growX().pushX().alignY("center"))
}
}
@@ -1,258 +0,0 @@
// 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.
package com.intellij.space.chat.ui.discussion
import circlet.code.api.CodeDiscussionRecord
import circlet.code.api.CodeDiscussionSnippet
import circlet.platform.api.Ref
import circlet.platform.client.property
import circlet.platform.client.resolve
import com.intellij.icons.AllIcons
import com.intellij.ide.plugins.newui.HorizontalLayout
import com.intellij.ide.plugins.newui.VerticalLayout
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.project.Project
import com.intellij.space.chat.model.api.SpaceChatItem
import com.intellij.space.chat.ui.SpaceChatAvatarType
import com.intellij.space.chat.ui.SpaceChatEditableComponent
import com.intellij.space.chat.ui.SpaceChatMarkdownTextComponent
import com.intellij.space.chat.ui.SpaceChatNewMessageWithAvatarComponent
import com.intellij.space.chat.ui.message.createResolvedComponent
import com.intellij.space.chat.ui.thread.createCollapsedThreadComponent
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.RoundedLineBorder
import com.intellij.ui.SideBorder
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.panels.NonOpaquePanel
import com.intellij.ui.scale.JBUIScale
import com.intellij.util.PathUtil
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.codereview.InlineIconButton
import com.intellij.util.ui.codereview.SingleValueModel
import com.intellij.util.ui.codereview.SingleValueModelImpl
import com.intellij.util.ui.codereview.ToggleableContainer
import com.intellij.util.ui.codereview.timeline.comment.SubmittableTextFieldModelBase
import com.intellij.util.ui.components.BorderLayoutPanel
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.launch
import net.miginfocom.layout.CC
import net.miginfocom.layout.LC
import net.miginfocom.swing.MigLayout
import runtime.Ui
import runtime.reactive.Property
import runtime.reactive.property.map
import java.awt.event.ActionListener
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.SwingConstants
internal class SpaceChatCodeDiscussionComponentFactory(
private val project: Project,
private val lifetime: Lifetime,
private val server: String,
) {
fun createComponent(message: SpaceChatItem, codeDiscussion: Ref<CodeDiscussionRecord>): JComponent? {
val discussionMessage = message.projectedItem ?: return null
val discussion = codeDiscussion.resolve()
val discussionProperty = codeDiscussion.property()
val resolved = lifetime.map(discussionProperty) { it.resolved }
val collapseButton = InlineIconButton(AllIcons.General.CollapseComponent, AllIcons.General.CollapseComponentHover)
val expandButton = InlineIconButton(AllIcons.General.ExpandComponent, AllIcons.General.ExpandComponentHover)
val filePath = discussion.anchor.filename ?: return null
val fileNameComponent = JBUI.Panels.simplePanel(
createFileNameComponent(filePath, collapseButton, expandButton, resolved)
).apply {
isOpaque = false
border = JBUI.Borders.empty(8)
}
val snippet = discussion.snippet ?: return null
val diffEditorComponent = when (snippet) {
is CodeDiscussionSnippet.PlainSnippet -> {
return null
}
is CodeDiscussionSnippet.InlineDiffSnippet -> createDiffComponent(project, discussion.anchor, snippet)
}.apply {
border = IdeBorderFactory.createBorder(SideBorder.TOP)
}
val snapshotComponent = JPanel(VerticalLayout(0)).apply {
isOpaque = true
background = EditorColorsManager.getInstance().globalScheme.defaultBackground
border = RoundedLineBorder(UIUtil.CONTRAST_BORDER_COLOR, IdeBorderFactory.BORDER_ROUNDNESS)
add(fileNameComponent)
add(diffEditorComponent, VerticalLayout.FILL_HORIZONTAL)
}
val reviewCommentComponent = SpaceChatMarkdownTextComponent(server)
val collapseModel = SingleValueModelImpl(true)
val panel = JPanel(VerticalLayout(JBUIScale.scale(4))).apply {
isOpaque = false
add(snapshotComponent, VerticalLayout.FILL_HORIZONTAL)
add(createEditableReviewCommentComponent(reviewCommentComponent, message, collapseModel), VerticalLayout.FILL_HORIZONTAL)
}
val outerReplyComponent = createOuterReplyComponent(discussionProperty, message)
val threadComponent = createThreadComponent(discussionProperty, message)
val component = JPanel(VerticalLayout(0)).apply {
isOpaque = false
add(panel, VerticalLayout.FILL_HORIZONTAL)
add(threadComponent, VerticalLayout.FILL_HORIZONTAL)
add(outerReplyComponent, VerticalLayout.FILL_HORIZONTAL)
addDiscussionExpandCollapseHandling(
this,
collapseModel,
listOf(outerReplyComponent, threadComponent, reviewCommentComponent, diffEditorComponent),
collapseButton,
expandButton,
resolved
)
}
discussionMessage.forEach(lifetime) {
reviewCommentComponent.setMarkdownText(it.text, it.edited != null)
reviewCommentComponent.repaint()
}
return component
}
private fun createOuterReplyComponent(discussion: Property<CodeDiscussionRecord>, message: SpaceChatItem): JComponent {
val newMessageStateModel = SingleValueModelImpl(false)
val replyAction = ActionLink(SpaceBundle.message("chat.reply.action")) {
newMessageStateModel.value = true
}
val resolveComponent = createResolveComponent(lifetime, message.chat.client, discussion)
val actionsComponent = JPanel(HorizontalLayout(JBUI.scale(5))).apply {
isOpaque = false
border = JBUI.Borders.emptyTop(8)
add(replyAction)
add(resolveComponent)
}
val outerReplyComponent = ToggleableContainer.create(
newMessageStateModel,
mainComponentSupplier = { actionsComponent },
toggleableComponentSupplier = {
val statsPlace = SpaceStatsCounterCollector.SendMessagePlace.FIRST_DISCUSSION_ANSWER
val submittableModel = object : SubmittableTextFieldModelBase("") {
override fun submit() {
val isPending = false
SpaceStatsCounterCollector.SEND_MESSAGE.log(statsPlace, isPending)
launch(lifetime, Ui) {
val thread = message.loadThread(lifetime)
thread?.sendMessage(document.text, pending = isPending)
runWriteAction {
document.setText("")
}
newMessageStateModel.value = false
}
}
}
SpaceChatNewMessageWithAvatarComponent(lifetime, SpaceChatAvatarType.THREAD, submittableModel, statsPlace,
onCancel = {
newMessageStateModel.value = false
})
}
)
message.threadPreview!!.messageCount.forEach(lifetime) { count ->
actionsComponent.isVisible = count == 0
}
return BorderLayoutPanel().apply {
isOpaque = false
addToCenter(outerReplyComponent)
}
}
private fun createThreadComponent(discussionProperty: Property<CodeDiscussionRecord>, message: SpaceChatItem): JComponent {
val threadActionsFactory = SpaceChatDiscussionActionsFactory(
lifetime,
message.chat.client,
discussionProperty
)
return BorderLayoutPanel().apply {
isOpaque = false
addToCenter(createCollapsedThreadComponent(project, lifetime, message, threadActionsFactory, withFirst = false))
}
}
private fun createEditableReviewCommentComponent(
reviewCommentContent: JComponent,
message: SpaceChatItem,
collapseModel: SingleValueModel<Boolean>
): JComponent {
val editableReviewCommentComponent = SpaceChatEditableComponent(project, lifetime, reviewCommentContent, message)
editableReviewCommentComponent.editingModel.addValueUpdatedListener { isEditing ->
if (isEditing) {
collapseModel.value = false
}
}
return editableReviewCommentComponent
}
private fun createFileNameComponent(
filePath: String,
collapseButton: InlineIconButton,
expandButton: InlineIconButton,
resolved: Property<Boolean>
): JComponent {
val name = PathUtil.getFileName(filePath)
val parentPath = PathUtil.getParentPath(filePath)
val nameLabel = JBLabel(name, null, SwingConstants.LEFT).setCopyable(true)
val resolvedLabel = createResolvedComponent(lifetime, resolved)
return NonOpaquePanel(MigLayout(LC().insets("0").gridGap("${JBUI.scale(5)}", "0").fill().noGrid())).apply {
add(nameLabel, CC().minWidth("0").shrinkPrio(9))
if (parentPath.isNotBlank()) {
add(JBLabel(parentPath).apply {
foreground = UIUtil.getContextHelpForeground()
setCopyable(true)
}, CC().minWidth("0").shrinkPrio(10))
}
add(resolvedLabel, CC().hideMode(3).shrinkPrio(0))
add(collapseButton, CC().hideMode(3).shrinkPrio(0))
add(expandButton, CC().hideMode(3).shrinkPrio(0))
}
}
// TODO: Extract it to code-review module
private fun addDiscussionExpandCollapseHandling(
parentComponent: JComponent,
collapseModel: SingleValueModel<Boolean>,
componentsToHide: Collection<JComponent>,
collapseButton: InlineIconButton,
expandButton: InlineIconButton,
resolved: Property<Boolean>
) {
collapseButton.actionListener = ActionListener {
collapseModel.value = true
SpaceStatsCounterCollector.COLLAPSE_DISCUSSION.log(project)
}
expandButton.actionListener = ActionListener {
collapseModel.value = false
SpaceStatsCounterCollector.EXPAND_DISCUSSION.log(project)
}
fun stateUpdated(collapsed: Boolean) {
collapseButton.isVisible = resolved.value && !collapsed
expandButton.isVisible = resolved.value && collapsed
val areComponentsVisible = !resolved.value || !collapsed
componentsToHide.forEach { it.isVisible = areComponentsVisible }
parentComponent.revalidate()
parentComponent.repaint()
}
collapseModel.addValueUpdatedListener {
stateUpdated(it)
}
resolved.forEach(lifetime) {
collapseModel.value = it
}
}
}
@@ -1,143 +0,0 @@
// 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.
package com.intellij.space.chat.ui.discussion
import circlet.code.api.CodeDiscussionAnchor
import circlet.code.api.CodeDiscussionSnippet
import com.intellij.diff.util.DiffDrawUtil
import com.intellij.diff.util.TextDiffType
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.LineNumberConverter
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.space.messages.SpaceBundle
import com.intellij.ui.JBColor
import com.intellij.ui.JBColor.namedColor
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.codereview.timeline.TimelineDiffComponentFactory
import org.jetbrains.annotations.Nls
import runtime.code.DiffLineType
import runtime.code.InlineDiffLine
import runtime.text.TextRange
import runtime.text.end
import java.awt.Color
import javax.swing.JComponent
internal fun createDiffComponent(
project: Project,
anchor: CodeDiscussionAnchor,
snippet: CodeDiscussionSnippet.InlineDiffSnippet
): JComponent {
val lines = snippet.lines
val lineSeparator = "\n"
val text = lines.joinToString(lineSeparator) { line ->
line.text
}
val anchorLine = lines.findAnchorLine(anchor)
val editorFactory = EditorFactory.getInstance()
return TimelineDiffComponentFactory(project, editorFactory).createDiffComponent(text) { editor ->
editor
.addLinesHighlighting(lines, anchorLine, lineSeparator)
.convertLinesNumber(lines)
editor.scrollPane.border = JBUI.Borders.empty()
}
}
private fun EditorEx.convertLinesNumber(lines: List<InlineDiffLine>): EditorEx {
if (lines.any { it.newLineNum != null && it.oldLineNum != null }) {
gutter.setLineNumberConverter(
object : LineNumberConverter {
override fun convert(editor: Editor, lineNumber: Int) = lines[lineNumber - 1].oldLineNum?.plus(1)
override fun getMaxLineNumber(editor: Editor): Int? = lines.mapNotNull { it.oldLineNum }.maxOrNull()
},
object : LineNumberConverter {
override fun convert(editor: Editor, lineNumber: Int) = lines[lineNumber - 1].newLineNum?.plus(1)
override fun getMaxLineNumber(editor: Editor): Int? = lines.mapNotNull { it.newLineNum }.maxOrNull()
}
)
}
else {
gutter.setLineNumberConverter(object : LineNumberConverter {
override fun convert(editor: Editor, lineNumber: Int) = lines[lineNumber - 1].newLineNum?.plus(1)
override fun getMaxLineNumber(editor: Editor): Int? = lines.last().newLineNum
})
}
return this
}
private fun EditorEx.addLinesHighlighting(
lines: List<InlineDiffLine>,
anchorLine: InlineDiffLine?,
@NlsSafe separator: String
): EditorEx {
val prefixLengths = ArrayList<Int>()
prefixLengths.add(0)
for (i in lines.indices) {
prefixLengths.add(prefixLengths[i] + lines[i].text.length + separator.length)
}
lines.forEachIndexed { i, line ->
addFullLineHighlighting(line, i, anchorLine)
val prefixLength = prefixLengths[i]
line.inserts?.let { ranges -> addInlineHighlighting(line, prefixLength, ranges, TextDiffType.INSERTED) }
line.deletes?.let { ranges -> addInlineHighlighting(line, prefixLength, ranges, TextDiffType.DELETED) }
}
return this
}
private fun EditorEx.addFullLineHighlighting(line: InlineDiffLine, lineIndex: Int, anchorLine: InlineDiffLine?) {
if (anchorLine != null && anchorLine == line) {
DiffDrawUtil.createHighlighter(this, lineIndex, lineIndex + 1, AnchorLine, false)
}
else {
val diffType = when (line.type) {
DiffLineType.ADDED -> TextDiffType.INSERTED
DiffLineType.DELETED -> TextDiffType.DELETED
DiffLineType.MODIFIED -> TextDiffType.MODIFIED
DiffLineType.CONFLICT_OLD -> TextDiffType.CONFLICT
DiffLineType.CONFLICT_NEW -> TextDiffType.CONFLICT
DiffLineType.FILTERED_ADDED -> TextDiffType.INSERTED
DiffLineType.FILTERED_DELETED -> TextDiffType.DELETED
DiffLineType.FILTERED_MODIFIED -> TextDiffType.MODIFIED
null -> null
}
val ignored = (line.deletes?.any { range -> !isFullLineRange(line, range) } ?: false) ||
(line.inserts?.any { range -> !isFullLineRange(line, range) } ?: false)
if (diffType != null) {
DiffDrawUtil.createHighlighter(this, lineIndex, lineIndex + 1, diffType, ignored)
}
}
}
private fun isFullLineRange(line: InlineDiffLine, range: TextRange): Boolean = line.text.length == range.length
private fun EditorEx.addInlineHighlighting(line: InlineDiffLine, prefixLength: Int, ranges: List<TextRange>, diffType: TextDiffType) {
ranges.forEach { range ->
if (!isFullLineRange(line, range)) {
DiffDrawUtil.createInlineHighlighter(this, prefixLength + range.start, prefixLength + range.end, diffType)
}
}
}
private fun List<InlineDiffLine>.findAnchorLine(anchor: CodeDiscussionAnchor): InlineDiffLine? =
this.find { line ->
line.oldLineNum != null && anchor.oldLine == line.oldLineNum ||
line.newLineNum != null && anchor.line == line.newLineNum
}
private object AnchorLine : TextDiffType {
@Nls
override fun getName() = SpaceBundle.message("review.diff.type.anchor.name")
override fun getColor(editor: Editor?): Color = namedColor(
"Space.Review.diffAnchorBackground",
JBColor(0xFBF1D1, 0x544B2D)
)
override fun getIgnoredColor(editor: Editor?) = getColor(editor)
override fun getMarkerColor(editor: Editor?) = getColor(editor)
}
@@ -1,17 +0,0 @@
// 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.
package com.intellij.space.chat.ui.discussion
import circlet.code.api.CodeDiscussionRecord
import circlet.platform.client.KCircletClient
import com.intellij.space.chat.ui.thread.SpaceChatThreadActionsFactory
import libraries.coroutines.extra.Lifetime
import runtime.reactive.Property
import javax.swing.JComponent
internal class SpaceChatDiscussionActionsFactory(
private val lifetime: Lifetime,
private val client: KCircletClient,
private val discussion: Property<CodeDiscussionRecord>
) : SpaceChatThreadActionsFactory {
override fun createActionsComponent(): JComponent = createResolveComponent(lifetime, client, discussion)
}
@@ -1,96 +0,0 @@
// 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.
package com.intellij.space.chat.ui.discussion
import circlet.code.api.CodeDiscussionRecord
import circlet.code.codeReview
import circlet.platform.client.KCircletClient
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.ui.components.ActionLink
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.codereview.SingleValueModel
import com.intellij.util.ui.codereview.SingleValueModelImpl
import com.intellij.util.ui.components.BorderLayoutPanel
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.delay
import libraries.coroutines.extra.launch
import runtime.Ui
import runtime.reactive.Property
import javax.swing.JComponent
import javax.swing.JLabel
internal fun createResolveComponent(
lifetime: Lifetime,
client: KCircletClient,
discussion: Property<CodeDiscussionRecord>,
): JComponent {
val resolvingLabel = JLabel(SpaceBundle.message("chat.resolving.action.state")).apply {
foreground = UIUtil.getContextHelpForeground()
}
val reopeningLabel = JLabel(SpaceBundle.message("chat.reopening.action.state")).apply {
foreground = UIUtil.getContextHelpForeground()
}
val resolvingModel = SingleValueModelImpl(ResolvingState.READY)
val resolveReopenLabel = createResolveReopenLabel(lifetime, client, discussion, resolvingModel)
val contentPanel = BorderLayoutPanel().apply {
isOpaque = false
addToCenter(resolveReopenLabel)
}
resolvingModel.addValueUpdatedListener { newState ->
val stateLabel = when (newState) {
ResolvingState.RESOLVING -> resolvingLabel
ResolvingState.REOPENING -> reopeningLabel
ResolvingState.READY -> resolveReopenLabel
}
contentPanel.removeAll()
contentPanel.addToCenter(stateLabel)
contentPanel.revalidate()
contentPanel.repaint()
}
return contentPanel
}
private fun createResolveReopenLabel(
lifetime: Lifetime,
client: KCircletClient,
discussion: Property<CodeDiscussionRecord>,
resolvingModel: SingleValueModel<ResolvingState>
): JComponent {
val reviewService = client.codeReview
fun resolve() {
val currentDiscussion = discussion.value
launch(lifetime, Ui) {
if (!currentDiscussion.resolved) {
SpaceStatsCounterCollector.RESOLVE_DISCUSSION.log()
resolvingModel.value = ResolvingState.RESOLVING
}
else {
SpaceStatsCounterCollector.REOPEN_DISCUSSION.log()
resolvingModel.value = ResolvingState.REOPENING
}
delay(200) // reduce status label blinking
reviewService.resolveCodeDiscussion(currentDiscussion.id, !currentDiscussion.resolved)
resolvingModel.value = ResolvingState.READY
}
}
val label = ActionLink(SpaceBundle.message("chat.resolve.action")) {
resolve()
}
discussion.forEach(lifetime) {
label.text = if (it.resolved) {
SpaceBundle.message("chat.reopen.action")
}
else {
SpaceBundle.message("chat.resolve.action")
}
}
return label
}
private enum class ResolvingState {
RESOLVING,
REOPENING,
READY
}
@@ -1,12 +0,0 @@
// 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.
package com.intellij.space.chat.ui.header
import com.intellij.space.chat.model.api.SpaceChatHeaderDetails
import com.intellij.space.chat.model.impl.SpaceChatReviewHeaderDetails
import libraries.coroutines.extra.Lifetime
import javax.swing.JComponent
internal fun SpaceChatHeaderDetails.createComponent(lifetime: Lifetime): JComponent = when (this) {
is SpaceChatReviewHeaderDetails -> SpaceChatReviewHeaderComponent(lifetime, this)
else -> throw IllegalArgumentException("This chat header type is not supported")
}
@@ -1,87 +0,0 @@
// 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.
package com.intellij.space.chat.ui.header
import circlet.code.api.CodeReviewState
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.text.HtmlBuilder
import com.intellij.space.chat.model.impl.SpaceChatReviewHeaderDetails
import com.intellij.space.chat.ui.SpaceChatAvatarType
import com.intellij.space.chat.ui.getGrayTextHtml
import com.intellij.space.ui.resizeIcon
import com.intellij.space.vcs.review.HtmlEditorPane
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import icons.VcsCodeReviewIcons
import libraries.coroutines.extra.Lifetime
import net.miginfocom.layout.CC
import net.miginfocom.layout.LC
import net.miginfocom.swing.MigLayout
import org.jetbrains.annotations.Nls
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
internal class SpaceChatReviewHeaderComponent(
lifetime: Lifetime,
details: SpaceChatReviewHeaderDetails
) : JPanel() {
companion object {
private const val STATE_ICON_FACTOR: Double = 2.0 / 3
private const val STATE_ICON_GAP_FACTOR: Double = 0.5
}
private val stateIconSize: Int
get() = (SpaceChatAvatarType.MAIN_CHAT.size.get() * STATE_ICON_FACTOR).toInt()
init {
isOpaque = false
layout = MigLayout(LC().gridGap("0", "0")
.insets("0", "0", "0", "0")
.fill()).apply {
columnConstraints = "[][]"
}
val reviewStateIconPanel = BorderLayoutPanel().apply {
isOpaque = false
border = JBUI.Borders.emptyRight((stateIconSize * STATE_ICON_GAP_FACTOR).toInt())
}
val headerContent = HtmlEditorPane().apply {
font = font.deriveFont((font.size * 1.5).toFloat())
}
add(reviewStateIconPanel, CC().pushY())
add(headerContent, CC().pushX().alignY("center"))
details.title.forEach(lifetime) { newTitle ->
headerContent.setBody(getHeaderHtml(newTitle, details.reviewKey))
}
details.state.forEach(lifetime) { newState ->
if (reviewStateIconPanel.components.isNotEmpty()) {
reviewStateIconPanel.remove(0)
}
reviewStateIconPanel.add(getReviewStateIcon(newState), 0)
reviewStateIconPanel.revalidate()
reviewStateIconPanel.repaint()
}
}
private fun getReviewStateIcon(state: CodeReviewState): JComponent {
val icon = when (state) {
CodeReviewState.Opened -> VcsCodeReviewIcons.PullRequestOpen
CodeReviewState.Closed, CodeReviewState.Deleted -> VcsCodeReviewIcons.PullRequestClosed
}
return JLabel(resizeIcon(icon, stateIconSize))
}
@Nls
private fun getHeaderHtml(@Nls title: String, @NlsSafe reviewKey: String?): String {
val builder = HtmlBuilder().append(title)
if (reviewKey != null) {
builder
.nbsp()
.appendRaw(getGrayTextHtml(reviewKey))
}
return builder.toString()
}
}
@@ -1,151 +0,0 @@
// 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.
package com.intellij.space.chat.ui.message
import circlet.client.api.CApplicationPrincipalDetails
import circlet.client.api.CPrincipal
import circlet.client.api.CUserPrincipalDetails
import circlet.platform.client.resolve
import com.intellij.icons.AllIcons
import com.intellij.ide.plugins.newui.HorizontalLayout
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.space.chat.model.api.SpaceChatItem
import com.intellij.space.chat.model.api.SpaceChatItemAdditionalFeature
import com.intellij.space.chat.ui.link
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.space.ui.SpaceAutoUpdatableComponentService
import com.intellij.space.utils.SpaceUrls
import com.intellij.space.utils.formatPrettyDateTime
import com.intellij.space.vcs.review.HtmlEditorPane
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.codereview.InlineIconButton
import icons.VcsCodeReviewIcons
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.delay
import libraries.coroutines.extra.launch
import runtime.Ui
import java.awt.event.ActionListener
import javax.swing.JComponent
import javax.swing.JPanel
internal class MessageTitleComponent(
private val project: Project,
private val lifetime: Lifetime,
message: SpaceChatItem
) : JPanel(HorizontalLayout(JBUI.scale(5))) {
val actionsPanel = JPanel(HorizontalLayout(JBUI.scale(5))).apply {
isOpaque = false
isVisible = false
add(createStartThreadButton(message))
createEditButton(message)?.let { add(it) }
createDeleteButton(message)?.let { add(it) }
}
init {
val authorPanel = HtmlEditorPane().apply {
putClientProperty(UIUtil.HIDE_EDITOR_FROM_DATA_CONTEXT_PROPERTY, true)
setBody(createMessageAuthorChunk(message.author).bold().toString())
}
val timePanel = SpaceAutoUpdatableComponentService.createAutoUpdatableComponent {
HtmlEditorPane().apply {
putClientProperty(UIUtil.HIDE_EDITOR_FROM_DATA_CONTEXT_PROPERTY, true)
foreground = UIUtil.getContextHelpForeground()
setBody(HtmlChunk.text(message.created.formatPrettyDateTime()).toString()) // NON-NLS
}
}
isOpaque = false
add(authorPanel)
add(timePanel)
val showResolvedStateFeature = message.additionalFeatures.filterIsInstance<SpaceChatItemAdditionalFeature.ShowResolvedState>().singleOrNull()
if (showResolvedStateFeature != null) {
add(createResolvedComponent(lifetime, showResolvedStateFeature.resolved))
}
add(actionsPanel)
launch(lifetime, Ui) {
delay(2000)
if (!message.delivered) {
add(JBLabel(AnimatedIcon.Default.INSTANCE))
revalidate()
repaint()
}
}
}
private fun createMessageAuthorChunk(author: CPrincipal): HtmlChunk =
when (val details = author.details) {
is CUserPrincipalDetails -> {
val user = details.user.resolve()
user.link()
}
is CApplicationPrincipalDetails -> {
val app = details.app.resolve()
HtmlChunk.link(SpaceUrls.app(app), app.name) // NON-NLS
}
else -> {
HtmlChunk.text(author.name) // NON-NLS
}
}
private fun createDeleteButton(message: SpaceChatItem): JComponent? {
if (!message.canDelete) {
return null
}
return InlineIconButton(
VcsCodeReviewIcons.Delete,
VcsCodeReviewIcons.DeleteHovered,
tooltip = SpaceBundle.message("chat.message.action.delete.tooltip")
).apply {
actionListener = ActionListener {
if (
MessageDialogBuilder.yesNo(
SpaceBundle.message("chat.message.delete.dialog.title"),
SpaceBundle.message("chat.message.delete.dialog.message")
).ask(this)
) {
SpaceStatsCounterCollector.DELETE_MESSAGE.log(project)
launch(lifetime, Ui) {
message.delete()
}
}
}
}
}
private fun createEditButton(message: SpaceChatItem): JComponent? {
if (!message.canEdit) {
return null
}
return InlineIconButton(
AllIcons.General.Inline_edit,
AllIcons.General.Inline_edit_hovered,
tooltip = SpaceBundle.message("chat.message.action.edit.tooltip")
).apply {
actionListener = ActionListener {
message.startEditing()
}
}
}
private fun createStartThreadButton(message: SpaceChatItem): JComponent {
val startThreadVm = message.startThreadVm
val button = InlineIconButton(
VcsCodeReviewIcons.Comment,
VcsCodeReviewIcons.CommentHovered,
tooltip = SpaceBundle.message("chat.message.action.start.thread.tooltip")
).apply {
actionListener = ActionListener {
startThreadVm.startWritingFirstMessage()
}
}
startThreadVm.canStartThread.forEach(lifetime) {
button.isVisible = it
}
return button
}
}
@@ -1,28 +0,0 @@
// 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.
package com.intellij.space.chat.ui.message
import com.intellij.icons.AllIcons
import com.intellij.space.chat.model.api.SpaceChatItem
import com.intellij.space.messages.SpaceBundle
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.panels.HorizontalLayout
import com.intellij.util.ui.UIUtil
import javax.swing.JPanel
import javax.swing.SwingConstants
internal class SpaceChatMessagePendingHeader(private val item: SpaceChatItem) : JPanel(HorizontalLayout(5)) {
init {
isOpaque = false
isVisible = item.pending == true
val infoLabel = JBLabel(SpaceBundle.message("chat.message.pending.text"), AllIcons.General.Note, SwingConstants.LEFT).apply {
foreground = UIUtil.getContextHelpForeground()
}
val postNowLabel = ActionLink(SpaceBundle.message("chat.message.pending.post.action")) {
item.chat.publishMessage(item.id)
}
add(infoLabel)
add(postNowLabel)
}
}
@@ -1,116 +0,0 @@
// 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.
package com.intellij.space.chat.ui.message
import circlet.client.api.AttachmentInfo
import circlet.client.api.UnfurlAttachment
import com.intellij.ide.plugins.newui.VerticalLayout
import com.intellij.openapi.ui.OnePixelDivider
import com.intellij.openapi.util.NlsSafe
import com.intellij.space.chat.model.api.SpaceMCMessageContent
import com.intellij.space.chat.model.api.SpaceMCMessageElement
import com.intellij.space.chat.model.api.SpaceMCMessageSection
import com.intellij.space.chat.ui.SpaceChatMarkdownTextComponent
import com.intellij.space.chat.ui.getGrayTextHtml
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.SideBorder
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import org.jetbrains.annotations.NonNls
import javax.swing.JComponent
import javax.swing.JPanel
internal class SpaceMCMessageComponent(
@NonNls private val server: String,
message: SpaceMCMessageContent,
attachments: List<AttachmentInfo>
) : BorderLayoutPanel() {
companion object {
private const val SMALL_FONT_SCALING = 0.9
private const val HEADER_FONT_SCALING = 1.2
}
private val unfurls = attachments.mapNotNull { it.details }.filterIsInstance<UnfurlAttachment>()
init {
isOpaque = false
message.outline?.let { outline ->
addToTop(SpaceChatMarkdownTextComponent(server, outline.text, initialUnfurls = unfurls).withSmallFont())
}
val sectionsComponent = message.sections.map { it.createComponent() }.wrapToSingleComponent()
addToCenter(SpaceStyledMessageComponent(sectionsComponent, style = message.style))
}
private fun SpaceMCMessageSection.createComponent(): JComponent =
when (this) {
is SpaceMCMessageSection.Divider -> createDividerPanel()
is SpaceMCMessageSection.Section -> createComponent()
}
private fun SpaceMCMessageSection.Section.createComponent(): JComponent = JPanel(VerticalLayout(JBUI.scale(10))).apply {
isOpaque = false
header?.let { add(createHeaderComponent(it), VerticalLayout.FILL_HORIZONTAL) }
val elementsComponent = elements.map { it.createComponent() }.wrapToSingleComponent()
add(elementsComponent, VerticalLayout.FILL_HORIZONTAL)
footer?.let { add(createFooterComponent(it), VerticalLayout.FILL_HORIZONTAL) }
}
private fun SpaceMCMessageElement.createComponent(): JComponent =
when (this) {
is SpaceMCMessageElement.Text -> createComponent()
is SpaceMCMessageElement.MessageDivider -> createDividerPanel()
}
private fun SpaceMCMessageElement.Text.createComponent(): JComponent {
val textComponent = SpaceChatMarkdownTextComponent(server, content, initialUnfurls = unfurls)
return BorderLayoutPanel().apply {
isOpaque = false
createIconComponent()?.let { iconComponent ->
addToLeft(
BorderLayoutPanel().apply {
isOpaque = false
border = JBUI.Borders.emptyRight(3)
addToTop(iconComponent)
}
)
}
addToCenter(textComponent)
}
}
private fun SpaceMCMessageElement.Text.createIconComponent(): JComponent? = icon?.let { JBLabel(icon) }
private fun createHeaderComponent(@NlsSafe text: String) = SpaceChatMarkdownTextComponent(server, text, initialUnfurls = unfurls).apply {
scaleFont(HEADER_FONT_SCALING)
}
private fun createFooterComponent(@NlsSafe text: String) =
SpaceChatMarkdownTextComponent(server, getGrayTextHtml(text), initialUnfurls = unfurls).withSmallFont()
private fun JComponent.withSmallFont(): JComponent = scaleFont(SMALL_FONT_SCALING)
private fun JComponent.scaleFont(scaling: Double): JComponent = apply {
font = font.deriveFont((font.size * scaling).toFloat())
}
private fun createDividerPanel() = JPanel().apply {
isOpaque = false
border = IdeBorderFactory.createBorder(OnePixelDivider.BACKGROUND, SideBorder.TOP)
}
private fun List<JComponent>.wrapToSingleComponent(): JComponent {
val wrapper = JPanel(VerticalLayout(JBUI.scale(10)))
forEach { component ->
wrapper.add(component, VerticalLayout.FILL_HORIZONTAL)
}
return wrapper.apply {
isOpaque = false
}
}
}
@@ -1,21 +0,0 @@
// 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.
package com.intellij.space.chat.ui.message
import com.intellij.space.messages.SpaceBundle
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.UIUtil
import libraries.coroutines.extra.Lifetime
import runtime.reactive.Property
import javax.swing.JComponent
internal fun createResolvedComponent(lifetime: Lifetime, resolved: Property<Boolean>): JComponent {
val resolvedLabel = JBLabel(SpaceBundle.message("chat.message.resolved.text"), UIUtil.ComponentStyle.SMALL).apply {
foreground = UIUtil.getContextHelpForeground()
background = UIUtil.getPanelBackground()
isOpaque = true
}
resolved.forEach(lifetime) {
resolvedLabel.isVisible = it
}
return resolvedLabel
}
@@ -1,45 +0,0 @@
// 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.
package com.intellij.space.chat.ui.message
import circlet.client.api.mc.MessageStyle
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import java.awt.*
import javax.swing.JComponent
internal class SpaceStyledMessageComponent(content: JComponent, style: MessageStyle = MessageStyle.PRIMARY) : BorderLayoutPanel() {
// TODO: create colors for Darcula
private val lineColor = when (style) {
MessageStyle.PRIMARY -> Color(0xB3D8F8)
MessageStyle.SECONDARY -> Color(0xB5B9BD)
MessageStyle.SUCCESS -> Color(0xC7E3D4)
MessageStyle.ERROR -> Color(0xECC5C6)
MessageStyle.WARNING -> Color(0xF1E4C9)
}
private val lineWidth
get() = JBUI.scale(6)
private val lineCenterX
get() = lineWidth / 2
private val yRoundOffset
get() = lineWidth / 2
init {
addToCenter(content)
isOpaque = false
border = JBUI.Borders.empty(yRoundOffset, 15, yRoundOffset, 0)
}
override fun paint(g: Graphics?) {
super.paint(g)
with(g as Graphics2D) {
setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
color = lineColor
stroke = BasicStroke(lineWidth.toFloat() / 2 + 1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL)
drawLine(lineCenterX, yRoundOffset, lineCenterX, height - yRoundOffset)
}
}
}
@@ -1,182 +0,0 @@
// 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.
package com.intellij.space.chat.ui.thread
import circlet.m2.threads.M2ThreadPreviewVm
import circlet.platform.api.toKDateTime
import circlet.platform.client.resolve
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.space.chat.model.api.SpaceChatItem
import com.intellij.space.chat.ui.SpaceChatAvatarType
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.space.ui.SpaceAutoUpdatableComponentService
import com.intellij.space.ui.SpaceAvatarProvider
import com.intellij.space.utils.formatPrettyDateTime
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.labels.LinkLabel
import com.intellij.ui.components.panels.HorizontalLayout
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.components.BorderLayoutPanel
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.launch
import runtime.Ui
import runtime.reactive.Property
import runtime.reactive.SequentialLifetimes
import javax.swing.JComponent
import javax.swing.JPanel
internal fun createCollapsedThreadComponent(
project: Project,
lifetime: Lifetime,
message: SpaceChatItem,
threadActionsFactory: SpaceChatThreadActionsFactory?,
withFirst: Boolean = true
): JComponent {
val contentPanel = BorderLayoutPanel().apply {
isOpaque = false
border = JBUI.Borders.emptyTop(8)
}
val threadPreview = message.threadPreview
val loadingThreadsSqLifetime = SequentialLifetimes(lifetime)
message.loadingThread.forEach(lifetime) { loadingThread ->
val loadingThreadLifetime = loadingThreadsSqLifetime.next()
contentPanel.removeAll()
if (loadingThread != null) {
if (!loadingThread.isCompleted) {
val loadingThreadLabel = JBLabel(SpaceBundle.message("chat.message.loading.thread.label")).apply {
foreground = UIUtil.getContextHelpForeground()
}
contentPanel.addToCenter(loadingThreadLabel)
contentPanel.revalidate()
contentPanel.repaint()
}
launch(lifetime, Ui) {
val threadValue = loadingThread.await()
val threadComponent = createThreadComponent(
project, lifetime, threadValue,
pendingStateProvider = { false },
threadActionsFactory = threadActionsFactory,
folded = false,
withFirst = withFirst
)
threadValue.mvms.forEach(loadingThreadLifetime) { messages ->
contentPanel.isVisible = messages.messages.size > if (withFirst) 0 else 1
}
contentPanel.removeAll()
contentPanel.addToCenter(threadComponent)
contentPanel.revalidate()
contentPanel.repaint()
}
}
else {
if (threadPreview != null) {
val actionsComponent = createActionsComponent(project, lifetime, message, threadPreview, threadActionsFactory)
contentPanel.addToCenter(actionsComponent)
threadPreview.messageCount.forEach(loadingThreadLifetime) { count ->
contentPanel.isVisible = count > 0
}
}
else {
contentPanel.isVisible = false
}
contentPanel.revalidate()
contentPanel.repaint()
}
}
return contentPanel
}
private fun createActionsComponent(
project: Project,
lifetime: Lifetime,
message: SpaceChatItem,
threadPreview: M2ThreadPreviewVm,
threadActionsFactory: SpaceChatThreadActionsFactory?
): JComponent {
val authorsPanel = createThreadAuthorAvatarsComponent(lifetime, threadPreview)
val dateComponent = createDateComponent(lifetime, threadPreview.lastReplyTime)
val repliesComponent = createRepliesLink(project, lifetime, threadPreview.messageCount) {
launch(lifetime, Ui) {
message.loadThread(lifetime)
}
}
return JPanel(HorizontalLayout(JBUI.scale(5))).apply {
isOpaque = false
add(authorsPanel)
add(repliesComponent)
add(dateComponent)
threadActionsFactory?.createActionsComponent()?.let { add(it) }
}
}
private fun createThreadAuthorAvatarsComponent(lifetime: Lifetime, threadPreview: M2ThreadPreviewVm): JComponent {
val contentPanel = BorderLayoutPanel().apply {
isOpaque = false
}
val avatarsProvider = SpaceAvatarProvider(lifetime, contentPanel, SpaceChatAvatarType.THREAD.size)
threadPreview.authors.forEach(lifetime) { authors ->
contentPanel.removeAll()
if (authors.isNullOrEmpty()) {
contentPanel.border = JBUI.Borders.emptyLeft(10)
}
else {
contentPanel.border = JBUI.Borders.empty()
val icons = authors.take(3).map { avatarsProvider.getIcon(it.resolve()) }
val iconsPanel = JPanel(HorizontalLayout(1)).apply {
isOpaque = false
for (icon in icons) {
add(JBLabel(icon))
}
}
contentPanel.addToCenter(iconsPanel)
contentPanel.revalidate()
contentPanel.repaint()
}
}
return contentPanel
}
private fun createDateComponent(lifetime: Lifetime, lastReplyTime: Property<Long?>): JComponent {
val contentPanel = BorderLayoutPanel().apply {
isOpaque = false
}
lastReplyTime.forEach(lifetime) { time ->
contentPanel.removeAll()
if (time == null) {
contentPanel.isVisible = false
}
else {
contentPanel.isVisible = true
val dateComponent = SpaceAutoUpdatableComponentService.createAutoUpdatableComponent {
JBLabel(HtmlChunk.text(time.toKDateTime().formatPrettyDateTime()).toString()).apply { // NON-NLS
putClientProperty(UIUtil.HIDE_EDITOR_FROM_DATA_CONTEXT_PROPERTY, true)
foreground = UIUtil.getContextHelpForeground()
setCopyable(true)
}
}
contentPanel.addToCenter(dateComponent)
}
contentPanel.revalidate()
contentPanel.repaint()
}
return contentPanel
}
private fun createRepliesLink(project: Project, lifetime: Lifetime, messagesCount: Property<Int>, action: () -> Unit): JComponent {
val contentPanel = BorderLayoutPanel().apply {
isOpaque = false
}
messagesCount.forEach(lifetime) { count ->
val repliesLink = LinkLabel.create(SpaceBundle.message("chat.message.replies.link", count)) {
SpaceStatsCounterCollector.OPEN_THREAD.log(project)
action()
}
contentPanel.removeAll()
contentPanel.addToCenter(repliesLink)
contentPanel.revalidate()
contentPanel.repaint()
}
return contentPanel
}
@@ -1,37 +0,0 @@
// 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.
package com.intellij.space.chat.ui.thread
import circlet.client.api.M2ChannelRecord
import circlet.m2.ChannelsVm
import circlet.m2.M2MessageVm
import circlet.m2.channel.M2ChannelVm
import circlet.platform.api.Ref
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.space.chat.model.api.SpaceChatItem
import com.intellij.space.chat.model.impl.SpaceChatItemImpl.Companion.convertToChatItem
import com.intellij.space.chat.ui.SpaceChatContentPanelBase
import com.intellij.space.chat.ui.getLink
import com.intellij.space.stats.SpaceStatsCounterCollector
import libraries.coroutines.extra.Lifetime
internal class SpaceChatStandaloneThreadComponent(
private val project: Project,
private val lifetime: Lifetime,
parent: Disposable,
channelsVm: ChannelsVm,
chatRecord: Ref<M2ChannelRecord>,
private val pendingStateProvider: () -> Boolean,
private val threadActionsFactory: SpaceChatThreadActionsFactory,
private val statsPlace: SpaceStatsCounterCollector.SendMessagePlace = SpaceStatsCounterCollector.SendMessagePlace.THREAD,
private val messageConverter: (index: Int, message: M2MessageVm) -> SpaceChatItem = { _, message ->
message.convertToChatItem(message.getLink())
}
) : SpaceChatContentPanelBase(lifetime, parent, channelsVm, chatRecord) {
override fun onChatLoad(chatVm: M2ChannelVm) {
stopLoadingContent(
createThreadComponent(project, lifetime, chatVm, pendingStateProvider, threadActionsFactory, statsPlace = statsPlace,
messageConverter = messageConverter)
)
}
}
@@ -1,8 +0,0 @@
// 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.
package com.intellij.space.chat.ui.thread
import javax.swing.JComponent
internal interface SpaceChatThreadActionsFactory {
fun createActionsComponent(): JComponent
}
@@ -1,109 +0,0 @@
// 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.
package com.intellij.space.chat.ui.thread
import circlet.m2.M2MessageVm
import circlet.m2.channel.M2ChannelVm
import com.intellij.ide.plugins.newui.HorizontalLayout
import com.intellij.ide.plugins.newui.VerticalLayout
import com.intellij.openapi.project.Project
import com.intellij.space.chat.model.api.SpaceChatItem
import com.intellij.space.chat.model.impl.SpaceChatItemImpl.Companion.convertToChatItem
import com.intellij.space.chat.ui.*
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.space.ui.SpaceAvatarProvider
import com.intellij.ui.components.ActionLink
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.codereview.SingleValueModelImpl
import com.intellij.util.ui.codereview.ToggleableContainer
import com.intellij.util.ui.codereview.timeline.TimelineComponent
import com.intellij.util.ui.codereview.timeline.thread.TimelineThreadCommentsPanel
import libraries.coroutines.extra.Lifetime
import javax.swing.JComponent
import javax.swing.JPanel
internal fun createThreadComponent(
project: Project,
lifetime: Lifetime,
thread: M2ChannelVm,
pendingStateProvider: () -> Boolean,
threadActionsFactory: SpaceChatThreadActionsFactory? = null,
withFirst: Boolean = true,
folded: Boolean = true,
statsPlace: SpaceStatsCounterCollector.SendMessagePlace = SpaceStatsCounterCollector.SendMessagePlace.THREAD,
messageConverter: (index: Int, message: M2MessageVm) -> SpaceChatItem = { _, message -> message.convertToChatItem(message.getLink()) }
): JComponent {
val threadComponent = JPanel(VerticalLayout(0)).apply {
isOpaque = false
}
val server = thread.client.server
val avatarSize = SpaceChatAvatarType.THREAD.size
val avatarProvider = SpaceAvatarProvider(lifetime, threadComponent, avatarSize)
val itemsListModel = SpaceChatItemListModel()
thread.mvms.forEach(lifetime) { messageList ->
val messages = messageList.messages
itemsListModel.messageListUpdated(
messages
.drop(if (withFirst) 0 else 1)
.mapIndexed { index, message -> messageConverter(index, message) }
)
threadComponent.isVisible = messages.size > if (withFirst) 0 else 1
}
val itemComponentFactory = SpaceChatItemComponentFactory(project, lifetime, server, avatarProvider)
val threadTimeline = if (folded) {
TimelineThreadCommentsPanel(
itemsListModel,
commentComponentFactory = itemComponentFactory::createComponent,
offset = 0
).apply {
border = JBUI.Borders.emptyBottom(5)
}
}
else {
TimelineComponent(itemsListModel, itemComponentFactory, offset = 0).apply {
border = JBUI.Borders.emptyBottom(5)
}
}
val actionsComponent = createActionsComponent(thread, threadActionsFactory, statsPlace, pendingStateProvider)
return threadComponent.apply {
add(threadTimeline, VerticalLayout.FILL_HORIZONTAL)
add(actionsComponent, VerticalLayout.FILL_HORIZONTAL)
}
}
private fun createActionsComponent(
thread: M2ChannelVm,
threadActionsFactory: SpaceChatThreadActionsFactory?,
statsPlace: SpaceStatsCounterCollector.SendMessagePlace,
pendingStateProvider: () -> Boolean
): JComponent {
val newMessageStateModel = SingleValueModelImpl(false)
val label = ActionLink(SpaceBundle.message("chat.reply.action")) {
newMessageStateModel.value = true
}
val additionalActions = threadActionsFactory?.createActionsComponent()
val actionsComponent = JPanel(HorizontalLayout(JBUI.scale(5))).apply {
isOpaque = false
border = JBUI.Borders.emptyLeft(SpaceChatAvatarType.THREAD.size.get() + SpaceChatItemComponentFactory.Item.AVATAR_GAP)
add(label)
additionalActions?.let { add(it) }
}
return ToggleableContainer.create(
newMessageStateModel,
mainComponentSupplier = { actionsComponent },
toggleableComponentSupplier = {
createNewMessageField(
thread,
statsPlace = statsPlace,
onCancel = { newMessageStateModel.value = false },
avatarType = SpaceChatAvatarType.THREAD,
pendingStateProvider = pendingStateProvider
)
}
)
}
@@ -1,66 +0,0 @@
// 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.
package com.intellij.space.components
import circlet.client.api.englishFullName
import circlet.platform.client.ConnectionStatus
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.space.ui.SpaceAvatarUtils
import com.intellij.space.ui.SpaceAvatars
import com.intellij.space.ui.SpaceImageLoader
import kotlinx.coroutines.CancellationException
import libraries.coroutines.extra.LifetimeSource
import libraries.klogging.logger
import runtime.reactive.Property
import runtime.reactive.awaitFirst
import runtime.reactive.filter
import runtime.reactive.property.mapInit
@Service
class SpaceUserAvatarProvider : Disposable {
companion object {
private val LOG = logger<SpaceUserAvatarProvider>()
fun getInstance(): SpaceUserAvatarProvider = service()
}
private val lifetime: LifetimeSource = LifetimeSource()
private val avatarPlaceholders: SpaceAvatars = SpaceAvatars.MainIcon
val avatars: Property<SpaceAvatars> = lifetime.mapInit(SpaceWorkspaceComponent.getInstance().workspace, avatarPlaceholders) { ws ->
ws ?: return@mapInit avatarPlaceholders
val id = ws.me.value.username
val name = ws.me.value.englishFullName()
val avatarTID = ws.me.value.smallAvatar ?: return@mapInit SpaceAvatarUtils.generateAvatars(id, name)
val imageLoader = SpaceImageLoader.getInstance()
// await connected state before trying to load image.
ws.client.connectionStatus.filter { it is ConnectionStatus.Connected }.awaitFirst(ws.lifetime)
try {
LOG.info { "loading user avatar: $avatarTID" }
val loadedImage = imageLoader.loadImageAsync(avatarTID)?.await()
if (loadedImage == null) {
SpaceAvatarUtils.generateAvatars(id, name)
}
else {
SpaceAvatarUtils.createAvatars(loadedImage)
}
}
catch (th: CancellationException) {
throw th
}
catch (e: Exception) {
LOG.error { "user avatar not loaded: $e" }
avatarPlaceholders
}
}
override fun dispose() {
lifetime.terminate()
}
}
@@ -1,262 +0,0 @@
// 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.
package com.intellij.space.components
import circlet.arenas.initCircletArenas
import circlet.client.api.impl.ApiClassesDeserializer
import circlet.client.api.impl.tombstones.registerArenaTombstones
import circlet.code.api.CodeReviewArena
import circlet.code.api.CodeReviewParticipantsArena
import circlet.code.api.ReviewPendingMessageCounterArena
import circlet.common.oauth.IdeaOAuthConfig
import circlet.permissions.FeatureFlagsVmPersistenceKey
import circlet.platform.api.oauth.OAuthTokenResponse
import circlet.platform.api.oauth.toTokenInfo
import circlet.platform.api.serialization.ExtendableSerializationRegistry
import circlet.platform.client.ClientArenaRegistry
import circlet.platform.workspaces.CodeFlowConfig
import circlet.platform.workspaces.WorkspaceConfiguration
import circlet.platform.workspaces.WorkspaceManagerHost
import circlet.workspaces.Workspace
import circlet.workspaces.WorkspaceManager
import com.intellij.ide.browsers.BrowserLauncher
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.Service.Level
import com.intellij.openapi.components.service
import com.intellij.openapi.wm.IdeFrame
import com.intellij.space.auth.SpaceAuthNotifier
import com.intellij.space.auth.startRedirectHandling
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.runtime.ApplicationDispatcher
import com.intellij.space.settings.SpaceLoginState
import com.intellij.space.settings.SpaceServerSettings
import com.intellij.space.settings.SpaceSettings
import com.intellij.space.settings.log
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.space.utils.IdeaPasswordSafePersistence
import com.intellij.space.utils.LifetimedDisposable
import com.intellij.space.utils.LifetimedDisposableImpl
import com.intellij.ui.AppIcon
import com.intellij.util.concurrency.AppExecutorUtil
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.withContext
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.launch
import libraries.coroutines.extra.usingSource
import libraries.klogging.KLogger
import libraries.klogging.assert
import libraries.klogging.logger
import runtime.Ui
import runtime.mutableUiDispatch
import runtime.persistence.InMemoryPersistence
import runtime.persistence.PersistenceConfiguration
import runtime.persistence.PersistenceKey
import runtime.reactive.MutableProperty
import runtime.reactive.Property
import runtime.reactive.SequentialLifetimes
import runtime.reactive.mutableProperty
import runtime.reactive.property.map
import java.awt.Component
import java.net.URI
import java.net.URL
import java.util.concurrent.CancellationException
import javax.swing.JFrame
import javax.swing.SwingUtilities
/**
* The main plugin's component that allows to log in to the Space server or disconnect from it.
* If possible, the component is automatically authorized when the app starts
*/
@Service(Level.APP)
internal class SpaceWorkspaceComponent : WorkspaceManagerHost(), LifetimedDisposable by LifetimedDisposableImpl() {
companion object {
internal fun getInstance(): SpaceWorkspaceComponent = service()
private val LOG: KLogger = logger<SpaceWorkspaceComponent>()
}
private val workspacesLifetimes = SequentialLifetimes(lifetime)
private val manager = mutableProperty<WorkspaceManager?>(null)
val workspace: Property<Workspace?> = map(manager) { wm ->
wm?.workspace?.value
}
val loginState: MutableProperty<SpaceLoginState> = mutableProperty(getInitialState())
init {
initApp()
val wsLifetime = workspacesLifetimes.next()
// sign in automatically on application startup.
launch(wsLifetime, Ui) {
val signInResult = autoSignIn(wsLifetime)
if (signInResult == AutoSignInResult.NOT_AUTHORIZED) {
authFailed()
}
}
workspace.forEach(lifetime) { ws ->
loginState.value = if (ws == null) {
SpaceLoginState.Disconnected(SpaceSettings.getInstance().serverSettings.server)
}
else {
SpaceLoginState.Connected(ws.client.server, ws)
}
}
}
private fun initApp() {
val application = ApplicationManager.getApplication()
mutableUiDispatch = ApplicationDispatcher(this, application)
initCircletArenas()
registerArenaTombstones(ExtendableSerializationRegistry.global)
ApiClassesDeserializer(ExtendableSerializationRegistry.global).registerDeserializers()
// code review
ClientArenaRegistry.register(CodeReviewArena)
ClientArenaRegistry.register(CodeReviewParticipantsArena)
ClientArenaRegistry.register(ReviewPendingMessageCounterArena)
circlet.code.api.impl.ApiClassesDeserializer(ExtendableSerializationRegistry.global).registerDeserializers()
}
override suspend fun authFailed() {
signOut(SpaceStatsCounterCollector.LogoutPlace.AUTH_FAIL)
SpaceAuthNotifier.authFailed()
}
private suspend fun signIn(lifetime: Lifetime, server: String): OAuthTokenResponse {
LOG.assert(manager.value == null, "manager.value == null")
val lt = workspacesLifetimes.next()
val newManager = createWorkspaceManager(lt, server)
val portsMapping: Map<Int, URL> = IdeaOAuthConfig.redirectURIs.map { rawUri -> URL(rawUri).let { url -> url.port to url } }.toMap()
val ports = portsMapping.keys
val (port, redirectUrl) = startRedirectHandling(lifetime, server, ports)
?: return OAuthTokenResponse.Error(server, "", SpaceBundle.message("auth.error.ports.busy.label"))
val authUrl = portsMapping.getValue(port)
val codeFlow = CodeFlowConfig(newManager.wsConfig, authUrl.toExternalForm())
val uri = URI(codeFlow.codeFlowURL())
try {
BrowserLauncher.instance.browse(uri)
}
catch (th: Throwable) {
return OAuthTokenResponse.Error(server, "", SpaceBundle.message("auth.error.cant.open.browser.label", server))
}
val response = withContext(AppExecutorUtil.getAppExecutorService().asCoroutineDispatcher()) {
codeFlow.handleCodeFlowRedirect(redirectUrl.await())
}
if (response is OAuthTokenResponse.Success) {
LOG.info { "A personal token was received" }
newManager.signInWithToken(response.toTokenInfo())
SpaceSettings.getInstance().serverSettings = SpaceServerSettings(true, server)
manager.value = newManager
}
return response
}
fun signInManually(server:String, uiLifetime: Lifetime, component: Component) {
var serverName = server
if (serverName.isBlank()) {
val error = SpaceBundle.message("login.panel.error.organization.url.should.not.be.empty")
loginState.value = SpaceLoginState.Disconnected("", error)
return
}
serverName = normalizeUrl(serverName)
launch(uiLifetime, Ui) {
uiLifetime.usingSource { connectLt ->
try {
loginState.value = SpaceLoginState.Connecting(serverName) {
connectLt.terminate()
loginState.value = SpaceLoginState.Disconnected(serverName)
}
when (val response = signIn(connectLt, serverName)) {
is OAuthTokenResponse.Error -> {
val error = response.description // NON-NLS
loginState.value = SpaceLoginState.Disconnected(serverName, error)
}
}
}
catch (th: CancellationException) {
throw th
}
catch (th: Throwable) {
log.error(th)
loginState.value = SpaceLoginState.Disconnected(
serverName,
th.message ?: SpaceBundle.message("auth.error.unknown.label", th.javaClass.simpleName)
)
}
val frame = SwingUtilities.getAncestorOfClass(JFrame::class.java, component)
AppIcon.getInstance().requestFocus(frame as IdeFrame?)
}
}
}
private fun normalizeUrl(serverName: String): String {
var result = serverName
result = if (result.startsWith("https://") || result.startsWith("http://")) result else "https://$result"
result = result.removeSuffix("/")
return result
}
fun signOut(statsPlace: SpaceStatsCounterCollector.LogoutPlace) {
SpaceStatsCounterCollector.LOG_OUT.log(statsPlace)
val oldManager = manager.value
oldManager?.signOut(true)
workspacesLifetimes.clear()
manager.value = null
val settings = SpaceSettings.getInstance()
settings.serverSettings = settings.serverSettings.copy(enabled = false)
}
private suspend fun autoSignIn(wsLifetime: Lifetime): AutoSignInResult {
val serverSettings = SpaceSettings.getInstance().serverSettings
val server = serverSettings.server
if (!serverSettings.enabled || server.isBlank()) {
return AutoSignInResult.NOT_AUTHORIZED_BEFORE
}
val newManager = createWorkspaceManager(wsLifetime, server)
return try {
if (newManager.signInNonInteractive()) {
manager.value = newManager
AutoSignInResult.AUTHORIZED
}
else {
AutoSignInResult.NOT_AUTHORIZED
}
}
catch (th: Throwable) {
LOG.info(th, "Couldn't authorize interactively")
AutoSignInResult.NOT_AUTHORIZED
}
}
private fun createWorkspaceManager(lifetime: Lifetime, server: String): WorkspaceManager {
val persistenceConfig = PersistenceConfiguration(
FeatureFlagsVmPersistenceKey,
PersistenceKey.AllArenas
)
val workspaceConfig = WorkspaceConfiguration(server, IdeaOAuthConfig.clientId, IdeaOAuthConfig.clientSecret)
return WorkspaceManager(lifetime, null, this, InMemoryPersistence(), IdeaPasswordSafePersistence, persistenceConfig, workspaceConfig)
}
private fun getInitialState(): SpaceLoginState {
val workspace = workspace.value ?: return SpaceLoginState.Disconnected("")
return SpaceLoginState.Connected(workspace.client.server, workspace)
}
private enum class AutoSignInResult {
NOT_AUTHORIZED_BEFORE,
NOT_AUTHORIZED,
AUTHORIZED
}
}
@@ -1,35 +0,0 @@
// 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.
package com.intellij.space.editor
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.vcs.editor.ComplexPathVirtualFileSystem
import com.intellij.vcs.editor.GsonComplexPathSerializer
internal class SpaceChatComplexPathVirtualFileSystem : ComplexPathVirtualFileSystem<SpaceChatComplexPathVirtualFileSystem.ComplexPath>(
GsonComplexPathSerializer(ComplexPath::class.java)
) {
fun getPath(sessionId: String, projectHash: String, id: String): String {
return getPath(ComplexPath(sessionId, projectHash, id))
}
data class ComplexPath(override val sessionId: String,
override val projectHash: String,
val id: String) : ComplexPathVirtualFileSystem.ComplexPath
override fun getProtocol(): String = PROTOCOL
override fun findOrCreateFile(project: Project, path: ComplexPath): VirtualFile? {
return project.service<SpaceVirtualFilesManager>().findChatFile(path.id)
}
companion object {
private const val PROTOCOL = "space-chat"
@JvmStatic
fun getInstance() = service<VirtualFileManager>().getFileSystem(PROTOCOL) as SpaceChatComplexPathVirtualFileSystem
}
}
@@ -1,36 +0,0 @@
// 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.
package com.intellij.space.editor
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.space.vcs.review.details.diff.SpaceDiffFileId
import com.intellij.vcs.editor.ComplexPathVirtualFileSystem
import com.intellij.vcs.editor.GsonComplexPathSerializer
internal class SpaceDiffComplexPathVirtualFileSystem : ComplexPathVirtualFileSystem<SpaceDiffComplexPathVirtualFileSystem.ComplexPath>(
GsonComplexPathSerializer(ComplexPath::class.java)
) {
fun getPath(sessionId: String, projectHash: String, fileId: SpaceDiffFileId): String {
return getPath(ComplexPath(sessionId, projectHash, fileId))
}
data class ComplexPath(override val sessionId: String,
override val projectHash: String,
val fileId: SpaceDiffFileId) : ComplexPathVirtualFileSystem.ComplexPath
override fun getProtocol(): String = PROTOCOL
override fun findOrCreateFile(project: Project, path: ComplexPath): VirtualFile? {
return project.service<SpaceVirtualFilesManager>().findDiffFile(path.fileId)
}
companion object {
private const val PROTOCOL = "space-diff"
@JvmStatic
fun getInstance() = service<VirtualFileManager>().getFileSystem(PROTOCOL) as SpaceDiffComplexPathVirtualFileSystem
}
}
@@ -1,63 +0,0 @@
// 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.
package com.intellij.space.editor
import circlet.client.api.M2ChannelRecord
import circlet.m2.ChannelsVm
import circlet.platform.api.Ref
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.NlsSafe
import com.intellij.space.chat.editor.SpaceChatFile
import com.intellij.space.chat.model.api.SpaceChatHeaderDetails
import com.intellij.space.vcs.review.details.SpaceReviewChangesVm
import com.intellij.space.vcs.review.details.diff.SpaceDiffFile
import com.intellij.space.vcs.review.details.diff.SpaceDiffFileData
import com.intellij.space.vcs.review.details.diff.SpaceDiffFileId
import com.intellij.space.vcs.review.details.diff.SpaceDiffVm
import com.intellij.util.containers.ContainerUtil.createWeakValueMap
import libraries.coroutines.extra.Lifetime
@Service
internal class SpaceVirtualFilesManager(private val project: Project) : Disposable {
// current time should be enough to distinguish the manager between launches
private val sessionId = System.currentTimeMillis().toString()
private val chatFiles = createWeakValueMap<String, SpaceChatFile>()
private val diffFiles = createWeakValueMap<SpaceDiffFileId, SpaceDiffFile>()
fun findChatFile(id: String) = chatFiles[id]
fun findDiffFile(fileId: SpaceDiffFileId) = diffFiles[fileId]
fun findOrCreateChatFile(
@NlsSafe id: String,
@NlsSafe path: String,
@NlsContexts.TabTitle displayName: String,
@NlsContexts.Tooltip tabTooltip: String,
channelsVm: ChannelsVm,
chatRecord: Ref<M2ChannelRecord>,
headerDetailsBuilder: (Lifetime) -> SpaceChatHeaderDetails
): SpaceChatFile = chatFiles.getOrPut(id) {
SpaceChatFile(sessionId, project.locationHash, id, path, displayName, tabTooltip, channelsVm, chatRecord, headerDetailsBuilder)
}
fun findOrCreateDiffFile(
changesVm: runtime.reactive.Property<SpaceReviewChangesVm>,
diffVm: SpaceDiffVm
): SpaceDiffFile = SpaceDiffFileId(diffVm.projectKey.key, diffVm.reviewKey, diffVm.reviewId).let { fileId ->
diffFiles.getOrPut(fileId) { SpaceDiffFile(sessionId, project.locationHash, fileId, diffVm.reviewKey) }
.apply { updateDiffPresentation(changesVm, diffVm) }
}
fun updateDiffPresentation(changesVm: runtime.reactive.Property<SpaceReviewChangesVm>,
diffVm: SpaceDiffVm) {
val fileId = SpaceDiffFileId(diffVm.projectKey.key, diffVm.reviewKey, diffVm.reviewId)
diffFiles[fileId]?.updateDiffFileData(SpaceDiffFileData(changesVm, diffVm))
}
override fun dispose() {
chatFiles.clear()
diffFiles.clear()
}
}
@@ -1,56 +0,0 @@
// 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.
package com.intellij.space.klogging.impl
import com.intellij.openapi.diagnostic.Logger
import libraries.klogging.BaseLogger
class ApplicationLogger(private val logger: Logger) : BaseLogger {
override val isTraceEnabled: Boolean get() = logger.isTraceEnabled
override val isDebugEnabled: Boolean get() = logger.isDebugEnabled
override val isInfoEnabled: Boolean get() = true
override val isWarnEnabled: Boolean get() = true
override val isErrorEnabled: Boolean get() = true
override fun trace(message: Any?) {
logger.trace(message.toString())
}
override fun debug(message: Any?) {
logger.debug(message.toString())
}
override fun info(message: Any?) {
logger.info(message.toString())
}
override fun warn(message: Any?) {
logger.warn(message.toString())
}
override fun trace(t: Throwable, message: Any?) {
logger.trace(message.toString())
logger.trace(t)
}
override fun debug(t: Throwable, message: Any?) {
logger.debug(message.toString(), t)
}
override fun info(t: Throwable, message: Any?) {
logger.info(message.toString(), t)
}
override fun warn(t: Throwable, message: Any?) {
logger.warn(message.toString(), t)
}
override fun error(message: Any?) {
logger.error(message.toString())
}
override fun error(t: Throwable, message: Any?) {
logger.error(message.toString(), t)
}
}
@@ -1,23 +0,0 @@
// 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.
package com.intellij.space.klogging.impl
import com.intellij.openapi.diagnostic.Logger
import libraries.klogging.KLogger
import libraries.klogging.KLoggerFactory
import libraries.klogging.LoggerNameSource
import libraries.klogging.loggerNameFromSource
import kotlin.reflect.KClass
object KLoggerFactoryIdea : KLoggerFactory {
override fun logger(owner: KClass<*>): KLogger = wrapLogger(Logger.getInstance(owner.java))
override fun logger(owner: Any): KLogger = logger(owner.javaClass.kotlin)
override fun logger(name: String): KLogger = wrapLogger(Logger.getInstance(name))
override fun logger(nameSource: LoggerNameSource): KLogger = logger(loggerNameFromSource(nameSource))
private fun wrapLogger(logger: Logger) = KLogger(wrapWithApplicationLogger(logger))
fun wrapWithApplicationLogger(logger: Logger): ApplicationLogger = ApplicationLogger(logger)
}
@@ -1,30 +0,0 @@
// 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.
package com.intellij.space.messages;
import com.intellij.DynamicBundle;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.PropertyKey;
import java.util.function.Supplier;
public class SpaceBundle extends DynamicBundle {
@NonNls public static final String BUNDLE = "messages.SpaceBundle";
private static final SpaceBundle INSTANCE = new SpaceBundle();
private SpaceBundle() {
super(BUNDLE);
}
@NotNull
public static @Nls String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) {
return INSTANCE.getMessage(key, params);
}
@NotNull
public static Supplier<@Nls String> messagePointer(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key,
Object @NotNull ... params) {
return INSTANCE.getLazyMessage(key, params);
}
}
@@ -1,11 +0,0 @@
// 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.
package com.intellij.space.messages
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.PropertyKey
internal object SpaceBundleExtensions {
fun messagePointer(@PropertyKey(resourceBundle = SpaceBundle.BUNDLE) key: String, vararg params: Any): () -> @Nls String = {
SpaceBundle.message(key, *params)
}
}
@@ -1,20 +0,0 @@
// 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.
package com.intellij.space.notification
import com.intellij.notification.impl.NotificationIdsHolder
class SpaceNotificationIdsHolder : NotificationIdsHolder {
override fun getNotificationIds(): List<String> {
return listOf(
GIT_REPO_INIT_ERROR,
PROJECT_SHARED_SUCCESSFULLY,
SHARING_NOT_FINISHED
)
}
companion object {
const val GIT_REPO_INIT_ERROR = "space.git.repo.init.error"
const val PROJECT_SHARED_SUCCESSFULLY = "space.project.shared.successfully"
const val SHARING_NOT_FINISHED = "space.sharing.not.finished"
}
}
@@ -1,50 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services
import com.intellij.build.DefaultBuildDescriptor
import com.intellij.build.events.BuildEvent
import com.intellij.build.events.MessageEvent
import com.intellij.build.events.impl.FinishBuildEventImpl
import com.intellij.build.events.impl.MessageEventImpl
import com.intellij.build.events.impl.StartBuildEventImpl
import com.intellij.build.events.impl.SuccessResultImpl
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.project.Project
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.plugins.pipelines.viewmodel.LogData
import runtime.reactive.ObservableList
fun publishBuildLog(project: Project, data: LogData) {
if (!ApplicationManager.getApplication().isUnitTestMode && !ApplicationManager.getApplication().isHeadlessEnvironment) {
val descriptor = DefaultBuildDescriptor(data.buildId, SpaceBundle.message("build.log.title"), project.basePath!!,
System.currentTimeMillis())
val view = ServiceManager.getService(project, SyncDslViewManager::class.java)
view.onEvent(data.buildId, StartBuildEventImpl(descriptor, SpaceBundle.message("build.log.message.started", project.name)))
view.onEvent(data.buildId, MessageEventImpl(data.buildId, MessageEvent.Kind.INFO, null,
SpaceBundle.message("build.log.message.process"), null))
if (!data.lifetime.isTerminated) {
data.messages.change.forEach(data.lifetime) { change ->
when (change) {
is ObservableList.Change.Add<BuildEvent> -> {
val message = change.newValue
view.onEvent(data.buildId, message)
}
else -> {
}
}
}
}
data.lifetime.addOrCallImmediately {
view.onEvent(data.buildId,
FinishBuildEventImpl(descriptor.id, null, System.currentTimeMillis(), SpaceBundle.message("build.log.message.finished"),
SuccessResultImpl(false)))
}
}
}
@@ -1,67 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services
import circlet.automation.bootstrap.AutomationDslEvaluationBootstrap
import circlet.pipelines.config.idea.api.IdeaScriptConfig
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.getProjectCachePath
import com.intellij.util.io.safeOutputStream
import libraries.klogging.logger
import java.io.IOException
import java.nio.file.FileSystemException
import java.nio.file.Files
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.nio.file.attribute.BasicFileAttributes
private val log = logger<ScriptKtsPersistentState>()
class ScriptKtsPersistentState(val project: Project) {
// not for load failure
fun load(): IdeaScriptConfig? {
val path = getCacheFile(project)
val fileAttributes = try {
Files.readAttributes(path, BasicFileAttributes::class.java)
}
catch (ignored: FileSystemException) {
return null
}
if (fileAttributes == null || !fileAttributes.isRegularFile) {
return null
}
return try {
Files.newBufferedReader(path, Charsets.UTF_8).use { reader ->
val evalService = AutomationDslEvaluationBootstrap(log, getAutomationConfigurationWithFallback()).loadEvaluatorForIdea()
if (evalService == null) {
log.error("DSL evaluation service not found, cannot deserialize automation DSL model")
return null
}
evalService.deserializeJsonConfig(reader.readLine())
}
}
catch (e: NoSuchFileException) {
null
}
catch (e: IOException) {
log.error(e)
null
}
}
private fun getCacheFile(project: Project): Path {
return project.getProjectCachePath("space_automation").resolve(".space.kts.dat")
}
fun save(config: IdeaScriptConfig) {
val path = getCacheFile(project)
path.safeOutputStream().use {
it.write(config.printJson().toByteArray(Charsets.UTF_8))
}
}
}
@@ -1,61 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services
import circlet.pipelines.config.dsl.scriptdefinition.ProjectScriptCompilationConfiguration
import circlet.pipelines.config.dsl.scriptdefinition.resolveScriptArtifact
import circlet.pipelines.config.ivy.resolver.ivyResolver
import com.intellij.openapi.diagnostic.logger
import com.intellij.space.messages.SpaceBundle
import kotlin.script.experimental.api.*
import kotlin.script.experimental.jvm.updateClasspath
internal class SpaceKtsCompilationConfiguration : ScriptCompilationConfiguration(ProjectScriptCompilationConfiguration(), body = {
refineConfiguration {
beforeCompiling { context ->
addAutomationRuntimeToClasspath(context)
}
}
ide {
acceptedLocations(ScriptAcceptedLocation.Everywhere)
}
}) {
companion object {
private val LOG = logger<SpaceKtsCompilationConfiguration>()
private val AUTH_ERROR = ResultWithDiagnostics.Failure(
ScriptDiagnostic(4, SpaceBundle.message("kts.file.login.error"))
)
private val DEPENDENCIES_LOADING_ERROR = ResultWithDiagnostics.Failure(
ScriptDiagnostic(4, SpaceBundle.message("kts.file.dependencies.error"))
)
private fun addAutomationRuntimeToClasspath(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> {
val config = getAutomationConfigurationOrNull()
if (config == null) {
LOG.debug("[space-kts] User not logged in to Space, cannot fetch Automation runtime")
return AUTH_ERROR
}
try {
LOG.debug("[space-kts] Resolving space-automation-runtime from ${config.locationDescription}")
return config.ivyResolver
.resolveScriptArtifact("com.jetbrains", "space-automation-runtime", "latest.integration")
.onSuccess { jars ->
LOG.debug("[space-kts] Successfully resolved space-automation-runtime: ${jars.joinToString()}")
ScriptCompilationConfiguration(context.compilationConfiguration) { updateClasspath(jars) }.asSuccess()
}
.onFailure { diagnostics ->
LOG.warn("[space-kts] Failure while downloading dependencies for .space.kts file: $diagnostics")
DEPENDENCIES_LOADING_ERROR
}
}
catch (th: Throwable) {
LOG.warn("[space-kts] Couldn't download dependencies for .space.kts file", th)
return DEPENDENCIES_LOADING_ERROR
}
}
}
}
@@ -1,90 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services
import com.intellij.space.plugins.pipelines.utils.DslFileFinder
import com.intellij.space.tools.spaceKtsToolwindow
import com.intellij.space.utils.LifetimedDisposable
import com.intellij.space.utils.LifetimedDisposableImpl
import com.intellij.space.vcs.PostStartupActivity
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.*
import libraries.klogging.logger
import runtime.reactive.Property
import runtime.reactive.mutableProperty
private val log = logger<SpaceKtsFileDetector>()
class SpaceKtsFileDetectorActivator : PostStartupActivity() {
override fun runActivity(project: Project) {
project.service<SpaceKtsFileDetector>()
}
}
// listens to file system and exposes script dsl file
@Service
class SpaceKtsFileDetector(val project: Project) : LifetimedDisposable by LifetimedDisposableImpl() {
val dslFile: Property<VirtualFile?> get() = _dslFile
private val _dslFile = mutableProperty<VirtualFile?>(null)
private val fileListener = object : VirtualFileListener {
override fun propertyChanged(event: VirtualFilePropertyEvent) {
if (event.propertyName == "name") {
handleFileChanged(event.oldValue.toString())
handleFileChanged(event.newValue.toString())
}
}
override fun fileCreated(event: VirtualFileEvent) {
handleFileChanged(event.fileName)
}
override fun fileDeleted(event: VirtualFileEvent) {
handleFileChanged(event.fileName)
}
override fun fileMoved(event: VirtualFileMoveEvent) {
handleFileChanged(event.fileName)
}
}
init {
log.info("SpaceKtsFileDetector")
refreshScript()
LocalFileSystem.getInstance().addVirtualFileListener(fileListener)
lifetime.add {
LocalFileSystem.getInstance().removeVirtualFileListener(fileListener)
}
_dslFile.forEach(lifetime) { file ->
project.spaceKtsToolwindow?.setAvailable(file != null, null)
}
}
fun refreshScript() {
runReadAction {
if (!project.isDisposed) {
_dslFile.value = checkIsDslFileExists()
}
}
}
fun handleFileChanged(name: String) {
if (DslFileFinder.checkFileNameIsApplicable(name)) {
refreshScript()
}
}
private fun checkIsDslFileExists(): VirtualFile? {
return DslFileFinder.find(project)
}
}
@@ -1,247 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services
import circlet.automation.bootstrap.AutomationCompilerBootstrap
import circlet.automation.bootstrap.AutomationDslEvaluationBootstrap
import circlet.automation.bootstrap.embeddedMavenServer
import circlet.automation.bootstrap.publicMavenServer
import circlet.pipelines.config.idea.api.IdeaScriptConfig
import circlet.pipelines.config.utils.AutomationCompilerConfiguration
import circlet.platform.client.backgroundDispatcher
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.invokeAndWaitIfNeeded
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.space.components.SpaceWorkspaceComponent
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.plugins.pipelines.utils.ObservableQueue
import com.intellij.space.plugins.pipelines.viewmodel.LogData
import com.intellij.space.plugins.pipelines.viewmodel.ScriptModel
import com.intellij.space.plugins.pipelines.viewmodel.ScriptState
import com.intellij.space.utils.LifetimedDisposable
import com.intellij.space.utils.LifetimedDisposableImpl
import kotlinx.coroutines.withContext
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.launch
import libraries.coroutines.extra.using
import libraries.klogging.*
import org.slf4j.event.Level
import org.slf4j.event.SubstituteLoggingEvent
import org.slf4j.helpers.SubstituteLogger
import runtime.Ui
import runtime.reactive.*
import runtime.reactive.property.flatMap
import runtime.reactive.property.map
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.io.path.name
private val log = logger<SpaceKtsModelBuilder>()
@Service
class SpaceKtsModelBuilder(val project: Project) : LifetimedDisposable by LifetimedDisposableImpl(), KLogging() {
private val persistentState = ScriptKtsPersistentState(project)
private val _model = mutableProperty<ScriptModelHolder?>(null)
private val _modelBuildIsRequested = mutableProperty(false)
// script is null when no build file is found in project
@Suppress("USELESS_CAST")
val script = map(_model) { it as ScriptModel? }
val config = flatMap(_model) { (it?.config ?: mutableProperty(null)) }
init {
launch(lifetime, backgroundDispatcher) {
val saved = log.catch { persistentState.load() }
withContext(Ui) {
project.service<SpaceKtsFileDetector>().dslFile.view(lifetime) { lt, file ->
_model.value = file?.let { ScriptModelHolder(lt, file, _modelBuildIsRequested, saved) }
}
config.skip(1).view(lifetime) { lt, config ->
if (config != null) {
launch(lt, backgroundDispatcher) {
persistentState.save(config)
}
}
}
}
}
}
fun requestModel() {
_modelBuildIsRequested.value = true
}
fun rebuildModel() {
_model.value?.rebuildModel()
}
// this class is created per automation script file found and it is responsible for building model content
private inner class ScriptModelHolder(val lifetime: Lifetime,
val scriptFile: VirtualFile,
modelBuildIsRequested: Property<Boolean>,
loadedConfig: IdeaScriptConfig?) : ScriptModel {
val sync = Any()
private val _config = mutableProperty(loadedConfig)
private val _error = mutableProperty<String?>(null)
private val _state = mutableProperty(if (loadedConfig == null) ScriptState.NotInitialised else ScriptState.Ready)
override val config: Property<IdeaScriptConfig?> get() = _config
override val error: Property<String?> get() = _error
override val state: Property<ScriptState> get() = _state
private val logBuildData = mutableProperty<LogData?>(null)
init {
// push to IDE build log
logBuildData.view(lifetime) { lt, log ->
if (log != null) {
publishBuildLog(project, log)
}
}
modelBuildIsRequested.whenTrue(lifetime) { lt ->
if (_state.value == ScriptState.NotInitialised)
rebuildModel()
}
}
fun rebuildModel() {
ProgressManager.getInstance().run(object : Task.Backgroundable(
project,
SpaceBundle.message("kts.progress.title.building.dsl.model"),
false
) {
override fun run(pi: ProgressIndicator) {
// todo: implement build cancellation and re-building
synchronized(sync) {
if (_state.value == ScriptState.Building)
return
_state.value = ScriptState.Building
}
val data = LogData()
try {
logBuildData.value = data
build(data)
}
finally {
data.close()
_state.value = ScriptState.Ready
}
}
})
}
private fun build(logData: LogData) {
lifetime.using { lt ->
invokeAndWaitIfNeeded(ModalityState.NON_MODAL) {
// We need to save the automation script to disk before compiling, to take into account the most recent changes.
// The script might have dependencies on other files in the project, so we also save other files.
FileDocumentManager.getInstance().saveAllDocuments()
}
compileAndEvaluate(logData.asLogger(lt), logData)
}
}
private fun compileAndEvaluate(eventLogger: KLogger, logData: LogData) {
val outputDir = createTempDir("space-automation-temp")
try {
val outputDirPath = outputDir.toPath()
val compiledJarPath = outputDirPath.resolve("compiledJar.jar")
val scriptRuntimePath = outputDirPath.resolve("space-automation-runtime.jar")
val configuration = getAutomationConfigurationWithFallback()
val compiler = AutomationCompilerBootstrap(log = eventLogger, configuration = configuration)
val compileResultCode = compiler.compile(script = Paths.get(scriptFile.path), jar = compiledJarPath)
if (compileResultCode != 0) {
_config.value = null
_error.value = SpaceBundle.message("kts.toolwindow.error.compilation.failed.code", compileResultCode)
return
}
if (!Files.isRegularFile(compiledJarPath)) {
_config.value = null
_error.value = SpaceBundle.message("kts.toolwindow.error.missing.compiled.jar", compiledJarPath.name)
return
}
// See AutomationCompiler.kt (in space project) where we copy the runtime jar into the output folder for all resolver types
if (!Files.exists(scriptRuntimePath)) {
_config.value = null
_error.value = SpaceBundle.message("kts.toolwindow.error.missing.runtime.jar", scriptRuntimePath.name)
return
}
val evaluator = AutomationDslEvaluationBootstrap(log = eventLogger, configuration = configuration).loadEvaluatorForIdea()
if (evaluator == null) {
_config.value = null
_error.value = SpaceBundle.message("kts.toolwindow.error.evaluation.service.not.found")
return
}
val evalResult = evaluator.evaluateAndValidate(compiledJarPath, scriptRuntimePath)
if (evalResult.validationErrors.any()) {
val validationErrorsPrefix = SpaceBundle.message("kts.toolwindow.validation.errors.prefix")
val message = evalResult.validationErrors.joinToString("\n", prefix = "$validationErrorsPrefix\n")
logData.error(message)
_config.value = null
_error.value = message
return
}
_config.value = evalResult.config
_error.value = null
}
catch (th: Throwable) {
val stackTraceText = th.stackTraceToString()
val logMessage = "${th.message}.\n$stackTraceText"
logData.error(logMessage)
// do not touch last config, just update the error state.
_error.value = stackTraceText
}
finally {
outputDir.deleteRecursively()
}
}
}
}
internal fun getAutomationConfigurationOrNull(): AutomationCompilerConfiguration? =
SpaceWorkspaceComponent.getInstance().workspace.value?.client?.server?.let {
AutomationCompilerConfiguration.Remote(server = embeddedMavenServer(it))
}
// Primary option is to download from currently connected server, fallback to the public maven
internal fun getAutomationConfigurationWithFallback(): AutomationCompilerConfiguration =
getAutomationConfigurationOrNull() ?: AutomationCompilerConfiguration.Remote(server = publicMavenServer)
private fun LogData.asLogger(lifetime: Lifetime): KLogger {
val queue = ObservableQueue.mutable<SubstituteLoggingEvent>()
queue.change.forEach(lifetime) {
val ev = it.index
when (ev.level) {
Level.INFO -> message(ev.message)
Level.DEBUG -> message(ev.message)
Level.WARN -> message(ev.message)
Level.ERROR -> error(ev.message)
Level.TRACE -> {
}
}
}
// slf4j's SubstituteLogger mechanism allows us to put logs as events in a queue (even though not initially made for this)
return KLogger(JVMLogger(SubstituteLogger("ScriptModelBuilderLogger", queue, false)))
}
@@ -1,27 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services
import java.io.File
import kotlin.script.experimental.api.ScriptEvaluationConfiguration
import kotlin.script.experimental.host.ScriptDefinition
import kotlin.script.experimental.host.ScriptingHostConfiguration
import kotlin.script.experimental.intellij.ScriptDefinitionsProvider
private class SpaceScriptDefinitionsProvider : ScriptDefinitionsProvider {
override val id = "SpaceScriptDefinitionsProvider"
override fun getDefinitionClasses(): Iterable<String> = listOf()
override fun getDefinitionsClassPath(): Iterable<File> = listOf()
override fun provideDefinitions(
baseHostConfiguration: ScriptingHostConfiguration,
loadedScriptDefinitions: List<ScriptDefinition>
): Iterable<ScriptDefinition> = listOf(
ScriptDefinition(
SpaceKtsCompilationConfiguration(),
ScriptEvaluationConfiguration.Default
)
)
override fun useDiscovery() = false
}
@@ -1,39 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services
import com.intellij.execution.lineMarker.RunLineMarkerContributor
import com.intellij.psi.PsiElement
class SpaceScriptRunLineMarkerProvider : RunLineMarkerContributor() {
override fun getInfo(element: PsiElement): Info? {
val refs = element.references
refs.forEach {
// TODO: optimize it!!!
/*
if (it is KtSimpleNameReference) {
val resolveResult = it.resolve()
if (resolveResult != null) {
val fqnName = resolveResult.getKotlinFqName()
if (fqnName != null) {
if (fqnName.asString() == "circlet.pipelines.config.dsl.api.Project.job") {
val valueArgumentList = element.nextSibling as KtValueArgumentList
val taskName = valueArgumentList.arguments.firstOrNull()?.children?.firstOrNull()?.reference?.canonicalText
if (taskName != null) {
val runAction = object : AnAction(ExecutionBundle.message("run.configurable.display.name"), null, AllIcons.RunConfigurations.TestState.Run) {
override fun actionPerformed(e: AnActionEvent) {
CircletRunConfigurationUtils.run(taskName, element.project)
}
}
return Info(runAction)
}
}
}
}
}
*/
}
return null
}
}
@@ -1,12 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services
import com.intellij.build.AbstractViewManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.intellij.space.messages.SpaceBundle
@Service
class SyncDslViewManager(project: Project) : AbstractViewManager(project) {
public override fun getViewName() = SpaceBundle.message("tab.title.space.automation.dsl")
}
@@ -1,51 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services.execution
import com.intellij.execution.process.ProcessHandler
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import libraries.io.random.Random
import libraries.klogging.KLogging
import java.io.OutputStream
@Service
class SpaceTaskRunner(val project: Project) {
companion object : KLogging()
private val ideaLocalRunnerLabel = "SpaceTaskRunner_${Random.nextString(10)}"
fun run(taskName: String): ProcessHandler {
// todo: implement using local run from the SDK.
return TaskProcessHandler(taskName)
}
private val newLine: String = System.getProperty("line.separator", "\n")
}
class TaskProcessHandler(private val taskName: String) : ProcessHandler() {
companion object : KLogging()
override fun getProcessInput(): OutputStream? {
return null
}
override fun detachIsDefault(): Boolean {
return false
}
override fun detachProcessImpl() {
logger.info("detachProcessImpl for task $taskName")
}
override fun destroyProcessImpl() {
logger.info("destroyProcessImpl for task $taskName")
notifyProcessTerminated(0)
}
fun dispose() {
notifyProcessTerminated(0)
}
}
@@ -1,27 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services.run
import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.execution.configurations.LocatableConfigurationBase
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
class SpaceRunConfiguration(project: Project, factory: ConfigurationFactory)
: LocatableConfigurationBase<SpaceRunTaskConfigurationOptions>(project, factory, "Run") {
override fun getConfigurationEditor(): SettingsEditor<out RunConfiguration> {
return SpaceRunTaskConfigurationEditor()
}
override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState? {
return SpaceRunTaskState(options, environment)
}
public override fun getOptions(): SpaceRunTaskConfigurationOptions {
return super.getOptions() as SpaceRunTaskConfigurationOptions
}
}
@@ -1,27 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services.run
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.configurations.SimpleConfigurationType
import com.intellij.openapi.components.BaseState
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NotNullLazyValue
import com.intellij.space.messages.SpaceBundle
import icons.SpaceIcons
import platform.common.ProductName
class SpaceRunConfigurationType : SimpleConfigurationType(
"SpaceRunConfiguration",
SpaceBundle.message("space.task.run.configuration.name"),
SpaceBundle.message("run.configuration.description", ProductName),
NotNullLazyValue.createValue { SpaceIcons.Main }
) {
override fun createTemplateConfiguration(project: Project): RunConfiguration {
return SpaceRunConfiguration(project, this)
}
override fun getOptionsClass(): Class<out BaseState>? {
return SpaceRunTaskConfigurationOptions::class.java
}
}
@@ -1,19 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services.run
import com.intellij.execution.ProgramRunnerUtil
import com.intellij.execution.RunManager
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.openapi.project.Project
object SpaceRunConfigurationUtils {
fun run(taskName: String, project: Project) {
val runManager = RunManager.getInstance(project)
val settings = runManager.createConfiguration("Run task $taskName", SpaceRunConfigurationType::class.java)
val configuration = settings.configuration as SpaceRunConfiguration
configuration.options.taskName = taskName
runManager.addConfiguration(settings)
runManager.selectedConfiguration = settings
ProgramRunnerUtil.executeConfiguration(settings, DefaultRunExecutor.getRunExecutorInstance())
}
}
@@ -1,31 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services.run
import com.intellij.openapi.options.SettingsEditor
import com.intellij.space.messages.SpaceBundle
import com.intellij.ui.components.JBTextField
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
class SpaceRunTaskConfigurationEditor : SettingsEditor<SpaceRunConfiguration>() {
private val taskNameField = JBTextField()
override fun resetEditorFrom(s: SpaceRunConfiguration) {
val options = s.options
taskNameField.text = options.taskName
}
override fun createEditor(): JComponent {
val panel = JPanel()
panel.layout = BoxLayout(panel, BoxLayout.LINE_AXIS)
panel.add(JLabel(SpaceBundle.message("run.configuration.editor.task.name.label")))
panel.add(taskNameField)
return panel
}
override fun applyEditorTo(s: SpaceRunConfiguration) {
val options = s.options
options.taskName = taskNameField.text
}
}
@@ -1,10 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services.run
import com.intellij.execution.configurations.LocatableRunConfigurationOptions
import com.intellij.util.xmlb.annotations.OptionTag
class SpaceRunTaskConfigurationOptions : LocatableRunConfigurationOptions() {
@get:OptionTag("TASK_NAME")
var taskName by string("")
}
@@ -1,26 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.services.run
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.CommandLineState
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.components.service
import com.intellij.space.plugins.pipelines.services.execution.SpaceTaskRunner
class SpaceRunTaskState(private val settings: SpaceRunTaskConfigurationOptions, environment: ExecutionEnvironment) : CommandLineState(
environment) {
override fun startProcess(): ProcessHandler {
val project = environment.project
val runner = project.service<SpaceTaskRunner>()
val taskName = settings.taskName
if (taskName == null) {
throw ExecutionException("TaskName is null") // NON-NLS
}
else {
return runner.run(taskName)
}
}
}
@@ -1,390 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.ui
import circlet.pipelines.DefaultDslFileName
import circlet.pipelines.config.idea.api.*
import circlet.pipelines.config.idea.api.ProcessExecutable.*
import circlet.pipelines.config.idea.api.ScriptStep.CompositeStep
import circlet.pipelines.config.idea.api.ScriptStep.SimpleStep
import com.intellij.icons.AllIcons
import com.intellij.ide.IdeBundle
import com.intellij.ide.util.PsiNavigationSupport
import com.intellij.openapi.actionSystem.ActionToolbarPosition
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.wm.ToolWindow
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.plugins.pipelines.services.SpaceKtsModelBuilder
import com.intellij.space.plugins.pipelines.viewmodel.ScriptState
import com.intellij.space.plugins.pipelines.viewmodel.SpaceModelTreeNode
import com.intellij.space.utils.LifetimedDisposable
import com.intellij.space.utils.LifetimedDisposableImpl
import com.intellij.ui.DumbAwareActionButton
import com.intellij.ui.ToggleActionButton
import com.intellij.ui.ToolbarDecorator
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.Panel
import com.intellij.ui.content.ContentFactory
import com.intellij.ui.treeStructure.Tree
import com.intellij.util.ui.JBEmptyBorder
import com.intellij.util.ui.tree.TreeUtil
import libraries.coroutines.extra.Lifetime
import libraries.klogging.KLogging
import runtime.reactive.bind
import runtime.reactive.mutableProperty
import runtime.reactive.view
import java.awt.CardLayout
import java.awt.Component
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreeSelectionModel
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.jvm.javaType
import kotlin.reflect.typeOf
class SpaceToolWindowViewModel(val lifetime: Lifetime) {
val taskIsRunning = mutableProperty(false)
val selectedNode = mutableProperty<SpaceModelTreeNode?>(null)
val extendedViewModeEnabled = mutableProperty(true)
}
@Service
class SpaceToolWindowService(val project: Project) : LifetimedDisposable by LifetimedDisposableImpl(), KLogging() {
val modelBuilder = project.service<SpaceKtsModelBuilder>()
val viewModel = SpaceToolWindowViewModel(lifetime)
fun createToolWindowContent(toolWindow: ToolWindow) {
// ping script builder first so that it starts building the model for the first time.
modelBuilder.requestModel()
val panel = Panel(title = null).apply {
add(createView())
}
val content = ContentFactory.SERVICE.getInstance().createContent(panel, "", false).apply {
isCloseable = false
}
toolWindow.contentManager.addContent(content)
}
fun createView(): JComponent {
logger.debug("createView. begin")
val layout = CardLayout()
val panel = JPanel(layout)
val treeCompName = "tree"
val missedDslCompName = "empty"
val treeView = createModelTreeView(lifetime, project)
panel.add(treeView, treeCompName)
panel.add(createViewForMissedDsl(project), missedDslCompName)
modelBuilder.script.view(lifetime) { lt, script ->
if (script == null) {
layout.show(panel, missedDslCompName)
}
else {
layout.show(panel, treeCompName)
}
}
logger.debug("createView. end")
return panel
}
private fun createModelTreeView(lifetime: Lifetime, project: Project): JComponent {
val tree = Tree()
tree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
val root = tree.model.root as DefaultMutableTreeNode
tree.selectionModel.addTreeSelectionListener {
val selectedNode = it.path.lastPathComponent
viewModel.selectedNode.value = if (selectedNode is SpaceModelTreeNode) selectedNode else null
}
TreeUtil.expandAll(tree)
tree.isRootVisible = false
lifetime.bind(modelBuilder.script) { script ->
bind(viewModel.extendedViewModeEnabled) { extendedViewModeEnabled ->
if (script != null) {
bind(script.config) { config ->
bind(script.error) { error ->
bind(script.state) { state ->
resetNodes(root, config, error, state, viewModel.extendedViewModeEnabled.value)
(tree.model as DefaultTreeModel).reload()
}
}
}
}
else {
// seems unreachable state.
root.removeAllChildren()
}
}
}
val refreshAction = object : DumbAwareActionButton(IdeBundle.message("action.refresh"), AllIcons.Actions.Refresh) {
override fun actionPerformed(e: AnActionEvent) {
modelBuilder.rebuildModel()
}
}
val expandAllAction = object : DumbAwareActionButton(IdeBundle.message("action.expand.all"), AllIcons.Actions.Expandall) {
override fun actionPerformed(e: AnActionEvent) {
if (modelBuilder.script.value?.state?.value == ScriptState.Building) {
return
}
TreeUtil.expandAll(tree)
}
}
val collapseAllAction = object : DumbAwareActionButton(IdeBundle.message("action.collapse.all"), AllIcons.Actions.Collapseall) {
override fun actionPerformed(e: AnActionEvent) {
if (modelBuilder.script.value?.state?.value == ScriptState.Building) {
return
}
TreeUtil.collapseAll(tree, 0)
}
}
val showExtendedInfoAction = object : ToggleActionButton(SpaceBundle.message("show.details"), AllIcons.Actions.Show) {
override fun isSelected(e: AnActionEvent?): Boolean {
return viewModel.extendedViewModeEnabled.value
}
override fun setSelected(e: AnActionEvent?, state: Boolean) {
viewModel.extendedViewModeEnabled.value = state
}
}
fun updateActionsIsEnabledStates() {
val smthIsRunning = modelBuilder.script.value?.state?.value == ScriptState.Building || viewModel.taskIsRunning.value
refreshAction.isEnabled = !smthIsRunning
}
viewModel.selectedNode.forEach(lifetime) {
updateActionsIsEnabledStates()
}
modelBuilder.script.view(lifetime) { lt, script ->
script?.state?.forEach(lt) {
updateActionsIsEnabledStates()
}
}
viewModel.taskIsRunning.forEach(lifetime) {
updateActionsIsEnabledStates()
}
val panel = ToolbarDecorator
.createDecorator(tree)
.addExtraAction(refreshAction)
// .addExtraAction(runAction)
.addExtraAction(expandAllAction)
.addExtraAction(collapseAllAction)
.addExtraAction(showExtendedInfoAction)
.setToolbarPosition(ActionToolbarPosition.TOP)
.createPanel()
panel.border = JBEmptyBorder(0)
return panel
}
private fun resetNodes(root: DefaultMutableTreeNode,
config: IdeaScriptConfig?,
error: String?,
state: ScriptState,
extendedViewModeEnabled: Boolean) {
root.removeAllChildren()
if (config == null) {
if (error != null) {
root.add(SpaceModelTreeNode(error))
return
}
if (state == ScriptState.Building) {
root.add(SpaceModelTreeNode("Compiling script..."))
return
}
return
}
val jobs = config.jobs
val targets = config.targets
val pipelines = config.pipelines
var jobsTypesCount = 0
if (jobs.any()) {
jobsTypesCount++
}
if (targets.any()) {
jobsTypesCount++
}
if (pipelines.any()) {
jobsTypesCount++
}
val shouldAddGroupingNodes = jobsTypesCount > 1
if (jobs.any()) {
val jobsCollectionNode = getGroupingNode(root, "jobs", shouldAddGroupingNodes)
config.jobs.forEach { job ->
val jobNode = job.toTreeNode(showChildren = extendedViewModeEnabled)
jobsCollectionNode.add(jobNode)
}
}
targets.forEach {
val child = SpaceModelTreeNode("target + ${it.name}. not implemented in UI yet")
root.add(child)
}
pipelines.forEach {
val child = SpaceModelTreeNode("pipelines + ${it.name}. not implemented in UI yet")
root.add(child)
}
}
private fun createViewForMissedDsl(project: Project): JComponent {
val panel = JPanel()
panel.layout = BoxLayout(panel, BoxLayout.Y_AXIS)
val infoText = JLabel(SpaceBundle.message("kts.toolwindow.automation.is.not.configured.label"))
infoText.isEnabled = false
infoText.alignmentX = Component.CENTER_ALIGNMENT
panel.add(infoText)
val createDslLink = ActionLink(SpaceBundle.message("kts.toolwindow.add.automation.script.link")) {
val basePath = project.basePath
if (basePath != null) {
val baseDirFile = LocalFileSystem.getInstance().findFileByPath(basePath)
if (baseDirFile != null) {
val application = ApplicationManager.getApplication()
application.runWriteAction {
val file = baseDirFile.createChildData(this, DefaultDslFileName)
val newLine = System.getProperty("line.separator", "\n")
val newFileContent =
"// Write your automation script $newLine" +
"// Once the script is ready, commit the changes to the repository $newLine" +
"// Use full DSL reference when needed https://jetbrains.team/help/space/automation-dsl.html $newLine" +
"job(\"My First Job\") {$newLine " +
"container(\"hello-world\")$newLine" +
"}"
VfsUtil.saveText(file, newFileContent)
PsiNavigationSupport.getInstance().createNavigatable(project, file, -1).navigate(true)
}
}
}
}
createDslLink.alignmentX = Component.CENTER_ALIGNMENT
panel.add(createDslLink)
val rootPanel = JPanel()
rootPanel.layout = GridBagLayout()
rootPanel.add(panel, GridBagConstraints())
return rootPanel
}
private fun getGroupingNode(root: DefaultMutableTreeNode, name: String, shouldAddGroupingNodes: Boolean): DefaultMutableTreeNode {
if (shouldAddGroupingNodes) {
val res = SpaceModelTreeNode(name, true)
root.add(res)
return res
}
return root
}
}
private fun ScriptJob.toTreeNode(showChildren: Boolean) = SpaceModelTreeNode(name, true).apply {
if (showChildren) {
if (triggers.any()) {
add(triggers.toTreeNode())
}
add(steps.toTreeNode())
}
}
private fun List<Trigger>.toTreeNode() = SpaceModelTreeNode("triggers").apply {
forEach { trigger ->
add(trigger.toTreeNode())
}
}
private fun Trigger.toTreeNode() = SpaceModelTreeNode(usefulSubTypeName() ?: "other trigger")
private fun StepSequence.toTreeNode() = SpaceModelTreeNode("steps").apply {
forEach { step ->
add(step.toTreeNode())
}
}
private fun ScriptStep.toTreeNode(): SpaceModelTreeNode = when (val step = this) {
is CompositeStep -> SpaceModelTreeNode(step.treeNodeText).apply {
step.children.forEach { subStep ->
add(subStep.toTreeNode())
}
}
is SimpleStep.Process.Container -> SpaceModelTreeNode("container: ${step.image}").apply {
add(step.data.exec.toTreeNode())
}
is SimpleStep.DockerComposeStep -> SpaceModelTreeNode("compose: ${step.mainService}")
else -> SpaceModelTreeNode(step.usefulSubTypeName())
}
private val CompositeStep.treeNodeText: String get() = when(this) {
is CompositeStep.Fork -> "parallel"
is CompositeStep.Sequence -> "sequence"
else -> usefulSubTypeName() ?: "composite step"
}
private fun ProcessExecutable.toTreeNode() = DefaultMutableTreeNode(treeNodeText)
private val ProcessExecutable.treeNodeText: String
get() = when (this) {
is ContainerExecutable.DefaultCommand -> "exec: defaultCommand${args.presentArgs()}"
is ContainerExecutable.OverrideEntryPoint -> "exec: overrideEntryPoint: $entryPoint${args.presentArgs()}"
is KotlinScript -> "exec: kts script"
is ShellScript -> "exec: shell script"
else -> "exec: ${usefulSubTypeName()}"
}
private fun List<String>.presentArgs(): String {
return if (this.any()) ". args: ${this.joinToString()}" else ""
}
/**
* Returns the name of this object's dynamic type (which is a subtype of its static type [T]).
* If this object is of an anonymous type, this method falls back to the name of the closest supertype of the runtime type of this object
* that implements/extends its static type [T].
* If this object is a direct anonymous implementation of the static type [T], this method returns `null` to allow other fallbacks.
*
* Most instances of the interfaces are anonymous classes at the moment in the space project.
* For instance, the [Trigger] interface may have subinterfaces that we don't know of at compile time here, but could be added in the
* future.
* Since the object is of an anonymous class, we should use the name of the closest supertype that implements [Trigger], e.g. GitPush.
*/
@OptIn(ExperimentalStdlibApi::class)
private inline fun <reified T : Any> T.usefulSubTypeName(): String? {
val simpleClassName = this::class.simpleName
if (simpleClassName != null) { // false for anonymous classes
return simpleClassName
}
// There is always at least one supertype (T), because we know this is an anonymous class that is also an instance of T
val subTypeOfT = this::class.supertypes.first { it.isSubtypeOf(typeOf<T>()) }
val simpleSubtypeName = subTypeOfT.javaType.typeName.substringAfterLast('.')
return simpleSubtypeName.takeIf { it != T::class.simpleName }
}
@@ -1,26 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.utils
import circlet.pipelines.DefaultDslFileName
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
object DslFileFinder {
fun find(project: Project): VirtualFile? {
val basePath = project.basePath ?: return null
val baseDirFile = LocalFileSystem.getInstance().findFileByPath(basePath) ?: return null
return find(baseDirFile)
}
fun checkFileNameIsApplicable(name: String): Boolean {
return DefaultDslFileName.equals(name, true)
}
private fun find(baseDirFile: VirtualFile): VirtualFile? {
return baseDirFile.children.firstOrNull {
checkFileNameIsApplicable(it.name)
}
}
}
@@ -1,91 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.utils
import runtime.reactive.Signal
import runtime.reactive.Source
import java.util.*
interface ObservableQueue<T> : Queue<T> {
@Suppress("unused")
sealed class Change<out T>(val index: T) {
class Add<out T>(val value: T) : Change<T>(value)
}
val change: Source<Change<T>>
companion object {
fun <T> mutable(): ObservableQueue<T> = ObservableMutableQueue()
}
}
class ObservableMutableQueue<T> : ObservableQueue<T>, Queue<T> {
private val storage = ArrayDeque<T>()
override val change = Signal.create<ObservableQueue.Change<T>>()
override fun contains(element: T): Boolean {
return storage.contains(element)
}
override fun addAll(elements: Collection<T>): Boolean {
return storage.addAll(elements)
}
override fun clear() {
TODO("not implemented") //return storage.clear()
}
override fun element(): T {
TODO("not implemented") //return storage.element()
}
override fun isEmpty(): Boolean {
return storage.isEmpty()
}
override fun remove(): T {
TODO("not implemented") //return storage.remove()
}
override fun remove(element: T): Boolean {
TODO("not implemented") //return storage.remove(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
TODO("not implemented") //return storage.containsAll(elements)
}
override fun removeAll(elements: Collection<T>): Boolean {
TODO("not implemented") //return storage.removeAll(elements)
}
override fun add(element: T): Boolean {
storage.add(element)
change.fire(ObservableQueue.Change.Add(element))
return true
}
override fun offer(e: T): Boolean {
TODO("not implemented") //return storage.offer(e)
}
override fun iterator(): MutableIterator<T> {
TODO("not implemented") //return storage.iterator()
}
override fun retainAll(elements: Collection<T>): Boolean {
TODO("not implemented") //return storage.retainAll(elements)
}
override fun peek(): T {
TODO("not implemented") //return storage.peek()
}
override fun poll(): T {
TODO("not implemented") //return storage.poll()
}
override val size: Int
get() = storage.size
}
@@ -1,50 +0,0 @@
// 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.
package com.intellij.space.plugins.pipelines.viewmodel
import circlet.pipelines.config.idea.api.IdeaScriptConfig
import com.intellij.build.events.BuildEvent
import com.intellij.build.events.BuildEventsNls
import com.intellij.build.events.impl.OutputBuildEventImpl
import libraries.coroutines.extra.LifetimeSource
import libraries.coroutines.extra.assertNotTerminated
import libraries.io.random.Random
import runtime.reactive.ObservableList
import runtime.reactive.Property
import javax.swing.tree.DefaultMutableTreeNode
enum class ScriptState { NotInitialised, Building, Ready }
interface ScriptModel {
val config: Property<IdeaScriptConfig?>
val error: Property<String?>
val state: Property<ScriptState>
}
class LogData {
// lifetime corresponding to the entire build process, it terminates when this build finishes
val lifetime get() = _buildLifetime
val buildId = Random.nextUID()
val messages = ObservableList.mutable<BuildEvent>()
private val _buildLifetime = LifetimeSource()
fun message(@BuildEventsNls.Message message: String) {
_buildLifetime.assertNotTerminated()
messages.add(OutputBuildEventImpl(buildId, message + "\n", true))
}
fun error(@BuildEventsNls.Message message: String) {
_buildLifetime.assertNotTerminated()
messages.add(OutputBuildEventImpl(buildId, message + "\n", false))
}
fun close() {
_buildLifetime.terminate()
}
}
class SpaceModelTreeNode(text: String? = null, val isRunnable: Boolean = false) : DefaultMutableTreeNode(text)
@@ -1,194 +0,0 @@
// 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.
package com.intellij.space.promo
import com.intellij.ide.BrowserUtil
import com.intellij.ide.ui.fullRow
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
import com.intellij.openapi.util.NlsSafe
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.ui.components.BrowserLink
import com.intellij.ui.layout.*
import com.intellij.ui.scale.ScaleContext
import com.intellij.ui.scale.ScaleType
import com.intellij.util.FontUtil
import com.intellij.util.JBHiDPIScaledImage
import com.intellij.util.SVGLoader
import com.intellij.util.ui.*
import icons.SpaceIcons
import libraries.klogging.logger
import java.awt.Color
import java.awt.Cursor
import java.awt.Dimension
import java.awt.Image
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.imageio.ImageIO
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JLayeredPane
internal const val SPACE_PROMO_VIDEO_URL: String = "https://www.jetbrains.com/space/?show-overview=from-ide&switch-to-branch=JS-14525-space-overview-video-qs-parameter"
internal const val EXPLORE_SPACE_PROMO_URL: String = "https://www.jetbrains.com/space/?fromIDEplugin"
internal const val SIGN_UP_SPACE_URL: String = "https://www.jetbrains.com/space/?fromIDEplugin#sign-up"
internal const val SPACE_TOOLBAR_PROMO_BANNER_PATH = "/images/spacePromo.png"
internal const val SPACE_TOOLBAR_PROMO_BANNER_PATH_RETINA = "/images/spacePromo@2x.png"
internal const val SPACE_BIG_PROMO_BANNER_NAME = "/images/spaceVideoPreview"
internal const val DARK_POSTFIX = "_dark"
internal const val HIDPI_POSTFIX = "@2x"
internal const val JETBRAINS_SPACE_LOGO = "/images/jetbrainsSpace.svg"
internal const val JETBRAINS_SPACE_LOGO_DARK = "/images/jetbrainsSpaceDark.svg"
internal fun LayoutBuilder.promoPanel(statsExplorePlace: SpaceStatsCounterCollector.ExplorePlace) {
fullRow { createSpaceByJetbrainsLabel()() }
fullRow { promoText(76)() }
fullRow { exploreSpaceLink(statsExplorePlace)() }.largeGapAfter()
}
internal fun toolbarPromoBanner(): JComponent? {
val imagePath = if (StartupUiUtil.isJreHiDPI()) SPACE_TOOLBAR_PROMO_BANNER_PATH_RETINA else SPACE_TOOLBAR_PROMO_BANNER_PATH
val image = ImageLoader.loadImage(imagePath, 364, 199) ?: return null
return wrapWithWatchSpaceOverviewLabelOverlay(JLabel(JBImageIcon(image)), SpaceStatsCounterCollector.OverviewPlace.MAIN_TOOLBAR)
}
internal fun bigPromoBanner(statsOverviewPlace: SpaceStatsCounterCollector.OverviewPlace): JComponent? =
bigPromoBanner(500, 285, statsOverviewPlace)
internal fun bigPromoBanner(width: Int, height: Int, statsOverviewPlace: SpaceStatsCounterCollector.OverviewPlace): JComponent? {
val isDarcula = StartupUiUtil.isUnderDarcula()
val themePart = if (isDarcula) DARK_POSTFIX else ""
val retinaPart = if (StartupUiUtil.isJreHiDPI()) HIDPI_POSTFIX else ""
val imagePath = "${SPACE_BIG_PROMO_BANNER_NAME}${retinaPart}${themePart}.png"
val image = ImageLoader.loadImage(imagePath, width, height) ?: return null
return wrapWithWatchSpaceOverviewLabelOverlay(
JLabel(JBImageIcon(image)),
statsOverviewPlace,
alwaysDisplayLabel = true,
useDarkLabel = isDarcula
)
}
internal fun exploreSpaceLink(statsExplorePlace: SpaceStatsCounterCollector.ExplorePlace): BrowserLink =
BrowserLink(
SpaceBundle.message("space.promo.explore.space.button"),
EXPLORE_SPACE_PROMO_URL
).apply {
addActionListener {
SpaceStatsCounterCollector.EXPLORE_SPACE.log(statsExplorePlace)
}
isFocusPainted = false
}
private fun wrapWithWatchSpaceOverviewLabelOverlay(
component: JComponent,
statsOverviewPlace: SpaceStatsCounterCollector.OverviewPlace,
alwaysDisplayLabel: Boolean = false,
useDarkLabel: Boolean = true
): JComponent {
@NlsSafe val text = "\u25B6${FontUtil.spaceAndThinSpace()}${SpaceBundle.message("space.promo.watch.space.overview.label")}"
val labelBackground = if (useDarkLabel) Color(0, 0, 0, 180) else Color(255, 255, 255, 180)
val labelForeground = if (useDarkLabel) Color.WHITE else Color.BLACK
val watchVideoButton = JLabel(text).apply {
isFocusable = false
isOpaque = true
isVisible = alwaysDisplayLabel
background = labelBackground
foreground = labelForeground
border = JBUI.Borders.empty(8)
}
return JComponentOverlay.createCentered(component, watchVideoButton).apply {
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
addMouseListener(object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent?) {
if (!alwaysDisplayLabel) watchVideoButton.isVisible = true
}
override fun mouseExited(e: MouseEvent?) {
if (!alwaysDisplayLabel) watchVideoButton.isVisible = false
}
override fun mouseClicked(e: MouseEvent?) {
SpaceStatsCounterCollector.WATCH_OVERVIEW.log(statsOverviewPlace)
watchPromoVideo()
}
})
}
}
internal fun createSpaceByJetbrainsLabel(): JComponent = JLabel().apply {
val jbSpaceSvgLogo = SvgLoader.loadSvg(if (StartupUiUtil.isUnderDarcula()) JETBRAINS_SPACE_LOGO_DARK else JETBRAINS_SPACE_LOGO, this)
if (jbSpaceSvgLogo != null) {
icon = jbSpaceSvgLogo
}
else {
icon = SpaceIcons.Main
text = SpaceBundle.message("product.name.jetbrains.space")
}
}
internal fun promoText(maxLineLength: Int = 0): JLabel = ComponentPanelBuilder.createCommentComponent(
SpaceBundle.message("space.promo.text.full"), true, maxLineLength).apply {
foreground = JBUI.CurrentTheme.Label.foreground()
font = JBUI.Fonts.label()
}
private fun watchPromoVideo() {
BrowserUtil.browse(SPACE_PROMO_VIDEO_URL)
}
private object JComponentOverlay {
fun createCentered(component: JComponent, centeredOverlay: JComponent): JLayeredPane {
val pane = object : JLayeredPane() {
override fun getPreferredSize(): Dimension = component.preferredSize
override fun doLayout() {
super.doLayout()
component.setBounds(0, 0, width, height)
centeredOverlay.bounds = SingleComponentCenteringLayout.getBoundsForCentered(component, centeredOverlay)
}
}
pane.isFocusable = false
pane.add(component, JLayeredPane.DEFAULT_LAYER, 1)
pane.add(centeredOverlay, JLayeredPane.DEFAULT_LAYER, 0)
return pane
}
}
val log = logger<ImageLoader>()
private object ImageLoader {
fun loadImage(path: String, width: Int, height: Int): Image? {
return try {
val img = ImageIO.read(javaClass.getResourceAsStream(path))
JBHiDPIScaledImage(img, width, height, img.type)
}
catch (e: Exception) {
log.error { "Image ${path} is not loaded" }
null
}
}
}
private object SvgLoader {
fun loadSvg(path: String, comp: JComponent): JBImageIcon? = try {
val bytes = javaClass.getResourceAsStream(path)?.readAllBytes()
val ctx = ScaleContext.create(comp)
val image = SVGLoader.loadWithoutCache(bytes, ctx.getScale(ScaleType.SYS_SCALE).toFloat())
val hiDpi = ImageUtil.ensureHiDPI(image, ctx)
JBImageIcon(hiDpi)
}
catch (e: Exception) {
log.error { "Svg ${path} is not loaded" }
null
}
}
@@ -1,102 +0,0 @@
// 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.
package com.intellij.space.runtime
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.Application
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.util.Disposer
import com.intellij.util.concurrency.AppExecutorUtil
import kotlinx.coroutines.*
import libraries.klogging.logger
import runtime.Cancellable
import runtime.CoroutineExceptionLogger
import runtime.Dispatcher
import runtime.TaskCancellable
import java.util.concurrent.Future
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
private val log = logger<ApplicationDispatcher>()
internal class ApplicationDispatcher(disposable: Disposable, private val application: Application) : Dispatcher {
private val executor = AppExecutorUtil.createBoundedScheduledExecutorService("Application Auxiliary Scheduler", 1)
private val context = ApplicationCoroutineContext(application, executor)
private val contextWithLog = context + CoroutineExceptionLogger.create(log)
override val coroutineContext: CoroutineContext
get() = contextWithLog
init {
Disposer.register(disposable, Disposable {
executor.shutdown()
try {
executor.awaitTermination(5, TimeUnit.SECONDS)
}
catch (e: InterruptedException) {
log.warn { "Executor was not properly terminated" }
}
})
}
override fun dispatch(fn: () -> Unit) {
application.invokeLater(fn, ModalityState.any())
}
override fun dispatch(delay: Int, fn: () -> Unit): Cancellable {
val invoke = java.lang.Runnable {
application.invokeLater(fn, ModalityState.any())
}
val disposable = executor.schedule(invoke, delay.toLong(), TimeUnit.MILLISECONDS)
return TaskCancellable(disposable)
}
override fun dispatchInterval(delay: Int, interval: Int, fn: () -> Unit): Cancellable {
val invoke = java.lang.Runnable {
application.invokeLater(fn, ModalityState.any())
}
val disposable = executor.scheduleWithFixedDelay(invoke, delay.toLong(), interval.toLong(), TimeUnit.MILLISECONDS)
return TaskCancellable(disposable)
}
}
@UseExperimental(InternalCoroutinesApi::class)
private class ApplicationCoroutineContext(
private val application: Application,
private val executor: ScheduledExecutorService
) : CoroutineDispatcher(), Delay {
override fun dispatch(context: CoroutineContext, block: Runnable) {
application.invokeLater(block, ModalityState.any())
}
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
val toResume = java.lang.Runnable {
application.invokeLater({ with(continuation) { this@ApplicationCoroutineContext.resumeUndispatched(Unit) } }, ModalityState.any())
}
executor.schedule(toResume, timeMillis, TimeUnit.MILLISECONDS)
}
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
val toResume = java.lang.Runnable {
application.invokeLater({
block.run()
}, ModalityState.any())
}
return DisposableFutureHandle(executor.schedule(toResume, timeMillis, TimeUnit.MILLISECONDS))
}
}
private class DisposableFutureHandle(private val future: Future<*>) : DisposableHandle {
override fun dispose() {
future.cancel(false)
}
override fun toString(): String = "DisposableFutureHandle[$future]"
}
@@ -1,13 +0,0 @@
// 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.
package com.intellij.space.settings
import circlet.workspaces.Workspace
import com.intellij.openapi.util.NlsContexts.DetailedDescription
sealed class SpaceLoginState(val server: String) {
class Disconnected(server: String, @DetailedDescription val error: String? = null) : SpaceLoginState(server)
class Connected(server: String, val workspace: Workspace) : SpaceLoginState(server)
class Connecting(server: String, private val cancelationHandler: () -> Unit) : SpaceLoginState(server) {
fun cancel() = cancelationHandler()
}
}
@@ -1,7 +0,0 @@
// 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.
package com.intellij.space.settings
data class SpaceServerSettings(
var enabled: Boolean = false,
var server: String = ""
)
@@ -1,40 +0,0 @@
// 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.
package com.intellij.space.settings
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.*
@State(
name = "SpaceServerConfigurable",
storages = [Storage(value = "SpaceServer.xml", roamingType = RoamingType.DEFAULT)]
)
class SpaceSettings : SimplePersistentStateComponent<SpaceSettingsState>(SpaceSettingsState()) {
var serverSettings: SpaceServerSettings
get() = state.serverSettings
set(value) {
state.serverSettings = value
}
var cloneType: CloneType
get() = state.cloneType
set(value) {
state.cloneType = value
}
companion object {
fun getInstance(): SpaceSettings = ApplicationManager.getApplication().getService(SpaceSettings::class.java)
}
}
class SpaceSettingsState : BaseState() {
var serverSettings: SpaceServerSettings by this.property(SpaceServerSettings()) {
it.enabled.not() && it.server.isBlank()
}
var cloneType: CloneType by enum(CloneType.HTTPS)
}
enum class CloneType {
HTTPS,
SSH
}
@@ -1,173 +0,0 @@
// 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.
package com.intellij.space.settings
import circlet.client.api.englishFullName
import com.intellij.ide.ui.fullRow
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogPanel
import com.intellij.space.components.SpaceUserAvatarProvider
import com.intellij.space.components.SpaceWorkspaceComponent
import com.intellij.space.messages.SpaceBundle
import com.intellij.space.promo.bigPromoBanner
import com.intellij.space.promo.promoPanel
import com.intellij.space.stats.SpaceStatsCounterCollector
import com.intellij.space.ui.LoginComponents.buildConnectingPanel
import com.intellij.space.ui.LoginComponents.loginPanel
import com.intellij.space.ui.LoginComponents.separatorRow
import com.intellij.space.ui.cleanupUrl
import com.intellij.space.ui.resizeIcon
import com.intellij.space.utils.LifetimedDisposable
import com.intellij.space.utils.LifetimedDisposableImpl
import com.intellij.space.utils.SpaceUrls
import com.intellij.ui.EnumComboBoxModel
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.BrowserLink
import com.intellij.ui.components.panels.VerticalLayout
import com.intellij.ui.layout.*
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.components.BorderLayoutPanel
import libraries.klogging.logger
import net.miginfocom.layout.CC
import net.miginfocom.layout.LC
import net.miginfocom.swing.MigLayout
import runtime.reactive.SequentialLifetimes
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
val log = logger<SpaceSettingsPanel>()
class SpaceSettingsPanel :
BoundConfigurable(SpaceBundle.message("configurable.name"), null),
SearchableConfigurable,
LifetimedDisposable by LifetimedDisposableImpl() {
private val sqLifetime = SequentialLifetimes(lifetime)
override fun getId(): String = "settings.space"
override fun createPanel(): DialogPanel = panel {
val panelLifetime = sqLifetime.next()
val accountPanel = BorderLayoutPanel()
row {
cell(isFullWidth = true) { accountPanel(pushX, growX) }
}
val separatorRow = separatorRow()
val cloneTypeRow = row {
cell(isFullWidth = true) {
label(SpaceBundle.message("settings.panel.clone.repositories.with.label"))
comboBox(EnumComboBoxModel(CloneType::class.java), SpaceSettings.getInstance()::cloneType)
createConfigureHttpAndSshLink()().withLargeLeftGap()
}
}
SpaceWorkspaceComponent.getInstance().loginState.forEach(panelLifetime) { loginState ->
accountPanel.removeAll()
accountPanel.addToCenter(createView(accountPanel, loginState))
accountPanel.revalidate()
accountPanel.repaint()
val rowsVisibility = when (loginState) {
is SpaceLoginState.Connected -> true
else -> false
}
separatorRow.visible = rowsVisibility
cloneTypeRow.visible = rowsVisibility
}
}
private fun createView(wrapper: JComponent, st: SpaceLoginState): JComponent {
when (st) {
is SpaceLoginState.Disconnected -> return buildSettingsLoginPanel(st) { server ->
SpaceWorkspaceComponent.getInstance().signInManually(server, lifetime, wrapper)
}
is SpaceLoginState.Connecting -> return buildConnectingPanel(st, SpaceStatsCounterCollector.LoginPlace.SETTINGS) {
st.cancel()
}
is SpaceLoginState.Connected -> {
val serverComponent = JLabel(cleanupUrl(st.server)).apply {
foreground = SimpleTextAttributes.GRAYED_ATTRIBUTES.fgColor
font = font.deriveFont((font.size * 0.9).toFloat())
}
val logoutButton = JButton(SpaceBundle.message("settings.panel.log.out.button.text")).apply {
addActionListener {
SpaceWorkspaceComponent.getInstance().signOut(SpaceStatsCounterCollector.LogoutPlace.SETTINGS)
}
}
val namePanel = JPanel(VerticalLayout(UIUtil.DEFAULT_VGAP)).apply {
add(JLabel(st.workspace.me.value.englishFullName())) // NON-NLS
add(serverComponent)
}
val infoPanel = JPanel(VerticalLayout(UIUtil.DEFAULT_VGAP)).apply {
add(namePanel)
val logoutButtonPanel = BorderLayoutPanel().addToLeft(logoutButton).apply {
border = JBUI.Borders.emptyTop(5)
}
add(logoutButtonPanel)
}
val avatarLabel = JLabel()
SpaceUserAvatarProvider.getInstance().avatars.forEach(lifetime) { avatars ->
avatarLabel.icon = resizeIcon(avatars.circle, 64)
}
return JPanel().apply {
layout = MigLayout(LC().gridGap("${JBUI.scale(10)}", "0")
.insets("0", "0", "0", "0")
.fill()).apply {
columnConstraints = "[][]"
}
add(avatarLabel, CC().pushY().alignY("center"))
add(infoPanel, CC().push())
}
}
}
}
private fun createConfigureHttpAndSshLink(): JComponent {
val contentPanel = BorderLayoutPanel().apply {
isOpaque = false
}
SpaceWorkspaceComponent.getInstance().workspace.forEach(lifetime) {
contentPanel.removeAll()
if (it != null) {
val url = SpaceUrls.git(it.me.value.username)
val browserLink = BrowserLink(SpaceBundle.message("settings.panel.configure.git.ssh.keys.http.password.link"), url).apply {
addActionListener {
SpaceStatsCounterCollector.OPEN_GIT_SETTINGS_IN_SPACE.log()
}
}
contentPanel.addToCenter(browserLink)
}
contentPanel.revalidate()
contentPanel.repaint()
}
return contentPanel
}
companion object {
fun openSettings(project: Project?) {
ShowSettingsUtil.getInstance().showSettingsDialog(project, SpaceSettingsPanel::class.java)
}
}
}
internal fun buildSettingsLoginPanel(st: SpaceLoginState.Disconnected,
loginAction: (String) -> Unit
): DialogPanel {
return panel {
loginPanel(st, SpaceStatsCounterCollector.LoginPlace.SETTINGS, isLoginActionDefault = false, withOrganizationsUrlLabel = true,
loginAction)
promoPanel(SpaceStatsCounterCollector.ExplorePlace.SETTINGS)
bigPromoBanner(SpaceStatsCounterCollector.OverviewPlace.SETTINGS)?.let {
fullRow { it() }
}
}
}
@@ -1,363 +0,0 @@
// 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.
package com.intellij.space.stats
import circlet.code.api.CodeReviewParticipantRole
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
import com.intellij.space.settings.SpaceLoginState
import com.intellij.space.vcs.review.list.ReviewListQuickFilter
internal class SpaceStatsCounterCollector : CounterUsagesCollector() {
override fun getGroup(): EventLogGroup = GROUP
companion object {
@JvmField
val GROUP = EventLogGroup("space", 1)
// --------------------
// General
// --------------------
@JvmField
val LOGIN_STATUS = EventFields.Enum<LoginState>("login_status")
@JvmField
val OPEN_MAIN_TOOLBAR_POPUP = GROUP.registerEvent("open_main_toolbar_popup", LOGIN_STATUS)
@JvmField
val OPEN_GIT_SETTINGS_IN_SPACE = GROUP.registerEvent("open_git_settings_in_space")
// --------------------
// Advertisement
// --------------------
@JvmField
val EXPLORE_PLACE = EventFields.Enum<ExplorePlace>("adv_explore_place")
@JvmField
val OVERVIEW_PLACE = EventFields.Enum<OverviewPlace>("adv_overview_place")
@JvmField
val EXPLORE_SPACE = GROUP.registerEvent("adv_explore_space", EXPLORE_PLACE)
@JvmField
val WATCH_OVERVIEW = GROUP.registerEvent("adv_watch_overview", OVERVIEW_PLACE)
@JvmField
val ADV_LOG_IN_LINK = GROUP.registerEvent("adv_log_in_link")
@JvmField
val SIGN_UP_LINK = GROUP.registerEvent("adv_sign_up_link")
// --------------------
// Clone
// --------------------
@JvmField
val OPEN_SPACE_CLONE_TAB = GROUP.registerEvent("open_space_clone_tab", LOGIN_STATUS)
@JvmField
val CLONE_REPO = GROUP.registerEvent("clone_repo")
// --------------------
// Share project
// --------------------
@JvmField
val OPEN_SHARE_PROJECT = GROUP.registerEvent("open_share_project", LOGIN_STATUS)
@JvmField
val START_CREATING_NEW_PROJECT = GROUP.registerEvent("start_creating_new_project")
@JvmField
val CREATE_NEW_PROJECT = GROUP.registerEvent("create_new_project")
@JvmField
val SHARE_PROJECT = GROUP.registerEvent("share_project")
// --------------------
// Chat
// --------------------
@JvmField
val SEND_MESSAGE_PLACE = EventFields.Enum<SendMessagePlace>("new_message_place")
@JvmField
val SEND_MESSAGE_IS_PENDING = EventFields.Boolean("new_message_is_pending")
@JvmField
val EDIT_MESSAGE_IS_EMPTY = EventFields.Boolean("edit_message_is_empty")
@JvmField
val SEND_MESSAGE = GROUP.registerEvent("chat_send_message", SEND_MESSAGE_PLACE, SEND_MESSAGE_IS_PENDING)
@JvmField
val DISCARD_SEND_MESSAGE = GROUP.registerEvent("chat_discard_send_message", SEND_MESSAGE_PLACE)
@JvmField
val START_EDIT_MESSAGE = GROUP.registerEvent("chat_start_edit_message")
@JvmField
val DISCARD_EDIT_MESSAGE = GROUP.registerEvent("chat_discard_edit_message")
@JvmField
val SEND_EDIT_MESSAGE = GROUP.registerEvent("chat_send_edit_message", EDIT_MESSAGE_IS_EMPTY)
@JvmField
val OPEN_THREAD = GROUP.registerEvent("chat_open_thread")
@JvmField
val DELETE_MESSAGE = GROUP.registerEvent("chat_delete_message")
@JvmField
val EXPAND_DISCUSSION = GROUP.registerEvent("chat_expand_discussion")
@JvmField
val COLLAPSE_DISCUSSION = GROUP.registerEvent("chat_collapse_discussion")
@JvmField
val RESOLVE_DISCUSSION = GROUP.registerEvent("chat_resolve_discussion")
@JvmField
val REOPEN_DISCUSSION = GROUP.registerEvent("chat_reopen_discussion")
// --------------------
// Login
// --------------------
@JvmField
val LOGIN_PLACE = EventFields.Enum<LoginPlace>("login_place")
@JvmField
val LOGOUT_PLACE = EventFields.Enum<LogoutPlace>("logout_place")
@JvmField
val LOG_IN = GROUP.registerEvent("button_log_in", LOGIN_PLACE)
@JvmField
val LOG_OUT = GROUP.registerEvent("button_log_out", LOGOUT_PLACE)
@JvmField
val CANCEL_LOGIN = GROUP.registerEvent("cancel_login", LOGIN_PLACE)
// --------------------
// ReviewsList
// --------------------
@JvmField
val QUICK_FILTER = EventFields.Enum<ReviewListQuickFilter>("quick_filter", transform = { it.name })
@JvmField
val OPEN_REVIEW_TYPE = EventFields.Enum<OpenReviewActionType>("open_review_type")
@JvmField
val FILTER_TEXT_EMPTY = EventFields.Boolean("filter_text_empty")
@JvmField
val REFRESH_REVIEWS_PLACE = EventFields.Enum<RefreshReviewsPlace>("refresh_reviews_place")
@JvmField
val REVIEWS_LOG_IN_LINK = GROUP.registerEvent("reviews_list_log_in_link")
@JvmField
val CHANGE_QUICK_FILTER = GROUP.registerEvent("reviews_list_change_quick_filter", QUICK_FILTER)
@JvmField
val CHANGE_TEXT_FILTER = GROUP.registerEvent("reviews_list_change_text_filter", FILTER_TEXT_EMPTY)
@JvmField
val OPEN_REVIEW = GROUP.registerEvent("reviews_list_open_review", OPEN_REVIEW_TYPE)
@JvmField
val REFRESH_REVIEWS_ACTION = GROUP.registerEvent("reviews_list_refresh_action", REFRESH_REVIEWS_PLACE)
// --------------------
// ReviewDetails
// --------------------
@JvmField
val PARTICIPANT_ROLE = EventFields.Enum<CodeReviewParticipantRole>("participant_role", transform = { it.name })
@JvmField
val PARTICIPANT_EDIT_TYPE = EventFields.Enum<ParticipantEditType>("participant_edit_type")
@JvmField
val COMMITS_SELECTION_TYPE = EventFields.Enum<CommitsSelectionType>("commits_selection_type")
@JvmField
val DETAILS_TAB_TYPE = EventFields.Enum<DetailsTabType>("details_tab_type")
@JvmField
val REVIEW_DIFF_PLACE = EventFields.Enum<ReviewDiffPlace>("review_diff_place")
@JvmField
val EDIT_PARTICIPANT = GROUP.registerEvent("review_details_edit_participant", PARTICIPANT_ROLE, PARTICIPANT_EDIT_TYPE)
@JvmField
val EDIT_PARTICIPANT_ICON = GROUP.registerEvent("review_details_add_participant_icon", PARTICIPANT_ROLE)
@JvmField
val SHOW_TIMELINE = GROUP.registerEvent("review_details_show_timeline")
@JvmField
val OPEN_PROJECT_IN_SPACE = GROUP.registerEvent("review_details_open_project_in_space")
@JvmField
val OPEN_REVIEW_IN_SPACE = GROUP.registerEvent("review_details_open_review_in_space")
@JvmField
val BACK_TO_LIST = GROUP.registerEvent("review_details_back_to_list", DETAILS_TAB_TYPE)
@JvmField
val CHANGE_COMMITS_SELECTION = GROUP.registerEvent("review_details_change_commits_selection", COMMITS_SELECTION_TYPE)
@JvmField
val SELECT_DETAILS_TAB = GROUP.registerEvent("review_details_select_details_tab", DETAILS_TAB_TYPE)
@JvmField
val OPEN_REVIEW_DIFF = GROUP.registerEvent("review_details_open_review_diff", REVIEW_DIFF_PLACE)
@JvmField
val CHECKOUT_BRANCH = GROUP.registerEvent("review_details_checkout_branch")
@JvmField
val UPDATE_BRANCH = GROUP.registerEvent("review_details_update_branch")
@JvmField
val ACCEPT_CHANGES = GROUP.registerEvent("review_details_accept_changes")
@JvmField
val WAIT_FOR_RESPONSE = GROUP.registerEvent("review_details_wait_for_response")
@JvmField
val RESUME_REVIEW = GROUP.registerEvent("review_details_resume_review")
// --------------------
// ReviewDiff
// --------------------
@JvmField
val LOADER_TYPE = EventFields.Enum<LoaderType>("loader_type")
@JvmField
val LEAVE_COMMENT = GROUP.registerEvent("review_diff_leave_comment")
@JvmField
val CLOSE_LEAVE_COMMENT = GROUP.registerEvent("review_diff_close_leave_comment")
@JvmField
val DIFF_LOADED = GROUP.registerEvent("review_diff_loaded", LOADER_TYPE)
}
// helper classes should be outside the companion to get rid of Companion class in imports and usages
enum class LoginState {
CONNECTED,
CONNECTING,
DISCONNECTED;
companion object {
fun convert(state: SpaceLoginState) = when (state) {
is SpaceLoginState.Connected -> CONNECTED
is SpaceLoginState.Connecting -> CONNECTING
is SpaceLoginState.Disconnected -> DISCONNECTED
}
}
}
enum class ExplorePlace {
MAIN_TOOLBAR,
SETTINGS,
SHARE,
CLONE
}
enum class OverviewPlace {
MAIN_TOOLBAR,
SETTINGS,
CLONE
}
enum class SendMessagePlace {
MAIN_CHAT,
THREAD,
DIFF,
NEW_THREAD,
FIRST_DISCUSSION_ANSWER,
NEW_DISCUSSION
}
enum class LoginPlace {
MAIN_TOOLBAR,
SETTINGS,
SHARE,
CLONE
}
enum class LogoutPlace {
ACTION,
SETTINGS,
MAIN_TOOLBAR,
CLONE,
AUTH_FAIL,
}
enum class OpenReviewActionType {
ENTER,
DOUBLE_CLICK,
ARROW
}
enum class RefreshReviewsPlace {
EMPTY_LIST,
CONTEXT_MENU
}
enum class ParticipantEditType {
ADD,
REMOVE
}
enum class CommitsSelectionType {
SINGLE,
ALL,
SUBSET_CONNECTED,
SUBSET_SPLIT;
companion object {
fun calculateSelectionType(newSelection: List<Int>, commitsCount: Int): CommitsSelectionType =
when (newSelection.size) {
commitsCount -> ALL
1 -> SINGLE
else -> calculateSubsetType(newSelection)
}
private fun calculateSubsetType(newSelection: List<Int>): CommitsSelectionType {
for (i in 1 until newSelection.size) {
if (newSelection[i - 1] + 1 != newSelection[i]) {
return SUBSET_SPLIT
}
}
return SUBSET_CONNECTED
}
}
}
enum class DetailsTabType {
DETAILS,
COMMITS
}
enum class ReviewDiffPlace {
EDITOR,
DIALOG
}
enum class LoaderType {
GIT,
SPACE
}
}
@@ -1,72 +0,0 @@
// 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.
package com.intellij.space.stats
import com.intellij.internal.statistic.beans.MetricEvent
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.service.fus.collectors.ProjectUsagesCollector
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.space.components.SpaceWorkspaceComponent
import com.intellij.space.plugins.pipelines.services.SpaceKtsFileDetector
import com.intellij.space.settings.CloneType
import com.intellij.space.settings.SpaceSettings
import com.intellij.space.stats.SpaceStatsCounterCollector.Companion.LOGIN_STATUS
import com.intellij.space.vcs.SpaceProjectContext
internal class SpaceStatsStateCollector : ProjectUsagesCollector() {
override fun getGroup() = GROUP
override fun getMetrics(project: Project): MutableSet<MetricEvent> {
val result = HashSet<MetricEvent>()
val workspace = SpaceWorkspaceComponent.getInstance()
result.add(LOGIN_STATE.metric(SpaceStatsCounterCollector.LoginState.convert(workspace.loginState.value)))
val context = SpaceProjectContext.getInstance(project)
result.add(ASSOCIATED_REPOS_STATE.metric(
context.currentContext.isAssociatedWithSpaceRepository,
context.probablyContainsSpaceRepo.value
))
val automationScriptDetector = project.service<SpaceKtsFileDetector>()
result.add(AUTOMATION_FILE_STATE.metric(automationScriptDetector.dslFile.value != null))
val cloneType = SpaceSettings.getInstance().cloneType
result.add(GIT_CLONE_TYPE_STATE.metric(cloneType))
return result
}
companion object {
@JvmField
val GROUP = EventLogGroup("space.state", 1)
@JvmField
val IS_ASSOCIATED_WITH_SPACE_REPO = EventFields.Boolean("is_associated_with_space_repo")
@JvmField
val IS_PROBABLY_CONTAINS_SPACE_REPO = EventFields.Boolean("is_probably_contains_space_repo")
@JvmField
val AUTOMATION_FILE_EXISTS = EventFields.Boolean("automation_file_exists")
@JvmField
val GIT_CLONE_TYPE = EventFields.Enum<CloneType>("type")
@JvmField
val LOGIN_STATE = GROUP.registerEvent("login_status", LOGIN_STATUS)
@JvmField
val ASSOCIATED_REPOS_STATE = GROUP.registerEvent(
"associated_repos_state",
IS_ASSOCIATED_WITH_SPACE_REPO,
IS_PROBABLY_CONTAINS_SPACE_REPO
)
@JvmField
val AUTOMATION_FILE_STATE = GROUP.registerEvent("automation_file_state", AUTOMATION_FILE_EXISTS)
@JvmField
val GIT_CLONE_TYPE_STATE = GROUP.registerEvent("git_clone_type", GIT_CLONE_TYPE)
}
}
@@ -1,36 +0,0 @@
// 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.
package com.intellij.space.tools
import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.space.plugins.pipelines.ui.SpaceToolWindowService
import com.intellij.space.utils.computeSafe
class SpaceToolWindowFactory : ToolWindowFactory, DumbAware {
override fun shouldBeAvailable(project: Project): Boolean {
return false
}
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val service = project.service<SpaceToolWindowService>()
service.createToolWindowContent(toolWindow)
}
override fun isDoNotActivateOnStart(): Boolean {
return true
}
companion object {
const val TOOL_WINDOW_ID = "Space Automation" // NON-NLS
}
}
val Project.spaceKtsToolwindow: ToolWindow?
get() = computeSafe {
toolWindowManager.getToolWindow(SpaceToolWindowFactory.TOOL_WINDOW_ID)
}
@@ -1,9 +0,0 @@
// 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.
package com.intellij.space.tools
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.openapi.wm.ex.ToolWindowManagerEx
val Project.toolWindowManager: ToolWindowManager get() = ToolWindowManager.getInstance(this)
val Project.toolWindowManagerEx: ToolWindowManagerEx get() = ToolWindowManagerEx.getInstanceEx(this)
@@ -1,69 +0,0 @@
// 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.
package com.intellij.space.ui
import circlet.client.api.TD_MemberProfile
import circlet.client.api.englishFullName
import circlet.platform.api.TID
import com.intellij.ui.scale.ScaleContext
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.AvatarUtils.generateColoredAvatar
import com.intellij.util.ui.ImageUtil
import com.intellij.util.ui.JBImageIcon
import com.intellij.util.ui.JBValue
import libraries.coroutines.extra.Lifetime
import libraries.coroutines.extra.launch
import runtime.Ui
import java.awt.Component
import java.awt.Graphics
import javax.swing.Icon
class SpaceAvatarProvider(
private val lifetime: Lifetime,
private val component: Component,
val iconSize: JBValue
) {
private val scaleContext = ScaleContext.create(component)
private val avatarIconsCache: MutableMap<TID, Icon> = HashMap()
private val imageLoader: SpaceImageLoader = SpaceImageLoader.getInstance()
@RequiresEdt
fun getIcon(user: TD_MemberProfile): Icon {
val iconSize = iconSize.get()
// so that icons are rescaled when any scale changes (be it font size or current DPI)
if (scaleContext.update(ScaleContext.create(component))) {
avatarIconsCache.clear()
}
return avatarIconsCache.getOrPut(user.id) {
val gradientIcon = resizeIcon(JBImageIcon(ImageUtil.createCircleImage(
generateColoredAvatar(
user.username,
user.englishFullName()
)
)), iconSize)
val icon = DelegatingIcon(gradientIcon)
user.smallAvatar?.let { tid ->
launch(lifetime, Ui) {
val image = imageLoader.loadImageAsync(tid)?.await()
if (image != null) {
val circleImage = ImageUtil.createCircleImage(image)
icon.delegate = resizeIcon(JBImageIcon(circleImage), iconSize)
component.repaint()
}
}
}
icon
}
}
}
private class DelegatingIcon(var delegate: Icon) : Icon {
override fun getIconHeight() = delegate.iconHeight
override fun getIconWidth() = delegate.iconWidth
override fun paintIcon(c: Component?, g: Graphics?, x: Int, y: Int) = delegate.paintIcon(c, g, x, y)
}

Some files were not shown because too many files have changed in this diff Show More