mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-13 15:52:01 +07:00
PY-77483, PY-75549, FUS-5195: Misc project pycharm (squashed commits from master)
Lots of files are picked from the master GitOrigin-RevId: 88dfc699cbfa2be9b11d1645c2c24221d16fbdc4
This commit is contained in:
committed by
intellij-monorepo-bot
parent
aa93e32d40
commit
9f31575c35
@@ -10,7 +10,7 @@ import org.jetbrains.intellij.build.io.copyFileToDir
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
open class PyCharmCommunityProperties(private val communityHome: Path) : PyCharmPropertiesBase() {
|
||||
open class PyCharmCommunityProperties(private val communityHome: Path) : PyCharmPropertiesBase(enlargeWelcomeScreen = true) {
|
||||
override val customProductCode: String
|
||||
get() = "PC"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.intellij.build.pycharm
|
||||
|
||||
import kotlinx.collections.immutable.plus
|
||||
import org.jetbrains.intellij.build.BuildContext
|
||||
import org.jetbrains.intellij.build.JetBrainsProductProperties
|
||||
import org.jetbrains.intellij.build.TEST_FRAMEWORK_WITH_JAVA_RT
|
||||
@@ -11,11 +12,15 @@ import java.util.function.Predicate
|
||||
|
||||
const val PYDEVD_PACKAGE: String = "pydevd_package"
|
||||
|
||||
abstract class PyCharmPropertiesBase : JetBrainsProductProperties() {
|
||||
abstract class PyCharmPropertiesBase(enlargeWelcomeScreen: Boolean = false) : JetBrainsProductProperties() {
|
||||
override val baseFileName: String
|
||||
get() = "pycharm"
|
||||
|
||||
init {
|
||||
if (enlargeWelcomeScreen) {
|
||||
additionalVmOptions += "-Dwelcome.screen.defaultWidth=1000"
|
||||
additionalVmOptions += "-Dwelcome.screen.defaultHeight=720"
|
||||
}
|
||||
reassignAltClickToMultipleCarets = true
|
||||
useSplash = true
|
||||
productLayout.addPlatformSpec(TEST_FRAMEWORK_WITH_JAVA_RT)
|
||||
|
||||
@@ -28,5 +28,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.whatsNew" />
|
||||
<orderEntry type="module" module-name="intellij.pycharm.community.ide.impl.promotion" scope="RUNTIME" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util.coroutines" />
|
||||
<orderEntry type="module" module-name="intellij.platform.experiment" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -2,7 +2,7 @@
|
||||
<!--Customization code for both Community and Pro PyCharms-->
|
||||
<dependencies>
|
||||
<plugin id="PythonCore"/>
|
||||
<module name="intellij.platform.whatsNew" />
|
||||
<module name="intellij.platform.whatsNew"/>
|
||||
</dependencies>
|
||||
|
||||
<projectListeners>
|
||||
@@ -11,7 +11,17 @@
|
||||
topic="com.intellij.workspaceModel.ide.JpsProjectLoadedListener"/>
|
||||
</projectListeners>
|
||||
|
||||
<extensionPoints>
|
||||
<extensionPoint interface="com.intellij.pycharm.community.ide.impl.miscProject.MiscFileType"
|
||||
qualifiedName="Pythonid.miscFileType" dynamic="true"/>
|
||||
</extensionPoints>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<refactoring.elementListenerProvider implementation="com.intellij.pycharm.community.ide.impl.miscProject.impl.MiscProjectListenerProvider"/>
|
||||
<statistics.counterUsagesCollector implementationClass="com.intellij.pycharm.community.ide.impl.miscProject.impl.MiscProjectUsageCollector"/>
|
||||
<experiment.abExperimentOption implementation="com.intellij.pycharm.community.ide.impl.miscProject.impl.PyMiscProjectExperimentOption"/>
|
||||
<registryKey defaultValue="5" description="Number of primary buttons on welcome screen (other go to 'more actions')"
|
||||
key="welcome.screen.primaryButtonsCount" restartRequired="true" overrides="true"/>
|
||||
<applicationInitializedListener implementation="com.intellij.pycharm.community.ide.impl.PyCharmCorePluginConfigurator"/>
|
||||
<applicationService serviceInterface="com.intellij.lang.IdeLanguageCustomization"
|
||||
serviceImplementation="com.intellij.pycharm.community.ide.impl.PyCharmPythonIdeLanguageCustomization"
|
||||
@@ -71,7 +81,8 @@
|
||||
<directoryIndexExcludePolicy implementation="com.intellij.pycharm.community.ide.impl.PyDirectoryIndexExcludePolicy"/>
|
||||
|
||||
<applicationService serviceImplementation="com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeSettings"/>
|
||||
<statistics.counterUsagesCollector implementationClass="com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeCollector"/>
|
||||
<statistics.counterUsagesCollector
|
||||
implementationClass="com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeCollector"/>
|
||||
<notificationGroup id="PyCharm Professional Advertiser" displayType="STICKY_BALLOON" isLogByDefault="false"
|
||||
bundle="messages.PyCharmCommunityCustomizationBundle" key="notification.group.pro.advertiser"/>
|
||||
|
||||
@@ -103,11 +114,14 @@
|
||||
</action>
|
||||
|
||||
<group id="WelcomeScreen.Platform.NewProject">
|
||||
<group id="WelcomeScreen.PyScratchFileActionGroup"
|
||||
class="com.intellij.pycharm.community.ide.impl.miscProject.impl.PyMiscFileActionGroup" compact="true"/>
|
||||
<group id="WelcomeScreen.CreateDirectoryProject"
|
||||
class="com.intellij.pycharm.community.ide.impl.newProjectWizard.impl.PyV3NewProjectStepAction"/>
|
||||
class="com.intellij.pycharm.community.ide.impl.newProjectWizard.impl.PyV3NewProjectStepAction"/>
|
||||
<reference ref="WelcomeScreen.OpenDirectoryProject"/>
|
||||
|
||||
<add-to-group group-id="WelcomeScreen.QuickStart" anchor="first"/>
|
||||
</group>
|
||||
</actions>
|
||||
|
||||
</idea-plugin>
|
||||
|
||||
@@ -59,4 +59,19 @@ sdk.notification.label.set.up.poetry.environment.from.pyproject.toml.dependencie
|
||||
notification.group.pro.advertiser=PyCharm Professional recommended
|
||||
|
||||
new.project.python.group.name=Python
|
||||
new.project.other.group.name=Other
|
||||
new.project.other.group.name=Other
|
||||
|
||||
misc.script.text=New Script
|
||||
|
||||
misc.project.generating.env=Preparing environment
|
||||
misc.project.filling.file=Filling the template file
|
||||
|
||||
misc.project.error.title=Error Creating Scratch File
|
||||
misc.project.error.install.python=Python could not be installed {0}. Try restarting PyCharm. If it did not help, install python manually and try again.
|
||||
misc.project.error.create.venv=Failed to create virtual environment: {0}. Please delete {1} if it exists, and try again. You can also create virtual environment in {1} and try again. Check logs for more info.
|
||||
misc.project.error.create.dir=Could not create {0} because of {1}. Please create it manually, and try again.
|
||||
|
||||
misc.project.error.all.pythons.bad=No usable pythons were found on your system. Please install python manually. Check logs for more info.
|
||||
|
||||
misc.no.python.found=No Python Interpreter Found on Your System.
|
||||
misc.install.python.question=Do you want to install the latest Python?
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject
|
||||
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.NlsActions
|
||||
import com.intellij.psi.PsiFile
|
||||
import javax.swing.Icon
|
||||
|
||||
/**
|
||||
* On a welcome screen user clicks in [icon] to get a project with [fileName] template filled by [fillFile]
|
||||
*/
|
||||
interface MiscFileType {
|
||||
companion object {
|
||||
val EP: ExtensionPointName<MiscFileType> = ExtensionPointName.create("Pythonid.miscFileType")
|
||||
}
|
||||
|
||||
val title: @NlsActions.ActionText String
|
||||
val icon: Icon
|
||||
val fileName: TemplateFileName
|
||||
|
||||
suspend fun fillFile(file: PsiFile, sdk: Sdk)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject
|
||||
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import org.jetbrains.annotations.NotNull
|
||||
|
||||
/**
|
||||
* [nameWithSuffix] will create `[nameNoExt]`N`.[ext] where `N > 0` if file already exists
|
||||
*/
|
||||
data class TemplateFileName(private val nameNoExt: @NotNull String, private val ext: @NonNls String) {
|
||||
|
||||
fun nameWithSuffix(suffixCount: Int): @NlsSafe String {
|
||||
val suffix = if (suffixCount == 0) "" else suffixCount.toString()
|
||||
return "$nameNoExt$suffix.$ext"
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates instance from `file.ext` ie `notebook.ipynb`
|
||||
*/
|
||||
fun parse(fileNameWithExt: @NonNls String): TemplateFileName {
|
||||
val filePaths = fileNameWithExt.split(".")
|
||||
assert(filePaths.size == 2) { "$fileNameWithExt must be file.ext" }
|
||||
return TemplateFileName(filePaths[0], filePaths[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject.impl
|
||||
|
||||
import com.intellij.psi.PsiDirectory
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.refactoring.listeners.RefactoringElementListener
|
||||
import com.intellij.refactoring.listeners.RefactoringElementListenerProvider
|
||||
import kotlin.io.path.name
|
||||
|
||||
/**
|
||||
* Listens for misc project rename
|
||||
*/
|
||||
internal class MiscProjectListenerProvider : RefactoringElementListenerProvider {
|
||||
override fun getListener(element: PsiElement): RefactoringElementListener? {
|
||||
if (!miscProjectEnabled.value) return null
|
||||
val dir = (element as? PsiDirectory) ?: return null
|
||||
// dir name is enough
|
||||
return if (dir.name == miscProjectDefaultPath.value.name) MiscProjectRenameReporter else null
|
||||
}
|
||||
}
|
||||
|
||||
private object MiscProjectRenameReporter : RefactoringElementListener {
|
||||
override fun elementMoved(newElement: PsiElement) = Unit
|
||||
|
||||
override fun elementRenamed(newElement: PsiElement) {
|
||||
MiscProjectUsageCollector.projectRenamed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject.impl
|
||||
|
||||
import com.intellij.internal.statistic.eventLog.EventLogGroup
|
||||
import com.intellij.internal.statistic.eventLog.events.EventId
|
||||
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
|
||||
|
||||
/**
|
||||
* Logs misc project creation and renaming to FUS
|
||||
*/
|
||||
internal object MiscProjectUsageCollector : CounterUsagesCollector() {
|
||||
private val GROUP: EventLogGroup = EventLogGroup("pycharm.misc.project", 2)
|
||||
private val MISC_PROJECT_CREATED: EventId = GROUP.registerEvent("misc.project.created", "Misc project created (user clicked on the button on welcome screen)")
|
||||
private val MISC_PROJECT_RENAMED: EventId = GROUP.registerEvent("misc.project.renamed", "Misc project renamed")
|
||||
|
||||
override fun getGroup(): EventLogGroup = GROUP
|
||||
|
||||
fun projectCreated() {
|
||||
MISC_PROJECT_CREATED.log()
|
||||
}
|
||||
|
||||
fun projectRenamed() {
|
||||
MISC_PROJECT_RENAMED.log()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject.impl
|
||||
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.NlsActions
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
|
||||
import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcome
|
||||
import com.intellij.pycharm.community.ide.impl.miscProject.MiscFileType
|
||||
import com.intellij.pycharm.community.ide.impl.miscProject.TemplateFileName
|
||||
import com.jetbrains.python.psi.icons.PythonPsiApiIcons
|
||||
import javax.swing.Icon
|
||||
|
||||
object MiscScriptFileType : MiscFileType {
|
||||
override val title: @NlsActions.ActionText String = PyCharmCommunityCustomizationBundle.message("misc.script.text")
|
||||
override val icon: Icon = PythonPsiApiIcons.Python_32x32
|
||||
override val fileName: TemplateFileName = TemplateFileName.parse("script.py")
|
||||
|
||||
override suspend fun fillFile(file: PsiFile, sdk: Sdk) {
|
||||
writeAction {
|
||||
PyWelcome.writeText(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject.impl
|
||||
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* How do we get system python?
|
||||
*/
|
||||
sealed interface ObtainPythonStrategy {
|
||||
/**
|
||||
* Find it on the system. If no python found -- [confirmInstallation] and install
|
||||
*/
|
||||
fun interface FindOnSystem : ObtainPythonStrategy {
|
||||
suspend fun confirmInstallation(): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Only use [pythons] provided explicitly
|
||||
*/
|
||||
data class UseThesePythons(val pythons: List<Pair<PythonSdkFlavor<*>, Collection<Path>>>) : ObtainPythonStrategy {
|
||||
init {
|
||||
assert(pythons.flatMap { it.second }.isNotEmpty()) { "When provided explicitly, pythons can't be empty" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject.impl
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.MessageDialogBuilder
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
|
||||
import com.intellij.pycharm.community.ide.impl.miscProject.MiscFileType
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.Result
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Action displayed on welcome screen to create a project by [miscFileType]
|
||||
*/
|
||||
internal class PyMiscFileAction(private val miscFileType: MiscFileType) : AnAction(
|
||||
miscFileType.title,
|
||||
null,
|
||||
miscFileType.icon
|
||||
) {
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
@RequiresEdt
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
MiscProjectUsageCollector.projectCreated()
|
||||
when (val r = createMiscProject(
|
||||
miscFileType,
|
||||
obtainPythonStrategy = object : ObtainPythonStrategy.FindOnSystem {
|
||||
override suspend fun confirmInstallation(): Boolean = withContext(Dispatchers.EDT) {
|
||||
MessageDialogBuilder.yesNo(
|
||||
PyCharmCommunityCustomizationBundle.message("misc.no.python.found"),
|
||||
PyCharmCommunityCustomizationBundle.message("misc.install.python.question")
|
||||
).ask(e.project)
|
||||
}
|
||||
},
|
||||
scopeProvider = { it.service<MyService>().scope })) {
|
||||
is Result.Success -> Unit
|
||||
is Result.Failure -> {
|
||||
Messages.showErrorDialog(null as Project?, r.error.text, PyCharmCommunityCustomizationBundle.message("misc.project.error.title"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
private class MyService(val scope: CoroutineScope)
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject.impl
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionGroup
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.pycharm.community.ide.impl.miscProject.MiscFileType
|
||||
|
||||
internal class PyMiscFileActionGroup : ActionGroup() {
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabledAndVisible = miscProjectEnabled.value
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
override fun getChildren(e: AnActionEvent?): Array<out AnAction> =
|
||||
if (miscProjectEnabled.value)
|
||||
(MiscFileType.EP.extensionList + listOf(MiscScriptFileType)).map { PyMiscFileAction(it) }.toTypedArray()
|
||||
else
|
||||
emptyArray()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject.impl
|
||||
|
||||
import com.intellij.platform.experiment.ab.impl.experiment.ABExperimentOption
|
||||
import com.intellij.platform.experiment.ab.impl.experiment.ABExperimentOptionId
|
||||
import com.intellij.platform.experiment.ab.impl.option.ABExperimentOptionGroupSize
|
||||
|
||||
/**
|
||||
* A/B testing option for misc pycharm project
|
||||
*/
|
||||
internal class PyMiscProjectExperimentOption : ABExperimentOption {
|
||||
override val id: ABExperimentOptionId = ABExperimentOptionId("pycharm.miscProject")
|
||||
|
||||
override fun getGroupSizeForIde(isPopularIde: Boolean): ABExperimentOptionGroupSize = ABExperimentOptionGroupSize(128) // half: 256/2
|
||||
|
||||
override fun checkIdeIsSuitable(): Boolean = true // This module only goes to PyCharm
|
||||
|
||||
override fun checkIdeVersionIsSuitable(): Boolean = true
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject.impl
|
||||
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.configurations.GeneralCommandLine
|
||||
import com.intellij.ide.impl.OpenProjectTask
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.readAction
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectBundle
|
||||
import com.intellij.openapi.project.ex.ProjectManagerEx
|
||||
import com.intellij.openapi.project.modules
|
||||
import com.intellij.openapi.project.rootManager
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.ModuleRootModificationUtil
|
||||
import com.intellij.openapi.vfs.VfsUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.platform.experiment.ab.impl.experiment.ABExperiment
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.TaskCancellation
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.intellij.platform.util.progress.withProgressText
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
|
||||
import com.intellij.pycharm.community.ide.impl.miscProject.MiscFileType
|
||||
import com.intellij.pycharm.community.ide.impl.miscProject.TemplateFileName
|
||||
import com.intellij.pycharm.community.ide.impl.miscProject.impl.ObtainPythonStrategy.FindOnSystem
|
||||
import com.intellij.pycharm.community.ide.impl.miscProject.impl.ObtainPythonStrategy.UseThesePythons
|
||||
import com.intellij.util.SystemProperties
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.io.awaitExit
|
||||
import com.jetbrains.python.LocalizedErrorString
|
||||
import com.jetbrains.python.PythonModuleTypeBase
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.mapResult
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.PySdkToInstallManager
|
||||
import com.jetbrains.python.sdk.PythonBinary
|
||||
import com.jetbrains.python.sdk.VirtualEnvReader
|
||||
import com.jetbrains.python.sdk.add.v2.createSdk
|
||||
import com.jetbrains.python.sdk.add.v2.createVirtualenv
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.installer.installBinary
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.IOException
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.createFile
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private val logger = fileLogger()
|
||||
|
||||
internal val miscProjectDefaultPath: Lazy<Path> = lazy { Path.of(SystemProperties.getUserHome()).resolve("PyCharmMiscProject") }
|
||||
internal val miscProjectEnabled: Lazy<Boolean> = lazy { ABExperiment.getABExperimentInstance().isExperimentOptionEnabled(PyMiscProjectExperimentOption::class.java) }
|
||||
|
||||
/**
|
||||
* Creates project in [projectPath] in modal window. Once created, uses [scopeProvider] to get scope
|
||||
* to launch [miscFileType] generation in background, returns it as a job.
|
||||
*
|
||||
* Pythons are obtained with [obtainPythonStrategy]
|
||||
*/
|
||||
@RequiresEdt
|
||||
fun createMiscProject(
|
||||
miscFileType: MiscFileType,
|
||||
scopeProvider: (Project) -> CoroutineScope,
|
||||
obtainPythonStrategy: ObtainPythonStrategy,
|
||||
projectPath: Path = miscProjectDefaultPath.value,
|
||||
): Result<Job, LocalizedErrorString> =
|
||||
runWithModalProgressBlocking(ModalTaskOwner.guess(),
|
||||
PyCharmCommunityCustomizationBundle.message("misc.project.generating.env"),
|
||||
TaskCancellation.cancellable()) {
|
||||
createProjectAndSdk(projectPath, obtainPythonStrategy)
|
||||
}.mapResult { (project, sdk) ->
|
||||
Result.Success(scopeProvider(project).launch {
|
||||
withBackgroundProgress(project, PyCharmCommunityCustomizationBundle.message("misc.project.filling.file")) {
|
||||
generateAndOpenFile(projectPath, project, miscFileType, sdk)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private suspend fun generateAndOpenFile(projectPath: Path, project: Project, fileType: MiscFileType, sdk: Sdk): PsiFile {
|
||||
val generateFile = generateFile(projectPath, fileType.fileName)
|
||||
val psiFile = openFile(project, generateFile)
|
||||
fileType.fillFile(psiFile, sdk)
|
||||
return psiFile
|
||||
}
|
||||
|
||||
private suspend fun openFile(project: Project, file: Path): PsiFile {
|
||||
val vfsFile = withContext(Dispatchers.IO) {
|
||||
VfsUtil.findFile(file, true) ?: error("Can't find VFS $file")
|
||||
}
|
||||
// `navigate` throws `AssertionError` from time to time due to platform API bug.
|
||||
// We "fix" it by means of retries
|
||||
return callWithRetry {
|
||||
withContext(Dispatchers.EDT) {
|
||||
val psiFile = readAction { PsiManager.getInstance(project).findFile(vfsFile) } ?: error("Can't find PSI for $vfsFile")
|
||||
psiFile.navigate(true)
|
||||
|
||||
return@withContext psiFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries [code] `10` times if it throws [AssertionError]
|
||||
*/
|
||||
private suspend fun <T> callWithRetry(code: suspend () -> T): T {
|
||||
val logger = fileLogger()
|
||||
repeat(10) {
|
||||
try {
|
||||
return code()
|
||||
}
|
||||
catch (e: AssertionError) {
|
||||
logger.warn(e)
|
||||
delay(100.milliseconds)
|
||||
}
|
||||
}
|
||||
return code()
|
||||
}
|
||||
|
||||
private suspend fun generateFile(where: Path, templateFileName: TemplateFileName): Path = withContext(Dispatchers.IO) {
|
||||
repeat(Int.MAX_VALUE) {
|
||||
val file = where.resolve(templateFileName.nameWithSuffix(it))
|
||||
try {
|
||||
file.createFile()
|
||||
return@withContext file
|
||||
}
|
||||
catch (_: FileAlreadyExistsException) {
|
||||
}
|
||||
}
|
||||
error("Too many files in $where")
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates project with 1 module in [projectPath] and sdk using the highest python.
|
||||
* Pythons are searched in system ([findPythonsOnSystem]) or provided explicitly (depends on [obtainPythonStrategy]).
|
||||
* In former case if no python were found, we [installLatestPython] (not in a latter case, though).
|
||||
*/
|
||||
private suspend fun createProjectAndSdk(
|
||||
projectPath: Path,
|
||||
obtainPythonStrategy: ObtainPythonStrategy,
|
||||
): Result<Pair<Project, Sdk>, LocalizedErrorString> {
|
||||
val projectPathVfs = createProjectDir(projectPath).getOr { return it }
|
||||
val venvDirPath = projectPath.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME)
|
||||
|
||||
// Find venv in project
|
||||
var venvPython: PythonBinary? = findExistingVenv(venvDirPath)
|
||||
|
||||
if (venvPython == null) {
|
||||
// No venv found -- find system python to create venv
|
||||
val systemPythonBinary = getSystemPython(obtainPythonStrategy).getOr { return it }
|
||||
logger.info("no venv in $venvDirPath, using system python $systemPythonBinary to create venv")
|
||||
// create venv using this system python
|
||||
createVenv(systemPythonBinary, venvDirPath = venvDirPath, projectPath = projectPath).getOr { return it }
|
||||
// try to find venv again
|
||||
venvPython = findExistingVenv(venvDirPath)
|
||||
if (venvPython == null) {
|
||||
// No venv even after venv installation
|
||||
return Result.failure(LocalizedErrorString(PyCharmCommunityCustomizationBundle.message("misc.project.error.create.venv", "", venvDirPath)))
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("using venv python $venvPython")
|
||||
val project = openProject(projectPath)
|
||||
val sdk = getSdk(venvPython, project)
|
||||
val module = project.modules.first()
|
||||
ensureModuleHasRoot(module, projectPathVfs)
|
||||
ModuleRootModificationUtil.setModuleSdk(module, sdk)
|
||||
return Result.Success(Pair(project, sdk))
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for existing venv in [venvDirPath] and make sure it is usable.
|
||||
* `null` means no venv or venv is broken (it doesn't report its version)
|
||||
*/
|
||||
private suspend fun findExistingVenv(
|
||||
venvDirPath: Path,
|
||||
): PythonBinary? = withContext(Dispatchers.IO) {
|
||||
val pythonPath = VirtualEnvReader.Instance.findPythonInPythonRoot(venvDirPath) ?: return@withContext null
|
||||
val flavor = PythonSdkFlavor.tryDetectFlavorByLocalPath(pythonPath.toString())
|
||||
if (flavor == null) {
|
||||
logger.warn("No flavor found for $pythonPath")
|
||||
return@withContext null
|
||||
}
|
||||
if (validatePythonAndGetVersion(pythonPath, flavor) == null) {
|
||||
logger.warn("No version string. python seems to be broken: $pythonPath")
|
||||
return@withContext null
|
||||
}
|
||||
return@withContext pythonPath
|
||||
}
|
||||
|
||||
private suspend fun createVenv(systemPython: PythonBinary, venvDirPath: Path, projectPath: Path): Result<Unit, LocalizedErrorString> =
|
||||
try {
|
||||
createVirtualenv(systemPython, venvDirPath, projectPath)
|
||||
Result.success(Unit)
|
||||
}
|
||||
catch (e: ExecutionException) {
|
||||
Result.failure(LocalizedErrorString(PyCharmCommunityCustomizationBundle.message("misc.project.error.create.venv", e.toString(), venvDirPath)))
|
||||
}
|
||||
|
||||
private suspend fun getSystemPython(obtainPythonStrategy: ObtainPythonStrategy): Result<PythonBinary, LocalizedErrorString> {
|
||||
// First, find the latest python according to strategy
|
||||
var systemPythonBinary = filterLatestUsablePython(
|
||||
when (obtainPythonStrategy) {
|
||||
is UseThesePythons -> obtainPythonStrategy.pythons
|
||||
is FindOnSystem -> findPythonsOnSystem()
|
||||
})
|
||||
|
||||
// No python found?
|
||||
if (systemPythonBinary == null) {
|
||||
// Only install if pythons weren't provided explicitly, see fun doc
|
||||
when (obtainPythonStrategy) {
|
||||
is UseThesePythons -> Unit
|
||||
is FindOnSystem -> {
|
||||
// User is ok with installation
|
||||
if (obtainPythonStrategy.confirmInstallation()) {
|
||||
// Install
|
||||
installLatestPython().onFailure { exception ->
|
||||
// Failed to install python?
|
||||
logger.warn("Python installation failed", exception)
|
||||
return Result.Failure(LocalizedErrorString(
|
||||
PyCharmCommunityCustomizationBundle.message("misc.project.error.install.python", exception.toString())))
|
||||
}
|
||||
// Find latest python again, after installation
|
||||
systemPythonBinary = filterLatestUsablePython(findPythonsOnSystem())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (systemPythonBinary == null) {
|
||||
Result.Failure(LocalizedErrorString(PyCharmCommunityCustomizationBundle.message("misc.project.error.all.pythons.bad")))
|
||||
}
|
||||
else {
|
||||
Result.Success(systemPythonBinary)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun openProject(projectPath: Path): Project {
|
||||
val projectManager = ProjectManagerEx.getInstanceEx()
|
||||
val project = projectManager.openProjectAsync(projectPath, OpenProjectTask {
|
||||
runConfigurators = false
|
||||
}) ?: error("Failed to open project in $projectPath, check logs")
|
||||
// There are countless number of reasons `openProjectAsync` might return null
|
||||
if (project.modules.isEmpty()) {
|
||||
writeAction {
|
||||
ModuleManager.getInstance(project).newModule(projectPath, PythonModuleTypeBase.getInstance().id)
|
||||
}
|
||||
}
|
||||
return project
|
||||
}
|
||||
|
||||
private suspend fun getSdk(pythonPath: PythonBinary, project: Project): Sdk =
|
||||
withProgressText(ProjectBundle.message("progress.text.configuring.sdk")) {
|
||||
val allJdks = ProjectJdkTable.getInstance().allJdks
|
||||
val currentSdk = allJdks.firstOrNull { sdk -> sdk.homeDirectory?.toNioPath() == pythonPath }
|
||||
if (currentSdk != null) return@withProgressText currentSdk
|
||||
|
||||
val localPythonVfs = withContext(Dispatchers.IO) { VfsUtil.findFile(pythonPath, true)!! }
|
||||
return@withProgressText createSdk(localPythonVfs, project.basePath?.let { Path.of(it) }, allJdks)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creating project != creating directory for it, but we need directory to create template file
|
||||
*/
|
||||
private suspend fun createProjectDir(projectPath: Path): Result<VirtualFile, LocalizedErrorString> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
projectPath.createDirectories()
|
||||
}
|
||||
catch (e: IOException) {
|
||||
thisLogger().warn("Couldn't create $projectPath", e)
|
||||
return@withContext Result.Failure(LocalizedErrorString(
|
||||
PyCharmCommunityCustomizationBundle.message("misc.project.error.create.dir", projectPath, e.localizedMessage)))
|
||||
}
|
||||
val projectPathVfs = VfsUtil.findFile(projectPath, true)
|
||||
?: error("Can't find VFS $projectPath")
|
||||
return@withContext Result.Success(projectPathVfs)
|
||||
}
|
||||
|
||||
private suspend fun ensureModuleHasRoot(module: Module, root: VirtualFile): Unit = writeAction {
|
||||
with(module.rootManager.modifiableModel) {
|
||||
try {
|
||||
if (root in contentRoots) return@writeAction
|
||||
addContentEntry(root)
|
||||
}
|
||||
finally {
|
||||
commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for system pythons. Returns flavor and all its pythons.
|
||||
*/
|
||||
fun findPythonsOnSystem(): List<Pair<PythonSdkFlavor<*>, Collection<Path>>> =
|
||||
PythonSdkFlavor.getApplicableFlavors(false) //system=platform dependent (exclude venv)
|
||||
.map { flavor ->
|
||||
flavor.dropCaches()
|
||||
flavor to flavor.suggestLocalHomePaths(null, null)
|
||||
}
|
||||
.filter { (_, pythons) ->
|
||||
pythons.isNotEmpty() // No need to have flavors without pythons
|
||||
}
|
||||
|
||||
suspend fun installLatestPython(): kotlin.Result<Unit> = withContext(Dispatchers.IO) {
|
||||
val pythonToInstall = PySdkToInstallManager.getAvailableVersionsToInstall().toSortedMap().values.last()
|
||||
return@withContext withContext(Dispatchers.EDT) {
|
||||
installBinary(pythonToInstall, null) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for the latest python among [flavorsToPythons]: each flavour might have 1 or more pythons.
|
||||
* Broken pythons are filtered out. If `null` is returned, no python found, you probably need to [installLatestPython]
|
||||
*/
|
||||
private suspend fun filterLatestUsablePython(flavorsToPythons: List<Pair<PythonSdkFlavor<*>, Collection<Path>>>): PythonBinary? {
|
||||
var current: Pair<LanguageLevel, Path>? = null
|
||||
for ((flavor, paths) in flavorsToPythons) {
|
||||
for (pythonPath in paths) {
|
||||
val versionString = validatePythonAndGetVersion(pythonPath, flavor) ?: continue
|
||||
val languageLevel = flavor.getLanguageLevelFromVersionString(versionString)
|
||||
|
||||
// Highest possible, no need to search further
|
||||
if (languageLevel == LanguageLevel.getLatest()) {
|
||||
return pythonPath
|
||||
}
|
||||
if (current == null || current.first < languageLevel) {
|
||||
// More recent Python found!
|
||||
current = Pair(languageLevel, pythonPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return current?.second
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that [pythonBinary] is executable and returns its version. `null` if python is broken (reports error to logs).
|
||||
*
|
||||
* Some pythons might be broken: they may be executable, even return a version, but still fail to execute it.
|
||||
* As we need workable pythons, we validate it by executing
|
||||
*/
|
||||
private suspend fun validatePythonAndGetVersion(pythonBinary: PythonBinary, flavor: PythonSdkFlavor<*>): String? = withContext(Dispatchers.IO) {
|
||||
val fileLogger = fileLogger()
|
||||
val process =
|
||||
try {
|
||||
GeneralCommandLine(pythonBinary.toString(), "-c", "print(1)").createProcess()
|
||||
}
|
||||
catch (e: ExecutionException) {
|
||||
fileLogger.warn("$pythonBinary is bad, skipping", e)
|
||||
return@withContext null
|
||||
}
|
||||
val timeout = 5.seconds
|
||||
return@withContext withTimeoutOrNull(timeout) {
|
||||
val exitCode = process.awaitExit()
|
||||
return@withTimeoutOrNull if (exitCode != 0) {
|
||||
fileLogger.warn("$pythonBinary returned $exitCode, skipping")
|
||||
null
|
||||
}
|
||||
else {
|
||||
flavor.getVersionString(pythonBinary.pathString)
|
||||
}
|
||||
}.also {
|
||||
if (it == null) {
|
||||
fileLogger.warn("$pythonBinary didn't return in $timeout, skipping")
|
||||
process.destroyForcibly()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@ApiStatus.Internal
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
/**
|
||||
* See `PY-75549`
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
package com.intellij.pycharm.community.ide.impl.miscProject;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -41,12 +41,14 @@ import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcom
|
||||
import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeCollector.RunConfigurationResult
|
||||
import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeCollector.ScriptResult
|
||||
import com.intellij.pycharm.community.ide.impl.newProjectWizard.welcome.PyWelcomeCollector.logWelcomeRunConfiguration
|
||||
import com.intellij.util.concurrency.annotations.RequiresReadLock
|
||||
import com.intellij.util.concurrency.annotations.RequiresWriteLock
|
||||
import com.intellij.xdebugger.XDebuggerUtil
|
||||
import com.jetbrains.python.PythonPluginDisposable
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.run.PythonRunConfigurationProducer
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
import org.jetbrains.annotations.CalledInAny
|
||||
import org.jetbrains.concurrency.CancellablePromise
|
||||
import java.util.concurrent.Callable
|
||||
@@ -199,7 +201,7 @@ internal object PyWelcome {
|
||||
|
||||
val psiFile = PsiManager.getInstance(project).findFile(file) ?: error("File $file was just created, but not found in PSI")
|
||||
|
||||
writeText(project, psiFile)?.also { line ->
|
||||
writeText(psiFile)?.also { line ->
|
||||
PyWelcomeCollector.logWelcomeScript(project, ScriptResult.CREATED)
|
||||
|
||||
XDebuggerUtil.getInstance().toggleLineBreakpoint(project, file, line)
|
||||
@@ -208,7 +210,10 @@ internal object PyWelcome {
|
||||
return psiFile
|
||||
}
|
||||
|
||||
private fun writeText(project: Project, psiFile: PsiFile): Int? {
|
||||
|
||||
@RequiresWriteLock
|
||||
internal fun writeText(psiFile: PsiFile): Int? {
|
||||
val project = psiFile.project
|
||||
val document = PsiDocumentManager.getInstance(project).getDocument(psiFile)
|
||||
if (document == null) {
|
||||
LOG.warn("Unable to get document for ${psiFile.virtualFile}")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python
|
||||
|
||||
import org.jetbrains.annotations.Nls
|
||||
|
||||
/**
|
||||
* Error string to be used with [Result]
|
||||
*/
|
||||
@JvmInline
|
||||
value class LocalizedErrorString(val text: @Nls String)
|
||||
@@ -1,18 +1,46 @@
|
||||
// Copyright 2000-2018 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.jetbrains.python
|
||||
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import com.jetbrains.python.Result.Failure
|
||||
import com.jetbrains.python.Result.Success
|
||||
|
||||
/**
|
||||
* Operation result to be used with pattern matching.
|
||||
* Must be replaced with stdlib solution after [https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/result.md] completion.
|
||||
* Operation result to be used as `Maybe` instead of checked exceptions.
|
||||
* Unlike Kotlin `Result`, [ERR] could be anything (See [LocalizedErrorString]).
|
||||
*
|
||||
* Can't be moved to core module because core modules do not support Kotlin and there is no sealed classes in java.
|
||||
* Typical usages:
|
||||
*
|
||||
* ```kotlin
|
||||
* when(val r = someFun() {
|
||||
* is Result.Success -> r.result // is ok
|
||||
* is Result.Failure -> r.error // is error
|
||||
* }
|
||||
* ```
|
||||
* Get result or throw error (I am 100% sure there is no error): [orThrow].
|
||||
*
|
||||
* Chain several calls, get latest result or first error (all errors are the same): [mapResult].
|
||||
*
|
||||
* When errors are different: [mapResultWithErr]
|
||||
*
|
||||
* Fast return: [getOr]
|
||||
* ```kotlin
|
||||
* fun foo() {
|
||||
* val data = getSomeResult().getOr { return }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Return from function with same error
|
||||
* ```kotlin
|
||||
* fun foo():Result<String, Int> {
|
||||
* // Returns Result<Foo, Int>
|
||||
* getSomeResult().getOr { return it }
|
||||
* }
|
||||
* ```
|
||||
* See showcase in tests.
|
||||
*/
|
||||
sealed class Result<SUCC, ERR> {
|
||||
data class Failure<SUCC, ERR>(val error: ERR) : Result<SUCC, ERR>()
|
||||
data class Success<SUCC, ERR>(val result: SUCC) : Result<SUCC, ERR>()
|
||||
sealed class Result<out SUCC, out ERR> {
|
||||
data class Failure<out ERR>(val error: ERR) : Result<Nothing, ERR>()
|
||||
data class Success<out SUCC>(val result: SUCC) : Result<SUCC, Nothing>()
|
||||
|
||||
fun <RES> map(map: (SUCC) -> RES): Result<RES, ERR> =
|
||||
when (this) {
|
||||
@@ -20,11 +48,70 @@ sealed class Result<SUCC, ERR> {
|
||||
is Failure -> Failure(error)
|
||||
}
|
||||
|
||||
/***
|
||||
* ```kotlin
|
||||
* val data = someFun().getOr { return }
|
||||
* ```
|
||||
*/
|
||||
inline fun getOr(onFailure: (err: Failure<ERR>) -> Nothing): SUCC {
|
||||
when (this) {
|
||||
is Failure -> onFailure(this)
|
||||
is Success -> return result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Same as [mapResult] but for different errors
|
||||
* ```kotlin
|
||||
* val drinkResultOrFirstError = findBeer()
|
||||
* .mapResult{ openBeer(it) }
|
||||
* .mapResultWithErr(
|
||||
* onSuccess = { drink(it) },
|
||||
* onErr = { LocalizedErrorString("Oops, ${it.message}") }
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
inline fun <NEW_ERR, NEW_S> mapResultWithErr(
|
||||
onSuccess: (SUCC) -> Result<NEW_S, NEW_ERR>,
|
||||
onErr: (ERR) -> NEW_ERR,
|
||||
): Result<NEW_S, NEW_ERR> =
|
||||
when (this) {
|
||||
is Success -> onSuccess(result)
|
||||
is Failure -> Failure(onErr(error))
|
||||
}
|
||||
|
||||
val successOrNull: SUCC? get() = if (this is Success) result else null
|
||||
fun orThrow(onError: (ERR) -> Throwable = { e -> AssertionError(e) }): SUCC {
|
||||
|
||||
|
||||
/**
|
||||
* Like Rust `unwrap`: returns result or throws exception. Use when error is unexpected
|
||||
*/
|
||||
fun orThrow(onError: (ERR) -> Throwable = { e -> if (e is Throwable) e else AssertionError(e) }): SUCC {
|
||||
when (this) {
|
||||
is Success -> return result
|
||||
is Failure -> throw onError(this.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To be backward compatible with Kotlin result
|
||||
companion object {
|
||||
fun <S> success(value: S) = Success(value)
|
||||
fun <E> failure(error: E) = Failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps success result to another one with same error
|
||||
* ```kotlin
|
||||
* val drinkResultOrFirstError = findBeer()
|
||||
* .mapResult{ openBeer(it) }
|
||||
* .mapResult{ drinkIt(it) }
|
||||
* ```
|
||||
*/
|
||||
fun <SUCC, NEW_S, ERR> Result<SUCC, ERR>.mapResult(map: (SUCC) -> Result<NEW_S, ERR>): Result<NEW_S, ERR> =
|
||||
when (this) {
|
||||
is Success -> map(result)
|
||||
is Failure -> this
|
||||
}
|
||||
@@ -26,4 +26,5 @@ public final class PythonPsiApiIcons {
|
||||
/** 16x16 */ public static final @NotNull Icon PropertyGetter = load("icons/com/jetbrains/python/psi/propertyGetter.svg", 1495604199, 2);
|
||||
/** 16x16 */ public static final @NotNull Icon PropertySetter = load("icons/com/jetbrains/python/psi/propertySetter.svg", -1451064081, 2);
|
||||
/** 16x16 */ public static final @NotNull Icon Python = load("icons/com/jetbrains/python/psi/python.svg", 2008591516, 8);
|
||||
/** 32x32 */ public static final @NotNull Icon Python_32x32 = load("icons/com/jetbrains/python/psi/python@32x32.svg", -2111231783, 2);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 2C22 2 22 4 22 8L22 13C22 14.6569 20.6569 16 19 16H13C10.2386 16 8 18.2386 8 21V22C4 22 2 22 2 16C2 9.99997 4 9.99997 8 9.99997L15 10C15.5523 10 16 9.55228 16 9C16 8.44771 15.5523 8 15 8H10C10 4 10 2 16 2ZM13 6C13.5523 6 14 5.55228 14 5C14 4.44772 13.5523 4 13 4C12.4477 4 12 4.44772 12 5C12 5.55228 12.4477 6 13 6Z" fill="#4682FA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 10V13C24 15.7614 21.7614 18 19 18H13C11.3431 18 10 19.3431 10 21L10 24C9.99893 28 10 30 16 30C22 30 22 28 22 24.0001L17 24C16.4477 24 16 23.5523 16 23C16 22.4477 16.4477 22 17 22L24 22C28 22.0011 30 22 30 16C30 10 27.9999 10 24 10ZM19 28C19.5523 28 20 27.5523 20 27C20 26.4477 19.5523 26 19 26C18.4477 26 18 26.4477 18 27C18 27.5523 18.4477 28 19 28Z" fill="#FFAF0F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 2C22 2 22 4 22 8L22 13C22 14.6569 20.6569 16 19 16H13C10.2386 16 8 18.2386 8 21V22C4 22 2 22 2 16C2 9.99997 4 9.99997 8 9.99997L15 10C15.5523 10 16 9.55228 16 9C16 8.44771 15.5523 8 15 8H10C10 4 10 2 16 2ZM13 6C13.5523 6 14 5.55228 14 5C14 4.44772 13.5523 4 13 4C12.4477 4 12 4.44772 12 5C12 5.55228 12.4477 6 13 6Z" fill="#548AF7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 10V13C24 15.7614 21.7614 18 19 18H13C11.3431 18 10 19.3431 10 21L10 24C9.99893 28 10 30 16 30C22 30 22 28 22 24.0001L17 24C16.4477 24 16 23.5523 16 23C16 22.4477 16.4477 22 17 22L24 22C28 22.0011 30 22 30 16C30 10 27.9999 10 24 10ZM19 28C19.5523 28 20 27.5523 20 27C20 26.4477 19.5523 26 19 26C18.4477 26 18 26.4477 18 27C18 27.5523 18.4477 28 19 28Z" fill="#F2C55C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -22,6 +22,7 @@ import com.jetbrains.python.psi.LanguageLevel;
|
||||
import com.jetbrains.python.psi.icons.PythonPsiApiIcons;
|
||||
import com.jetbrains.python.run.CommandLinePatcher;
|
||||
import com.jetbrains.python.sdk.*;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -99,6 +100,14 @@ public abstract class PythonSdkFlavor<D extends PyFlavorData> {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flavor might cache results of {@link #suggestLocalHomePaths(Module, UserDataHolder)}
|
||||
* This method resets them
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public void resetHomePathCache() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Flavor is added to result in {@link #getApplicableFlavors()} if this method returns true.
|
||||
|
||||
@@ -74,7 +74,7 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
|
||||
|
||||
override fun validate(baseDirPath: String): ValidationResult =
|
||||
when (val pathOrError = validatePath(baseDirPath)) {
|
||||
is Result.Success<Path, *> -> {
|
||||
is Result.Success -> {
|
||||
ValidationResult.OK
|
||||
}
|
||||
is Result.Failure -> ValidationResult(pathOrError.error)
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.intellij.openapi.module.ModuleUtil
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.vfs.VfsUtil
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.TaskCancellation
|
||||
@@ -19,7 +18,6 @@ import com.jetbrains.python.sdk.VirtualEnvReader
|
||||
import com.jetbrains.python.sdk.conda.createCondaSdkFromExistingEnv
|
||||
import com.jetbrains.python.sdk.excludeInnerVirtualEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
|
||||
import com.jetbrains.python.sdk.suggestAssociatedSdkName
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
@@ -66,15 +64,7 @@ suspend fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Pat
|
||||
return failure(message("commandLine.directoryCantBeAccessed", venvPath))
|
||||
}
|
||||
|
||||
// "suggest name" calls external process and can't be called from EDT
|
||||
val newSdk = withContext(Dispatchers.IO) {
|
||||
val suggestedName = /*suggestedSdkName ?:*/ suggestAssociatedSdkName(homeFile.path, projectPath.toString())
|
||||
SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), homeFile,
|
||||
PythonSdkType.getInstance(),
|
||||
false, null, suggestedName)!!
|
||||
}
|
||||
|
||||
addSdk(newSdk)
|
||||
val newSdk = createSdk(homeFile, projectPath, existingSdks.toTypedArray())
|
||||
|
||||
// todo check exclude
|
||||
ProjectManager.getInstance().openProjects
|
||||
|
||||
@@ -4,7 +4,39 @@ package com.jetbrains.python.sdk.add.v2
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.suggestAssociatedSdkName
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Creates and persists sdk.
|
||||
* [projectPath] is used to suggest name
|
||||
*/
|
||||
suspend fun createSdk(
|
||||
pythonBinaryPath: VirtualFile,
|
||||
projectPath: Path?,
|
||||
existingSdks: Array<Sdk>,
|
||||
): Sdk {
|
||||
val newSdk = withContext(Dispatchers.IO) {
|
||||
// "suggest name" calls external process and can't be called from EDT
|
||||
val suggestedName = /*suggestedSdkName ?:*/ suggestAssociatedSdkName(pythonBinaryPath.path, projectPath?.toString())
|
||||
SdkConfigurationUtil.setupSdk(existingSdks, pythonBinaryPath,
|
||||
PythonSdkType.getInstance(),
|
||||
null, suggestedName)
|
||||
}
|
||||
|
||||
addSdk(newSdk)
|
||||
return newSdk
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Persists [sdk]
|
||||
*/
|
||||
internal suspend fun addSdk(sdk: Sdk) {
|
||||
writeAction {
|
||||
ProjectJdkTable.getInstance().addJdk(sdk)
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.jetbrains.python.run.PythonExecution
|
||||
import com.jetbrains.python.run.prepareHelperScriptExecution
|
||||
import com.jetbrains.python.run.target.HelpersAwareLocalTargetEnvironmentRequest
|
||||
import com.jetbrains.python.sdk.PySdkSettings
|
||||
import com.jetbrains.python.sdk.PythonBinary
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
@@ -26,7 +27,7 @@ import java.nio.file.Path
|
||||
@Throws(ExecutionException::class)
|
||||
@Internal
|
||||
suspend fun createVirtualenv(
|
||||
baseInterpreterPath: Path,
|
||||
baseInterpreterPath: PythonBinary,
|
||||
venvRoot: Path,
|
||||
projectBasePath: Path,
|
||||
inheritSitePackages: Boolean = false,
|
||||
|
||||
@@ -84,6 +84,11 @@ public class WinPythonSdkFlavor extends CPythonSdkFlavor<PyFlavorData.Empty> {
|
||||
candidates.addAll(myAppxCache.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void resetHomePathCache() {
|
||||
myRegistryCache.drop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sdkSeemsValid(@NotNull Sdk sdk,
|
||||
PyFlavorData.@NotNull Empty flavorData,
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.jetbrains.python.sdk.conda.TargetEnvironmentRequestCommandExecutor
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import java.nio.file.Path
|
||||
@@ -33,17 +34,28 @@ typealias PathToPythonBinary = Path
|
||||
*/
|
||||
sealed class PythonType<T : Any>(private val tag: @NonNls String) {
|
||||
|
||||
suspend fun getTestEnvironment(vararg additionalTags: @NonNls String): Result<Pair<T, AutoCloseable>> =
|
||||
/**
|
||||
* Returns all test environments: each must be closed after the test.
|
||||
*/
|
||||
suspend fun getTestEnvironments(vararg additionalTags: @NonNls String): Flow<Pair<T, AutoCloseable>> =
|
||||
PyEnvTestSettings
|
||||
.fromEnvVariables()
|
||||
.pythons
|
||||
.asFlow()
|
||||
.map { it.toPath() }
|
||||
.firstOrNull { typeMatchesEnv(it, *additionalTags) }
|
||||
?.let { envDir ->
|
||||
Result.success(pythonPathToEnvironment(
|
||||
.filter { typeMatchesEnv(it, *additionalTags) }
|
||||
.map { envDir ->
|
||||
pythonPathToEnvironment(
|
||||
VirtualEnvReader.Instance.findPythonInPythonRoot(envDir)
|
||||
?: error("Can't find python binary in $envDir"), envDir)) // This is a misconfiguration, hence an error
|
||||
?: error("Can't find python binary in $envDir"), envDir) // This is a misconfiguration, hence an error
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns first (whatever it means) test environment and closable that must be closed after the test
|
||||
*/
|
||||
suspend fun getTestEnvironment(vararg additionalTags: @NonNls String): Result<Pair<T, AutoCloseable>> =
|
||||
getTestEnvironments(*additionalTags).firstOrNull()?.let { Result.success(it) }
|
||||
?: failure("No python found. See ${PyEnvTestSettings::class} class for more info")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user