Skip to content

Commit

Permalink
Merge #5715
Browse files Browse the repository at this point in the history
5715: PRJ: Project generation with cargo-generate r=ortem a=avrong

Currently, it is project generation using cargo-generate merged with standard cargo templates.

## CLion
<img width="912" alt="image" src="https://user-images.githubusercontent.com/6342851/88849247-4baad880-d1f2-11ea-8eb6-17f8e9b178ab.png">

## IntelliJ IDEA
<img width="929" alt="Frame 1(3)" src="https://user-images.githubusercontent.com/6342851/88853074-dc37e780-d1f7-11ea-8570-bcdef26ef108.png">

Related to #3066

Additionally:
Closes #5746


Co-authored-by: Aleksei Trifonov <[email protected]>
  • Loading branch information
bors[bot] and avrong authored Aug 4, 2020
2 parents 2d3be44 + d8b18f7 commit ea93932
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class CargoConfigurationWizardStep private constructor(
private val projectDescriptor: ProjectDescriptor? = null
) : ModuleWizardStep() {

private val newProjectPanel = RsNewProjectPanel(showProjectTypeCheckbox = projectDescriptor == null)
private val newProjectPanel = RsNewProjectPanel(showProjectTypeSelection = projectDescriptor == null)

override fun getComponent(): JComponent = layout {
newProjectPanel.attachTo(this)
Expand Down
22 changes: 14 additions & 8 deletions idea/src/main/kotlin/org/rust/ide/idea/RsModuleBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.util.Disposer
import org.rust.cargo.toolchain.RustToolchain
import org.rust.ide.newProject.ConfigurationData
import org.rust.ide.newProject.RsPackageNameValidator
import org.rust.ide.newProject.makeProject
import org.rust.ide.newProject.openFiles

/**
* Builder which is used when a new project or module is created and not imported from source.
Expand Down Expand Up @@ -46,12 +47,18 @@ class RsModuleBuilder : ModuleBuilder() {
// TODO: rewrite this somehow to fix `Synchronous execution on EDT` exception
// The problem is that `setupRootModel` is called on EDT under write action
// so `$ cargo init` invocation blocks UI thread
toolchain.rawCargo().init(
modifiableRootModel.project,

val template = configurationData?.template ?: return
val cargo = toolchain.rawCargo()
val project = modifiableRootModel.project

val generatedFiles = cargo.makeProject(
project,
modifiableRootModel.module,
root,
configurationData?.createBinary ?: true
)
root, template
) ?: return

project.openFiles(generatedFiles)
} catch (e: ExecutionException) {
LOG.error(e)
throw ConfigurationException(e.message)
Expand All @@ -61,8 +68,7 @@ class RsModuleBuilder : ModuleBuilder() {

@Throws(ConfigurationException::class)
override fun validateModuleName(moduleName: String): Boolean {
val isBinary = configurationData?.createBinary == true
val errorMessage = RsPackageNameValidator.validate(moduleName, isBinary) ?: return true
val errorMessage = configurationData?.template?.validateProjectName(moduleName) ?: return true
throw ConfigurationException(errorMessage)
}

Expand Down
72 changes: 62 additions & 10 deletions src/main/kotlin/org/rust/cargo/toolchain/Cargo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import org.rust.ide.experiments.RsExperiments
import org.rust.ide.notifications.showBalloon
import org.rust.openapiext.*
import org.rust.stdext.buildList
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
Expand Down Expand Up @@ -179,7 +180,7 @@ class Cargo(private val cargoExecutable: Path, private val rustcExecutable: Path
for (line in processOutput.stdoutLines) {
val jsonObject = try {
JsonParser.parseString(line).asJsonObject
} catch (ignore: JsonSyntaxException){
} catch (ignore: JsonSyntaxException) {
continue
}
val message = BuildScriptMessage.fromJson(jsonObject) ?: continue
Expand Down Expand Up @@ -240,6 +241,44 @@ class Cargo(private val cargoExecutable: Path, private val rustcExecutable: Path
return GeneratedFilesHolder(manifest, sourceFiles)
}

@Throws(ExecutionException::class)
fun generate(
project: Project,
owner: Disposable,
directory: VirtualFile,
template: String
): GeneratedFilesHolder? {
val path = directory.pathAsPath
val name = path.fileName.toString().replace(' ', '_')
val args = mutableListOf("--name", name, "--git", template)

// TODO: Rewrite this for the future versions of cargo-generate when init subcommand will be available
// See https://github.com/ashleygwilliams/cargo-generate/issues/193

// Generate a cargo-generate project inside a subdir
CargoCommandLine("generate", path, args).execute(project, owner)

// Move all the generated files to the project root and delete the subdir itself
val generatedDir = try {
File(path.toString(), project.name)
} catch (e: NullPointerException) {
LOG.warn("Failed to generate project using cargo-generate")
return null
}
val generatedFiles = generatedDir.walk().drop(1) // drop the `generatedDir` itself
for (generatedFile in generatedFiles) {
val newFile = File(path.toString(), generatedFile.name)
Files.move(generatedFile.toPath(), newFile.toPath())
}
generatedDir.delete()

fullyRefreshDirectory(directory)

val manifest = checkNotNull(directory.findChild(RustToolchain.CARGO_TOML)) { "Can't find the manifest file" }
val sourceFiles = listOf("main", "lib").mapNotNull { directory.findFileByRelativePath("src/${it}.rs") }
return GeneratedFilesHolder(manifest, sourceFiles)
}

@Throws(ExecutionException::class)
fun checkProject(
project: Project,
Expand Down Expand Up @@ -313,6 +352,25 @@ class Cargo(private val cargoExecutable: Path, private val rustcExecutable: Path
listener: ProcessListener? = null
): ProcessOutput = toGeneralCommandLine(project, this).execute(owner, ignoreExitCode, stdIn, listener)

fun installCargoGenerate(owner: Disposable, listener: ProcessListener) {
GeneralCommandLine(cargoExecutable)
.withParameters(listOf("install", "cargo-generate"))
.execute(owner, listener = listener)
}

fun checkNeedInstallCargoGenerate(): Boolean {
val crateName = "cargo-generate"
val minVersion = SemVer("v0.5.0", 0, 5, 0)
return checkBinaryCrateIsNotInstalled(crateName, minVersion)
}

private fun checkBinaryCrateIsNotInstalled(crateName: String, minVersion: SemVer?): Boolean {
val installed = listInstalledBinaryCrates().any { (name, version) ->
name == crateName && (minVersion == null || version != null && version >= minVersion)
}
return !installed
}

private var _http: HttpConfigurable? = null
private val http: HttpConfigurable
get() = _http ?: HttpConfigurable.getInstance()
Expand Down Expand Up @@ -435,16 +493,10 @@ class Cargo(private val cargoExecutable: Path, private val rustcExecutable: Path
message: String? = null,
minVersion: SemVer? = null
): Boolean {
fun isNotInstalled(): Boolean {
val cargo = project.toolchain?.rawCargo() ?: return false
val installed = cargo.listInstalledBinaryCrates().any { (name, version) ->
name == crateName && (minVersion == null || version != null && version >= minVersion)
}
return !installed
}

val cargo = project.toolchain?.rawCargo() ?: return false
val isNotInstalled = { cargo.checkBinaryCrateIsNotInstalled(crateName, minVersion) }
val needInstall = if (isDispatchThread) {
project.computeWithCancelableProgress("Checking if $crateName is installed...", ::isNotInstalled)
project.computeWithCancelableProgress("Checking if $crateName is installed...", isNotInstalled)
} else {
isNotInstalled()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ import org.rust.cargo.project.settings.ui.RustProjectSettingsPanel

data class ConfigurationData(
val settings: RustProjectSettingsPanel.Data,
val createBinary: Boolean
val template: RsProjectTemplate
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
package org.rust.ide.newProject

import com.intellij.facet.ui.ValidationResult
import com.intellij.ide.util.PsiNavigationSupport
import com.intellij.ide.util.projectWizard.AbstractNewProjectStep
import com.intellij.ide.util.projectWizard.CustomStepProjectGenerator
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.impl.welcomeScreen.AbstractActionWithPanel
import com.intellij.openapiext.isHeadlessEnvironment
import com.intellij.platform.DirectoryProjectGenerator
import com.intellij.platform.DirectoryProjectGeneratorBase
import com.intellij.platform.ProjectGeneratorPeer
Expand All @@ -35,25 +33,19 @@ class RsDirectoryProjectGenerator : DirectoryProjectGeneratorBase<ConfigurationD

override fun validate(baseDirPath: String): ValidationResult {
val crateName = File(baseDirPath).nameWithoutExtension
val isBinary = peer?.settings?.createBinary == true
val message = RsPackageNameValidator.validate(crateName, isBinary) ?: return ValidationResult.OK
val message = peer?.settings?.template?.validateProjectName(crateName) ?: return ValidationResult.OK
return ValidationResult(message)
}

override fun generateProject(project: Project, baseDir: VirtualFile, data: ConfigurationData, module: Module) {
val (settings, createBinary) = data
val (settings, template) = data
val cargo = settings.toolchain?.rawCargo() ?: return

val generatedFiles = project.computeWithCancelableProgress("Generating Cargo project...") {
settings.toolchain?.rawCargo()?.init(project, module, baseDir, createBinary)
cargo.makeProject(project, module, baseDir, template)
} ?: return

// Open new files
if (!isHeadlessEnvironment) {
val navigation = PsiNavigationSupport.getInstance()
navigation.createNavigatable(project, generatedFiles.manifest, -1).navigate(false)
for (file in generatedFiles.sourceFiles) {
navigation.createNavigatable(project, file, -1).navigate(true)
}
}
project.openFiles(generatedFiles)
}

override fun createStep(projectGenerator: DirectoryProjectGenerator<ConfigurationData>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import javax.swing.JComponent

class RsProjectGeneratorPeer : GeneratorPeerImpl<ConfigurationData>() {

private val newProjectPanel = RsNewProjectPanel(showProjectTypeCheckbox = true) { checkValid?.run() }
private val newProjectPanel = RsNewProjectPanel(showProjectTypeSelection = true) { checkValid?.run() }
private var checkValid: Runnable? = null

override fun getSettings(): ConfigurationData = newProjectPanel.data
Expand Down
26 changes: 26 additions & 0 deletions src/main/kotlin/org/rust/ide/newProject/RsProjectTemplate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.rust.ide.newProject

import com.intellij.icons.AllIcons
import org.rust.ide.icons.RsIcons
import javax.swing.Icon

sealed class RsProjectTemplate(val name: String, val isBinary: Boolean) {
abstract val icon: Icon

fun validateProjectName(crateName: String): String? = RsPackageNameValidator.validate(crateName, isBinary)
}

class RsGenericTemplate(name: String, isBinary: Boolean) : RsProjectTemplate(name, isBinary) {
override val icon: Icon = RsIcons.RUST
}

class RsCustomTemplate(name: String, val link: String, isBinary: Boolean = true) : RsProjectTemplate(name, isBinary) {
override val icon: Icon = AllIcons.Vcs.Vendors.Github
val shortLink: String
get() = link.substringAfter("//")
}
35 changes: 35 additions & 0 deletions src/main/kotlin/org/rust/ide/newProject/Utils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.rust.ide.newProject

import com.intellij.ide.util.PsiNavigationSupport
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapiext.isHeadlessEnvironment
import org.rust.cargo.toolchain.Cargo
import org.rust.cargo.toolchain.Cargo.Companion.GeneratedFilesHolder

fun Project.openFiles(files: GeneratedFilesHolder) = invokeLater {
if (!isHeadlessEnvironment) {
val navigation = PsiNavigationSupport.getInstance()
navigation.createNavigatable(this, files.manifest, -1).navigate(false)
for (file in files.sourceFiles) {
navigation.createNavigatable(this, file, -1).navigate(true)
}
}
}

fun Cargo.makeProject(
project: Project,
module: Module,
baseDir: VirtualFile,
template: RsProjectTemplate
): GeneratedFilesHolder? = when (template) {
is RsGenericTemplate -> init(project, module, baseDir, template.isBinary)
is RsCustomTemplate -> generate(project, module, baseDir, template.link)
}
Loading

0 comments on commit ea93932

Please sign in to comment.