Repository: intellij-rust/intellij-rust
Issue: Format on Save doesn't work in files that have `async` blocks or functions

## Environment

* **IntelliJ Rust plugin version:** 0.2.110.2150-192
* **Rust toolchain version:** 1.39.0
* **IDE name and version:** IntelliJ IDEA 2019.2.4 (Community Edition)
* **Operating system:** macOS 10.14.6

## Problem description
If the Format on Save option is enabled, it doesn't work with files that contain an `async` block or function. My guess is that the plugin needs to pass `--edition 2018` to `rustfmt`?

## Steps to reproduce

* Enable Format on Save.
* Write some code without `async`. See that it formats on save.
* Add an `async` function. See that it does not format on save anymore.

Changed files:
src/main/kotlin/org/rust/cargo/project/workspace/CargoWorkspace.kt, src/main/kotlin/org/rust/lang/core/psi/ext/RsElement.kt, src/main/kotlin/org/rust/cargo/toolchain/impl/CargoMetadata.kt, src/test/kotlin/org/rust/cargo/commands/RustfmtTest.kt, src/main/kotlin/org/rust/cargo/toolchain/Rustfmt.kt, src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt

diff --git a/src/main/kotlin/org/rust/cargo/project/workspace/CargoWorkspace.kt b/src/main/kotlin/org/rust/cargo/project/workspace/CargoWorkspace.kt
index 8f09908f7..7da1ce22f 100644
--- a/src/main/kotlin/org/rust/cargo/project/workspace/CargoWorkspace.kt
+++ b/src/main/kotlin/org/rust/cargo/project/workspace/CargoWorkspace.kt
@@ -135,8 +135,8 @@ interface CargoWorkspace {
         LIB, DYLIB, STATICLIB, CDYLIB, RLIB, PROC_MACRO, UNKNOWN
     }
 
-    enum class Edition {
-        EDITION_2015, EDITION_2018
+    enum class Edition(val presentation: String) {
+        EDITION_2015("2015"), EDITION_2018("2018")
     }
 
     class Feature(
diff --git a/src/main/kotlin/org/rust/cargo/toolchain/Rustfmt.kt b/src/main/kotlin/org/rust/cargo/toolchain/Rustfmt.kt
index 233fa7bbe..288c1f5bb 100644
--- a/src/main/kotlin/org/rust/cargo/toolchain/Rustfmt.kt
+++ b/src/main/kotlin/org/rust/cargo/toolchain/Rustfmt.kt
@@ -7,13 +7,17 @@ package org.rust.cargo.toolchain
 
 import com.intellij.execution.ExecutionException
 import com.intellij.openapi.Disposable
+import com.intellij.openapi.application.runReadAction
 import com.intellij.openapi.editor.Document
 import com.intellij.openapi.vfs.VirtualFile
 import com.intellij.openapiext.isUnitTestMode
+import com.intellij.util.text.SemVer
 import org.rust.cargo.project.model.CargoProject
 import org.rust.cargo.project.settings.rustSettings
 import org.rust.cargo.project.settings.toolchain
+import org.rust.cargo.project.workspace.CargoWorkspace
 import org.rust.cargo.runconfig.command.workingDirectory
+import org.rust.lang.core.psi.ext.edition
 import org.rust.lang.core.psi.isNotRustFile
 import org.rust.openapiext.*
 import org.rust.stdext.buildList
@@ -26,30 +30,28 @@ class Rustfmt(private val rustfmtExecutable: Path) {
         val file = document.virtualFile ?: return null
         if (file.isNotRustFile || !file.isValid) return null
 
-        val configPath = findConfigPathRecursively(file.parent, stopAt = cargoProject.workingDirectory)
-        return reformatText(cargoProject, document.text, configPath)
-    }
-
-    @Throws(ExecutionException::class)
-    private fun reformatText(
-        cargoProject: CargoProject,
-        text: String,
-        configPath: Path? = null,
-        parentDisposable: Disposable = cargoProject.project,
-        skipChildren: Boolean = checkSupportForSkipChildrenFlag(cargoProject)
-    ): String? {
         val arguments = buildList<String> {
             add("--emit=stdout")
 
-            if (skipChildren) {
+            if (checkSupportForSkipChildrenFlag(cargoProject)) {
                 add("--unstable-features")
                 add("--skip-children")
             }
 
+            val configPath = findConfigPathRecursively(file.parent, stopAt = cargoProject.workingDirectory)
             if (configPath != null) {
                 add("--config-path")
                 add(configPath.toString())
             }
+
+            val currentRustcVersion = cargoProject.rustcInfo?.version?.semver
+            if (currentRustcVersion != null && currentRustcVersion >= RUST_1_31) {
+                val edition = runReadAction {
+                    val psiFile = file.toPsiFile(cargoProject.project)
+                    psiFile?.edition ?: CargoWorkspace.Edition.EDITION_2018
+                }
+                add("--edition=${edition.presentation}")
+            }
         }
 
         val processOutput = try {
@@ -57,7 +59,7 @@ class Rustfmt(private val rustfmtExecutable: Path) {
                 .withWorkDirectory(cargoProject.workingDirectory)
                 .withParameters(arguments)
                 .withCharset(Charsets.UTF_8)
-                .execute(parentDisposable, false, stdIn = text.toByteArray())
+                .execute(cargoProject.project, false, stdIn = document.text.toByteArray())
         } catch (e: ExecutionException) {
             if (isUnitTestMode) throw e else return null
         }
@@ -93,6 +95,7 @@ class Rustfmt(private val rustfmtExecutable: Path) {
     }
 
     companion object {
+        private val RUST_1_31: SemVer = SemVer.parseFromText("1.31.0")!!
         private val CONFIG_FILES: List<String> = listOf("rustfmt.toml", ".rustfmt.toml")
 
         private fun findConfigPathRecursively(directory: VirtualFile, stopAt: Path): Path? {
diff --git a/src/main/kotlin/org/rust/cargo/toolchain/impl/CargoMetadata.kt b/src/main/kotlin/org/rust/cargo/toolchain/impl/CargoMetadata.kt
index 14ffca40a..c106e6df4 100644
--- a/src/main/kotlin/org/rust/cargo/toolchain/impl/CargoMetadata.kt
+++ b/src/main/kotlin/org/rust/cargo/toolchain/impl/CargoMetadata.kt
@@ -366,8 +366,8 @@ object CargoMetadata {
     }
 
     private fun String?.cleanEdition(): Edition = when (this) {
-        "2015" -> Edition.EDITION_2015
-        "2018" -> Edition.EDITION_2018
+        Edition.EDITION_2015.presentation -> Edition.EDITION_2015
+        Edition.EDITION_2018.presentation -> Edition.EDITION_2018
         else -> Edition.EDITION_2015
     }
 }
diff --git a/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt b/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt
index 91d64983a..921eaa09d 100644
--- a/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt
+++ b/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt
@@ -346,7 +346,7 @@ class RsErrorAnnotator : AnnotatorBase(), HighlightRangeExtension {
 
         val crate = path.crate
         val useSpeck = path.ancestorStrict<RsUseSpeck>()
-        val edition = path.containingCargoTarget?.edition
+        val edition = path.edition
 
         // `pub(crate)` should be annotated
         if (crate != null && (qualifier != null || path.ancestorStrict<RsVisRestriction>() == null)) {
diff --git a/src/main/kotlin/org/rust/lang/core/psi/ext/RsElement.kt b/src/main/kotlin/org/rust/lang/core/psi/ext/RsElement.kt
index 8d884dd76..7d60ef7a8 100644
--- a/src/main/kotlin/org/rust/lang/core/psi/ext/RsElement.kt
+++ b/src/main/kotlin/org/rust/lang/core/psi/ext/RsElement.kt
@@ -56,8 +56,11 @@ val RsElement.containingCargoTarget: CargoWorkspace.Target?
 
 val RsElement.containingCargoPackage: CargoWorkspace.Package? get() = containingCargoTarget?.pkg
 
+val PsiElement.edition: CargoWorkspace.Edition?
+    get() = contextOrSelf<RsElement>()?.containingCargoTarget?.edition
+
 val PsiElement.isEdition2018: Boolean
-    get() = contextOrSelf<RsElement>()?.containingCargoTarget?.edition == CargoWorkspace.Edition.EDITION_2018
+    get() = edition == CargoWorkspace.Edition.EDITION_2018
 
 /**
  * It is possible to match value with constant-like element, e.g.
diff --git a/src/test/kotlin/org/rust/cargo/commands/RustfmtTest.kt b/src/test/kotlin/org/rust/cargo/commands/RustfmtTest.kt
index 6a14ba248..8a8d3a603 100644
--- a/src/test/kotlin/org/rust/cargo/commands/RustfmtTest.kt
+++ b/src/test/kotlin/org/rust/cargo/commands/RustfmtTest.kt
@@ -9,18 +9,134 @@ import com.intellij.openapi.actionSystem.CommonDataKeys
 import com.intellij.openapi.editor.Editor
 import com.intellij.testFramework.MapDataContext
 import com.intellij.testFramework.TestActionEvent
+import org.intellij.lang.annotations.Language
+import org.rust.FileTreeBuilder
+import org.rust.MinRustcVersion
 import org.rust.cargo.RsWithToolchainTestBase
 import org.rust.cargo.project.settings.rustSettings
 import org.rust.fileTree
 import org.rust.ide.actions.RustfmtCargoProjectAction
 import org.rust.ide.actions.RustfmtFileAction
-import org.rust.openapiext.document
 import org.rust.openapiext.saveAllDocuments
 
 class RustfmtTest : RsWithToolchainTestBase() {
 
-    fun `test rustfmt file action`() {
-        val fileWithCaret = fileTree {
+    fun `test rustfmt file action`() = doTest({
+        toml("Cargo.toml", """
+            [package]
+            name = "hello"
+            version = "0.1.0"
+            authors = []
+        """)
+
+        dir("src") {
+            rust("main.rs", """
+                fn main() {/*caret*/
+                println!("Hello, ΣΠ∫!");
+                }
+            """)
+        }
+    }, """
+        fn main() {
+            println!("Hello, ΣΠ∫!");
+        }
+    """)
+
+    @MinRustcVersion("1.31.0")
+    fun `test rustfmt file action edition 2018`() = doTest({
+        toml("Cargo.toml", """
+            [package]
+            name = "hello"
+            version = "0.1.0"
+            authors = []
+            edition = "2018"
+        """)
+
+        dir("src") {
+            rust("main.rs", """
+                async fn foo() {/*caret*/
+                println!("Hello, ΣΠ∫!");
+                }
+            """)
+        }
+    }, """
+        async fn foo() {
+            println!("Hello, ΣΠ∫!");
+        }
+    """)
+
+    fun `test rustfmt cargo project action`() = doTest({
+        toml("Cargo.toml", """
+            [package]
+            name = "hello"
+            version = "0.1.0"
+            authors = []
+        """)
+
+        dir("src") {
+            rust("main.rs", """
+                fn main() {/*caret*/
+                    println!("Hello, ΣΠ∫!");
+                }
+            """)
+        }
+    }, """
+        fn main() {
+            println!("Hello, ΣΠ∫!");
+        }
+    """) { reformatCargoProject() }
+
+    fun `test rustfmt on save`() = doTest({
+        toml("Cargo.toml", """
+            [package]
+            name = "hello"
+            version = "0.1.0"
+            authors = []
+        """)
+
+        dir("src") {
+            rust("main.rs", """
+                fn main() {/*caret*/
+                    println!("Hello, ΣΠ∫!");
+                }
+            """)
+        }
+    }) {
+        myFixture.type("\\n\\n\\n")
+        project.rustSettings.modify { it.runRustfmtOnSave = true }
+        saveAllDocuments()
+    }
+
+    fun `test use config from project root`() = doTest({
+        toml("Cargo.toml", """
+            [package]
+            name = "hello"
+            version = "0.1.0"
+            authors = []
+        """)
+
+        toml("rustfmt.toml", """
+            remove_nested_parens = false # default: true
+        """)
+
+        dir("src") {
+            rust("main.rs", """
+                fn main() {/*caret*/
+                    ((((foo()))));
+                }
+            """)
+        }
+    })
+
+    fun `test use config from workspace root (rustfmt dot toml)`() = doTest({
+        toml("Cargo.toml", """
+            [workspace]
+            members = [
+                "hello"
+            ]
+        """)
+
+        dir("hello") {
             toml("Cargo.toml", """
                 [package]
                 name = "hello"
@@ -28,26 +144,29 @@ class RustfmtTest : RsWithToolchainTestBase() {
                 authors = []
             """)
 
+            toml("rustfmt.toml", """
+                remove_nested_parens = false
+            """)
+
             dir("src") {
                 rust("main.rs", """
                     fn main() {/*caret*/
-                    println!("Hello, ΣΠ∫!");
+                        ((((foo()))));
                     }
                 """)
             }
-        }.create().fileWithCaret
+        }
+    })
 
-        myFixture.configureFromTempProjectFile(fileWithCaret)
-        reformatDocument(myFixture.editor)
-        assertEquals("""
-            fn main() {
-                println!("Hello, ΣΠ∫!");
-            }
-        """.trimIndent(), myFixture.editor.document.text.trim())
-    }
+    fun `test use config from workspace root (dot rustfmt dot toml)`() = doTest({
+        toml("Cargo.toml", """
+            [workspace]
+            members = [
+                "hello"
+            ]
+        """)
 
-    fun `test rustfmt cargo project action`() {
-        val fileWithCaret = fileTree {
+        dir("hello") {
             toml("Cargo.toml", """
                 [package]
                 name = "hello"
@@ -55,28 +174,33 @@ class RustfmtTest : RsWithToolchainTestBase() {
                 authors = []
             """)
 
+            toml(".rustfmt.toml", """
+                remove_nested_parens = false
+            """)
+
             dir("src") {
                 rust("main.rs", """
                     fn main() {/*caret*/
-                        println!("Hello, ΣΠ∫!");
+                        ((((foo()))));
                     }
                 """)
             }
-        }.create().fileWithCaret
+        }
+    })
 
-        myFixture.configureFromTempProjectFile(fileWithCaret)
-        reformatCargoProject()
-        assertEquals("""
-            fn main() {
-                println!("Hello, ΣΠ∫!");
-            }
-        """.trimIndent(), myFixture.editor.document.text.trim())
-    }
+    fun `test use config from workspace root overrides config from project root`() = doTest({
+        toml("Cargo.toml", """
+            [workspace]
+            members = [
+                "hello"
+            ]
+        """)
 
-    fun `test rustfmt on save`() {
-        project.rustSettings.modify { it.runRustfmtOnSave = true }
+        toml("rustfmt.toml", """
+            control_brace_style = true
+        """)
 
-        val fileWithCaret = fileTree {
+        dir("hello") {
             toml("Cargo.toml", """
                 [package]
                 name = "hello"
@@ -84,26 +208,33 @@ class RustfmtTest : RsWithToolchainTestBase() {
                 authors = []
             """)
 
+            toml("rustfmt.toml", """
+                remove_nested_parens = false
+            """)
+
             dir("src") {
                 rust("main.rs", """
                     fn main() {/*caret*/
-                        println!("Hello, ΣΠ∫!");
+                        ((((foo()))));
                     }
                 """)
             }
-        }.create().fileWithCaret
+        }
+    })
 
-        val file = myFixture.configureFromTempProjectFile(fileWithCaret).virtualFile
-        val document = file.document!!
-        val prevText = document.text
-        myFixture.type("\\n\\n\\n")
+    fun `test use config from project root if config from workspace root is not presented`() = doTest({
+        toml("Cargo.toml", """
+            [workspace]
+            members = [
+                "hello"
+            ]
+        """)
 
-        saveAllDocuments()
-        assertEquals(prevText.trim(), document.text.trim())
-    }
+        toml("rustfmt.toml", """
+            remove_nested_parens = false
+        """)
 
-    fun `test use config from project root`() {
-        val fileWithCaret = fileTree {
+        dir("hello") {
             toml("Cargo.toml", """
                 [package]
                 name = "hello"
@@ -111,10 +242,6 @@ class RustfmtTest : RsWithToolchainTestBase() {
                 authors = []
             """)
 
-            toml("rustfmt.toml", """
-                remove_nested_parens = false # default: true
-            """)
-
             dir("src") {
                 rust("main.rs", """
                     fn main() {/*caret*/
@@ -122,165 +249,8 @@ class RustfmtTest : RsWithToolchainTestBase() {
                     }
                 """)
             }
-        }.create().fileWithCaret
-
-        myFixture.configureFromTempProjectFile(fileWithCaret)
-        val prevText = editor.document.text
-        reformatDocument(myFixture.editor)
-        assertEquals(prevText.trim(), editor.document.text.trim())
-    }
-
-    fun `test use config from workspace root (rustfmt dot toml)`() {
-        val fileWithCaret = fileTree {
-            toml("Cargo.toml", """
-                [workspace]
-                members = [
-                    "hello"
-                ]
-            """)
-
-            dir("hello") {
-                toml("Cargo.toml", """
-                    [package]
-                    name = "hello"
-                    version = "0.1.0"
-                    authors = []
-                """)
-
-                toml("rustfmt.toml", """
-                    remove_nested_parens = false
-                """)
-
-                dir("src") {
-                    rust("main.rs", """
-                        fn main() {/*caret*/
-                            ((((foo()))));
-                        }
-                    """)
-                }
-            }
-        }.create().fileWithCaret
-
-        myFixture.configureFromTempProjectFile(fileWithCaret)
-        val prevText = editor.document.text
-        reformatDocument(myFixture.editor)
-        assertEquals(prevText.trim(), editor.document.text.trim())
-    }
-
-    fun `test use config from workspace root (dot rustfmt dot toml)`() {
-        val fileWithCaret = fileTree {
-            toml("Cargo.toml", """
-                [workspace]
-                members = [
-                    "hello"
-                ]
-            """)
-
-            dir("hello") {
-                toml("Cargo.toml", """
-                    [package]
-                    name = "hello"
-                    version = "0.1.0"
-                    authors = []
-                """)
-
-                toml(".rustfmt.toml", """
-                    remove_nested_parens = false
-                """)
-
-                dir("src") {
-                    rust("main.rs", """
-                        fn main() {/*caret*/
-                            ((((foo()))));
-                        }
-                    """)
-                }
-            }
-        }.create().fileWithCaret
-
-        myFixture.configureFromTempProjectFile(fileWithCaret)
-        val prevText = editor.document.text
-        reformatDocument(myFixture.editor)
-        assertEquals(prevText.trim(), editor.document.text.trim())
-    }
-
-    fun `test use config from workspace root overrides config from project root`() {
-        val fileWithCaret = fileTree {
-            toml("Cargo.toml", """
-                [workspace]
-                members = [
-                    "hello"
-                ]
-            """)
-
-            toml("rustfmt.toml", """
-                control_brace_style = true
-            """)
-
-            dir("hello") {
-                toml("Cargo.toml", """
-                    [package]
-                    name = "hello"
-                    version = "0.1.0"
-                    authors = []
-                """)
-
-                toml("rustfmt.toml", """
-                    remove_nested_parens = false
-                """)
-
-                dir("src") {
-                    rust("main.rs", """
-                        fn main() {/*caret*/
-                            ((((foo()))));
-                        }
-                    """)
-                }
-            }
-        }.create().fileWithCaret
-
-        myFixture.configureFromTempProjectFile(fileWithCaret)
-        val prevText = editor.document.text
-        reformatDocument(myFixture.editor)
-        assertEquals(prevText.trim(), editor.document.text.trim())
-    }
-
-    fun `test use config from project root if config from workspace root is not presented`() {
-        val fileWithCaret = fileTree {
-            toml("Cargo.toml", """
-                [workspace]
-                members = [
-                    "hello"
-                ]
-            """)
-
-            toml("rustfmt.toml", """
-                remove_nested_parens = false
-            """)
-
-            dir("hello") {
-                toml("Cargo.toml", """
-                    [package]
-                    name = "hello"
-                    version = "0.1.0"
-                    authors = []
-                """)
-
-                dir("src") {
-                    rust("main.rs", """
-                        fn main() {/*caret*/
-                            ((((foo()))));
-                        }
-                    """)
-                }
-            }
-        }.create().fileWithCaret
-
-        myFixture.configureFromTempProjectFile(fileWithCaret)
-        val prevText = editor.document.text
-        reformatDocument(myFixture.editor)
-        assertEquals(prevText.trim(), editor.document.text.trim())
-    }
+        }
+    })
 
     private fun reformatDocument(editor: Editor) {
         val dataContext = MapDataContext(mapOf(
@@ -310,4 +280,25 @@ class RustfmtTest : RsWithToolchainTestBase() {
 
         action.actionPerformed(e)
     }
+
+    /**
+     * Note: [expectedTextSupplier] is called right before [action].
+     */
+    private fun doTest(
+        treeBuilder: FileTreeBuilder.() -> Unit,
+        expectedTextSupplier: () -> String = { editor.document.text },
+        action: () -> Unit = { reformatDocument(myFixture.editor) }
+    ) {
+        val fileWithCaret = fileTree(treeBuilder).create().fileWithCaret
+        myFixture.configureFromTempProjectFile(fileWithCaret)
+        val expected = expectedTextSupplier()
+        action()
+        assertEquals(expected.trim(), editor.document.text.trim())
+    }
+
+    private fun doTest(
+        treeBuilder: FileTreeBuilder.() -> Unit,
+        @Language("Rust") expectedText: String,
+        action: () -> Unit = { reformatDocument(myFixture.editor) }
+    ) = doTest(treeBuilder, { expectedText.trimIndent() }, action)
 }