Skip to content

Commit ea93932

Browse files
bors[bot]avrong
andauthored
Merge #5715
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]>
2 parents 2d3be44 + d8b18f7 commit ea93932

File tree

10 files changed

+284
-55
lines changed

10 files changed

+284
-55
lines changed

idea/src/main/kotlin/org/rust/ide/idea/CargoConfigurationWizardStep.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class CargoConfigurationWizardStep private constructor(
2828
private val projectDescriptor: ProjectDescriptor? = null
2929
) : ModuleWizardStep() {
3030

31-
private val newProjectPanel = RsNewProjectPanel(showProjectTypeCheckbox = projectDescriptor == null)
31+
private val newProjectPanel = RsNewProjectPanel(showProjectTypeSelection = projectDescriptor == null)
3232

3333
override fun getComponent(): JComponent = layout {
3434
newProjectPanel.attachTo(this)

idea/src/main/kotlin/org/rust/ide/idea/RsModuleBuilder.kt

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import com.intellij.openapi.roots.ModifiableRootModel
1818
import com.intellij.openapi.util.Disposer
1919
import org.rust.cargo.toolchain.RustToolchain
2020
import org.rust.ide.newProject.ConfigurationData
21-
import org.rust.ide.newProject.RsPackageNameValidator
21+
import org.rust.ide.newProject.makeProject
22+
import org.rust.ide.newProject.openFiles
2223

2324
/**
2425
* Builder which is used when a new project or module is created and not imported from source.
@@ -46,12 +47,18 @@ class RsModuleBuilder : ModuleBuilder() {
4647
// TODO: rewrite this somehow to fix `Synchronous execution on EDT` exception
4748
// The problem is that `setupRootModel` is called on EDT under write action
4849
// so `$ cargo init` invocation blocks UI thread
49-
toolchain.rawCargo().init(
50-
modifiableRootModel.project,
50+
51+
val template = configurationData?.template ?: return
52+
val cargo = toolchain.rawCargo()
53+
val project = modifiableRootModel.project
54+
55+
val generatedFiles = cargo.makeProject(
56+
project,
5157
modifiableRootModel.module,
52-
root,
53-
configurationData?.createBinary ?: true
54-
)
58+
root, template
59+
) ?: return
60+
61+
project.openFiles(generatedFiles)
5562
} catch (e: ExecutionException) {
5663
LOG.error(e)
5764
throw ConfigurationException(e.message)
@@ -61,8 +68,7 @@ class RsModuleBuilder : ModuleBuilder() {
6168

6269
@Throws(ConfigurationException::class)
6370
override fun validateModuleName(moduleName: String): Boolean {
64-
val isBinary = configurationData?.createBinary == true
65-
val errorMessage = RsPackageNameValidator.validate(moduleName, isBinary) ?: return true
71+
val errorMessage = configurationData?.template?.validateProjectName(moduleName) ?: return true
6672
throw ConfigurationException(errorMessage)
6773
}
6874

src/main/kotlin/org/rust/cargo/toolchain/Cargo.kt

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import org.rust.ide.experiments.RsExperiments
4343
import org.rust.ide.notifications.showBalloon
4444
import org.rust.openapiext.*
4545
import org.rust.stdext.buildList
46+
import java.io.File
4647
import java.nio.file.Files
4748
import java.nio.file.Path
4849
import java.nio.file.Paths
@@ -179,7 +180,7 @@ class Cargo(private val cargoExecutable: Path, private val rustcExecutable: Path
179180
for (line in processOutput.stdoutLines) {
180181
val jsonObject = try {
181182
JsonParser.parseString(line).asJsonObject
182-
} catch (ignore: JsonSyntaxException){
183+
} catch (ignore: JsonSyntaxException) {
183184
continue
184185
}
185186
val message = BuildScriptMessage.fromJson(jsonObject) ?: continue
@@ -240,6 +241,44 @@ class Cargo(private val cargoExecutable: Path, private val rustcExecutable: Path
240241
return GeneratedFilesHolder(manifest, sourceFiles)
241242
}
242243

244+
@Throws(ExecutionException::class)
245+
fun generate(
246+
project: Project,
247+
owner: Disposable,
248+
directory: VirtualFile,
249+
template: String
250+
): GeneratedFilesHolder? {
251+
val path = directory.pathAsPath
252+
val name = path.fileName.toString().replace(' ', '_')
253+
val args = mutableListOf("--name", name, "--git", template)
254+
255+
// TODO: Rewrite this for the future versions of cargo-generate when init subcommand will be available
256+
// See https://github.com/ashleygwilliams/cargo-generate/issues/193
257+
258+
// Generate a cargo-generate project inside a subdir
259+
CargoCommandLine("generate", path, args).execute(project, owner)
260+
261+
// Move all the generated files to the project root and delete the subdir itself
262+
val generatedDir = try {
263+
File(path.toString(), project.name)
264+
} catch (e: NullPointerException) {
265+
LOG.warn("Failed to generate project using cargo-generate")
266+
return null
267+
}
268+
val generatedFiles = generatedDir.walk().drop(1) // drop the `generatedDir` itself
269+
for (generatedFile in generatedFiles) {
270+
val newFile = File(path.toString(), generatedFile.name)
271+
Files.move(generatedFile.toPath(), newFile.toPath())
272+
}
273+
generatedDir.delete()
274+
275+
fullyRefreshDirectory(directory)
276+
277+
val manifest = checkNotNull(directory.findChild(RustToolchain.CARGO_TOML)) { "Can't find the manifest file" }
278+
val sourceFiles = listOf("main", "lib").mapNotNull { directory.findFileByRelativePath("src/${it}.rs") }
279+
return GeneratedFilesHolder(manifest, sourceFiles)
280+
}
281+
243282
@Throws(ExecutionException::class)
244283
fun checkProject(
245284
project: Project,
@@ -313,6 +352,25 @@ class Cargo(private val cargoExecutable: Path, private val rustcExecutable: Path
313352
listener: ProcessListener? = null
314353
): ProcessOutput = toGeneralCommandLine(project, this).execute(owner, ignoreExitCode, stdIn, listener)
315354

355+
fun installCargoGenerate(owner: Disposable, listener: ProcessListener) {
356+
GeneralCommandLine(cargoExecutable)
357+
.withParameters(listOf("install", "cargo-generate"))
358+
.execute(owner, listener = listener)
359+
}
360+
361+
fun checkNeedInstallCargoGenerate(): Boolean {
362+
val crateName = "cargo-generate"
363+
val minVersion = SemVer("v0.5.0", 0, 5, 0)
364+
return checkBinaryCrateIsNotInstalled(crateName, minVersion)
365+
}
366+
367+
private fun checkBinaryCrateIsNotInstalled(crateName: String, minVersion: SemVer?): Boolean {
368+
val installed = listInstalledBinaryCrates().any { (name, version) ->
369+
name == crateName && (minVersion == null || version != null && version >= minVersion)
370+
}
371+
return !installed
372+
}
373+
316374
private var _http: HttpConfigurable? = null
317375
private val http: HttpConfigurable
318376
get() = _http ?: HttpConfigurable.getInstance()
@@ -435,16 +493,10 @@ class Cargo(private val cargoExecutable: Path, private val rustcExecutable: Path
435493
message: String? = null,
436494
minVersion: SemVer? = null
437495
): Boolean {
438-
fun isNotInstalled(): Boolean {
439-
val cargo = project.toolchain?.rawCargo() ?: return false
440-
val installed = cargo.listInstalledBinaryCrates().any { (name, version) ->
441-
name == crateName && (minVersion == null || version != null && version >= minVersion)
442-
}
443-
return !installed
444-
}
445-
496+
val cargo = project.toolchain?.rawCargo() ?: return false
497+
val isNotInstalled = { cargo.checkBinaryCrateIsNotInstalled(crateName, minVersion) }
446498
val needInstall = if (isDispatchThread) {
447-
project.computeWithCancelableProgress("Checking if $crateName is installed...", ::isNotInstalled)
499+
project.computeWithCancelableProgress("Checking if $crateName is installed...", isNotInstalled)
448500
} else {
449501
isNotInstalled()
450502
}

src/main/kotlin/org/rust/ide/newProject/ConfigurationData.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ import org.rust.cargo.project.settings.ui.RustProjectSettingsPanel
99

1010
data class ConfigurationData(
1111
val settings: RustProjectSettingsPanel.Data,
12-
val createBinary: Boolean
12+
val template: RsProjectTemplate
1313
)

src/main/kotlin/org/rust/ide/newProject/RsDirectoryProjectGenerator.kt

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66
package org.rust.ide.newProject
77

88
import com.intellij.facet.ui.ValidationResult
9-
import com.intellij.ide.util.PsiNavigationSupport
109
import com.intellij.ide.util.projectWizard.AbstractNewProjectStep
1110
import com.intellij.ide.util.projectWizard.CustomStepProjectGenerator
1211
import com.intellij.openapi.module.Module
1312
import com.intellij.openapi.project.Project
1413
import com.intellij.openapi.vfs.VirtualFile
1514
import com.intellij.openapi.wm.impl.welcomeScreen.AbstractActionWithPanel
16-
import com.intellij.openapiext.isHeadlessEnvironment
1715
import com.intellij.platform.DirectoryProjectGenerator
1816
import com.intellij.platform.DirectoryProjectGeneratorBase
1917
import com.intellij.platform.ProjectGeneratorPeer
@@ -35,25 +33,19 @@ class RsDirectoryProjectGenerator : DirectoryProjectGeneratorBase<ConfigurationD
3533

3634
override fun validate(baseDirPath: String): ValidationResult {
3735
val crateName = File(baseDirPath).nameWithoutExtension
38-
val isBinary = peer?.settings?.createBinary == true
39-
val message = RsPackageNameValidator.validate(crateName, isBinary) ?: return ValidationResult.OK
36+
val message = peer?.settings?.template?.validateProjectName(crateName) ?: return ValidationResult.OK
4037
return ValidationResult(message)
4138
}
4239

4340
override fun generateProject(project: Project, baseDir: VirtualFile, data: ConfigurationData, module: Module) {
44-
val (settings, createBinary) = data
41+
val (settings, template) = data
42+
val cargo = settings.toolchain?.rawCargo() ?: return
43+
4544
val generatedFiles = project.computeWithCancelableProgress("Generating Cargo project...") {
46-
settings.toolchain?.rawCargo()?.init(project, module, baseDir, createBinary)
45+
cargo.makeProject(project, module, baseDir, template)
4746
} ?: return
4847

49-
// Open new files
50-
if (!isHeadlessEnvironment) {
51-
val navigation = PsiNavigationSupport.getInstance()
52-
navigation.createNavigatable(project, generatedFiles.manifest, -1).navigate(false)
53-
for (file in generatedFiles.sourceFiles) {
54-
navigation.createNavigatable(project, file, -1).navigate(true)
55-
}
56-
}
48+
project.openFiles(generatedFiles)
5749
}
5850

5951
override fun createStep(projectGenerator: DirectoryProjectGenerator<ConfigurationData>,

src/main/kotlin/org/rust/ide/newProject/RsProjectGeneratorPeer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import javax.swing.JComponent
1515

1616
class RsProjectGeneratorPeer : GeneratorPeerImpl<ConfigurationData>() {
1717

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

2121
override fun getSettings(): ConfigurationData = newProjectPanel.data
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Use of this source code is governed by the MIT license that can be
3+
* found in the LICENSE file.
4+
*/
5+
6+
package org.rust.ide.newProject
7+
8+
import com.intellij.icons.AllIcons
9+
import org.rust.ide.icons.RsIcons
10+
import javax.swing.Icon
11+
12+
sealed class RsProjectTemplate(val name: String, val isBinary: Boolean) {
13+
abstract val icon: Icon
14+
15+
fun validateProjectName(crateName: String): String? = RsPackageNameValidator.validate(crateName, isBinary)
16+
}
17+
18+
class RsGenericTemplate(name: String, isBinary: Boolean) : RsProjectTemplate(name, isBinary) {
19+
override val icon: Icon = RsIcons.RUST
20+
}
21+
22+
class RsCustomTemplate(name: String, val link: String, isBinary: Boolean = true) : RsProjectTemplate(name, isBinary) {
23+
override val icon: Icon = AllIcons.Vcs.Vendors.Github
24+
val shortLink: String
25+
get() = link.substringAfter("//")
26+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Use of this source code is governed by the MIT license that can be
3+
* found in the LICENSE file.
4+
*/
5+
6+
package org.rust.ide.newProject
7+
8+
import com.intellij.ide.util.PsiNavigationSupport
9+
import com.intellij.openapi.application.invokeLater
10+
import com.intellij.openapi.module.Module
11+
import com.intellij.openapi.project.Project
12+
import com.intellij.openapi.vfs.VirtualFile
13+
import com.intellij.openapiext.isHeadlessEnvironment
14+
import org.rust.cargo.toolchain.Cargo
15+
import org.rust.cargo.toolchain.Cargo.Companion.GeneratedFilesHolder
16+
17+
fun Project.openFiles(files: GeneratedFilesHolder) = invokeLater {
18+
if (!isHeadlessEnvironment) {
19+
val navigation = PsiNavigationSupport.getInstance()
20+
navigation.createNavigatable(this, files.manifest, -1).navigate(false)
21+
for (file in files.sourceFiles) {
22+
navigation.createNavigatable(this, file, -1).navigate(true)
23+
}
24+
}
25+
}
26+
27+
fun Cargo.makeProject(
28+
project: Project,
29+
module: Module,
30+
baseDir: VirtualFile,
31+
template: RsProjectTemplate
32+
): GeneratedFilesHolder? = when (template) {
33+
is RsGenericTemplate -> init(project, module, baseDir, template.isBinary)
34+
is RsCustomTemplate -> generate(project, module, baseDir, template.link)
35+
}

0 commit comments

Comments
 (0)