Repository: intellij-rust/intellij-rust
Issue: Use `--offline` instead of `-Zoffline` on cargo +1.36

## Environment

* **IntelliJ Rust plugin version:** 0.2.104.2715-192-nightly
* **Rust toolchain version:** 1.39.0-nightly (bea0372a1 2019-08-20) x86_64-apple-darwin
* **IDE name and version:** CL-192.5728.100
* **Operating system:** macOS 10.14.6

## Problem description

The `--offline` flag which allows using cargo without a network connection [has been stabilized](https://github.com/rust-lang/cargo/pull/6934) in 1.36.0. However, IntelliJ Rust still passes `-Zoffline` to cargo when "Offline mode" setting is enabled.

## Steps to reproduce

1. Enable "Offline mode" setting in `Preferences > Languages & Frameworks > Rust > Cargo`

2. Attach a Cargo project via Cargo Tool Window.

3. Check Event Log:
```
Cargo project update failed:
Execution failed (exit code 101).
/Users/mikhail.chernyavsky/.cargo/bin/cargo metadata --verbose --format-version 1 --all-features -Zoffline
stdout : error: unknown `-Z` flag specified: offline
stderr : 
```

<!--
Please include as much of your codebase as needed to reproduce the error.
If the relevant files are large, please provide a link to a public repository or a [Gist](https://gist.github.com/).
-->            

Changed files:
src/main/kotlin/org/rust/cargo/runconfig/buildtool/CargoBuildTaskRunner.kt, src/main/kotlin/org/rust/cargo/runconfig/CargoRunStateBase.kt, src/main/kotlin/org/rust/cargo/project/configurable/CargoConfigurable.kt, src/main/kotlin/org/rust/cargo/toolchain/Cargo.kt, src/main/kotlin/org/rust/cargo/runconfig/legacy/RsAsyncRunner.kt, src/main/kotlin/org/rust/cargo/toolchain/Rustfmt.kt, idea/src/main/kotlin/org/rust/ide/idea/RsModuleBuilder.kt, src/main/kotlin/org/rust/ide/actions/RsCreateCrateAction.kt, src/main/kotlin/org/rust/ide/newProject/RsDirectoryProjectGenerator.kt, src/main/kotlin/org/rust/cargo/runconfig/Utils.kt, src/test/kotlin/org/rust/cargo/toolchain/CargoTest.kt

diff --git a/idea/src/main/kotlin/org/rust/ide/idea/RsModuleBuilder.kt b/idea/src/main/kotlin/org/rust/ide/idea/RsModuleBuilder.kt
index 48d004d10..013f48997 100644
--- a/idea/src/main/kotlin/org/rust/ide/idea/RsModuleBuilder.kt
+++ b/idea/src/main/kotlin/org/rust/ide/idea/RsModuleBuilder.kt
@@ -43,8 +43,12 @@ class RsModuleBuilder : ModuleBuilder() {
         // Just work if user "creates new project" over an existing one.
         if (toolchain != null && root.findChild(RustToolchain.CARGO_TOML) == null) {
             try {
-                toolchain.rawCargo().init(modifiableRootModel.module, root,
-                    configurationData?.createBinary ?: true)
+                toolchain.rawCargo().init(
+                    modifiableRootModel.project,
+                    modifiableRootModel.module,
+                    root,
+                    configurationData?.createBinary ?: true
+                )
             } catch (e: ExecutionException) {
                 LOG.error(e)
                 throw ConfigurationException(e.message)
diff --git a/src/main/kotlin/org/rust/cargo/project/configurable/CargoConfigurable.kt b/src/main/kotlin/org/rust/cargo/project/configurable/CargoConfigurable.kt
index e4c347fea..6a4616239 100644
--- a/src/main/kotlin/org/rust/cargo/project/configurable/CargoConfigurable.kt
+++ b/src/main/kotlin/org/rust/cargo/project/configurable/CargoConfigurable.kt
@@ -46,9 +46,8 @@ class CargoConfigurable(project: Project) : RsConfigurableBase(project) {
         row("Compile all project targets if possible:", compileAllTargetsCheckBox, """
             Pass `--target-all` option to cargo build/check command.
         """)
-        row("Offline mode (nightly only):", useOfflineCheckbox, """
-            Pass `-Z offline` option to cargo not to perform network requests.
-            Used only for nightly toolchain.
+        row("Offline mode:", useOfflineCheckbox, """
+            Pass `--offline` option to cargo not to perform network requests.
         """)
         block("External Linter") {
             row("External linter:", externalLinterComboBox, """
diff --git a/src/main/kotlin/org/rust/cargo/runconfig/CargoRunStateBase.kt b/src/main/kotlin/org/rust/cargo/runconfig/CargoRunStateBase.kt
index 7ba72fbdb..7b69c4ea2 100644
--- a/src/main/kotlin/org/rust/cargo/runconfig/CargoRunStateBase.kt
+++ b/src/main/kotlin/org/rust/cargo/runconfig/CargoRunStateBase.kt
@@ -54,7 +54,7 @@ abstract class CargoRunStateBase(
     override fun startProcess(): ProcessHandler = startProcess(emulateTerminal = false)
 
     fun startProcess(emulateTerminal: Boolean): ProcessHandler {
-        var commandLine = cargo().toColoredCommandLine(prepareCommandLine())
+        var commandLine = cargo().toColoredCommandLine(environment.project, prepareCommandLine())
         if (emulateTerminal) {
             commandLine = PtyCommandLine(commandLine)
                 .withInitialColumns(PtyCommandLine.MAX_COLUMNS)
diff --git a/src/main/kotlin/org/rust/cargo/runconfig/Utils.kt b/src/main/kotlin/org/rust/cargo/runconfig/Utils.kt
index 441b30a12..062bf5030 100644
--- a/src/main/kotlin/org/rust/cargo/runconfig/Utils.kt
+++ b/src/main/kotlin/org/rust/cargo/runconfig/Utils.kt
@@ -58,7 +58,6 @@ fun Project.buildProject() {
                 ?: false
             if (allTargets) add("--all-targets")
         }
-        if (settings.useOffline) add("-Zoffline")
     }
 
     // Initialize run content manager
diff --git a/src/main/kotlin/org/rust/cargo/runconfig/buildtool/CargoBuildTaskRunner.kt b/src/main/kotlin/org/rust/cargo/runconfig/buildtool/CargoBuildTaskRunner.kt
index ce185cab1..1f7fb2dcb 100644
--- a/src/main/kotlin/org/rust/cargo/runconfig/buildtool/CargoBuildTaskRunner.kt
+++ b/src/main/kotlin/org/rust/cargo/runconfig/buildtool/CargoBuildTaskRunner.kt
@@ -92,7 +92,6 @@ class CargoBuildTaskRunner : ProjectTaskRunner() {
                     ?: false
                 if (allTargets) add("--all-targets")
             }
-            if (settings.useOffline) add("-Zoffline")
         }
 
         return cargoProjects.mapNotNull { cargoProject ->
diff --git a/src/main/kotlin/org/rust/cargo/runconfig/legacy/RsAsyncRunner.kt b/src/main/kotlin/org/rust/cargo/runconfig/legacy/RsAsyncRunner.kt
index 0cc0b0b86..044b3d75e 100644
--- a/src/main/kotlin/org/rust/cargo/runconfig/legacy/RsAsyncRunner.kt
+++ b/src/main/kotlin/org/rust/cargo/runconfig/legacy/RsAsyncRunner.kt
@@ -139,7 +139,7 @@ abstract class RsAsyncRunner(
         val cargo = state.cargo()
 
         val processForUserOutput = ProcessOutput()
-        val processForUser = RsKillableColoredProcessHandler(cargo.toColoredCommandLine(command))
+        val processForUser = RsKillableColoredProcessHandler(cargo.toColoredCommandLine(project, command))
 
         processForUser.addProcessListener(CapturingProcessAdapter(processForUserOutput))
 
@@ -168,7 +168,7 @@ abstract class RsAsyncRunner(
                             }
 
                             val processForJson = CapturingProcessHandler(
-                                cargo.toGeneralCommandLine(command.prependArgument("--message-format=json"))
+                                cargo.toGeneralCommandLine(project, command.prependArgument("--message-format=json"))
                             )
                             val output = processForJson.runProcessWithProgressIndicator(indicator)
                             if (output.isCancelled || output.exitCode != 0) {
diff --git a/src/main/kotlin/org/rust/cargo/toolchain/Cargo.kt b/src/main/kotlin/org/rust/cargo/toolchain/Cargo.kt
index 20ac96c13..ebf260cc1 100644
--- a/src/main/kotlin/org/rust/cargo/toolchain/Cargo.kt
+++ b/src/main/kotlin/org/rust/cargo/toolchain/Cargo.kt
@@ -25,6 +25,7 @@ import org.rust.cargo.project.model.cargoProjects
 import org.rust.cargo.project.settings.rustSettings
 import org.rust.cargo.project.settings.toolchain
 import org.rust.cargo.runconfig.buildtool.CargoPatch
+import org.rust.cargo.runconfig.command.CargoCommandConfiguration.Companion.findCargoProject
 import org.rust.cargo.toolchain.Rustup.Companion.checkNeedInstallClippy
 import org.rust.ide.actions.InstallBinaryCrateAction
 import org.rust.ide.notifications.showBalloon
@@ -95,10 +96,6 @@ class Cargo(private val cargoExecutable: Path) {
         listener: ProcessListener? = null
     ): String {
         val additionalArgs = mutableListOf("--verbose", "--format-version", "1", "--all-features")
-        if (owner.rustSettings.useOffline) {
-            additionalArgs += "-Zoffline"
-        }
-
         return CargoCommandLine("metadata", projectDirectory, additionalArgs)
             .execute(owner, listener = listener)
             .stdout
@@ -107,6 +104,7 @@ class Cargo(private val cargoExecutable: Path) {
 
     @Throws(ExecutionException::class)
     fun init(
+        project: Project,
         owner: Disposable,
         directory: VirtualFile,
         createBinary: Boolean,
@@ -124,7 +122,7 @@ class Cargo(private val cargoExecutable: Path) {
 
         args.add(path.toString())
 
-        CargoCommandLine("init", path, args).execute(owner)
+        CargoCommandLine("init", path, args).execute(project, owner)
         fullyRefreshDirectory(directory)
 
         val manifest = checkNotNull(directory.findChild(RustToolchain.CARGO_TOML)) { "Can't find the manifest file" }
@@ -152,28 +150,37 @@ class Cargo(private val cargoExecutable: Path) {
             }
 
             if (settings.compileAllTargets && checkSupportForBuildCheckAllTargets()) add("--all-targets")
-            if (settings.useOffline) add("-Zoffline")
             addAll(ParametersListUtil.parse(settings.externalLinterArguments))
         }
 
         val useClippy = settings.externalLinter == ExternalLinter.CLIPPY
             && !checkNeedInstallClippy(project, cargoProjectDirectory)
         val checkCommand = if (useClippy) "clippy" else "check"
-        return CargoCommandLine(checkCommand, cargoProjectDirectory, arguments).execute(owner, ignoreExitCode = true)
+        return CargoCommandLine(checkCommand, cargoProjectDirectory, arguments)
+            .execute(project, owner, ignoreExitCode = true)
     }
 
-    fun toColoredCommandLine(commandLine: CargoCommandLine): GeneralCommandLine =
-        toGeneralCommandLine(commandLine, colors = true)
+    fun toColoredCommandLine(project: Project, commandLine: CargoCommandLine): GeneralCommandLine =
+        toGeneralCommandLine(project, commandLine, colors = true)
 
-    fun toGeneralCommandLine(commandLine: CargoCommandLine): GeneralCommandLine =
-        toGeneralCommandLine(commandLine, colors = false)
+    fun toGeneralCommandLine(project: Project, commandLine: CargoCommandLine): GeneralCommandLine =
+        toGeneralCommandLine(project, commandLine, colors = false)
 
-    private fun toGeneralCommandLine(commandLine: CargoCommandLine, colors: Boolean): GeneralCommandLine =
+    private fun toGeneralCommandLine(project: Project, commandLine: CargoCommandLine, colors: Boolean): GeneralCommandLine =
         with(patchArgs(commandLine, colors)) {
             val parameters = buildList<String> {
                 if (channel != RustChannel.DEFAULT) {
                     add("+$channel")
                 }
+                if (project.rustSettings.useOffline) {
+                    val cargoProject = findCargoProject(project, additionalArguments, workingDirectory)
+                    val rustcVersion = cargoProject?.rustcInfo?.version?.semver
+                    when {
+                        rustcVersion == null -> Unit
+                        rustcVersion < RUST_1_36 -> add("-Zoffline")
+                        else -> add("--offline")
+                    }
+                }
                 add(command)
                 addAll(additionalArguments)
             }
@@ -190,11 +197,12 @@ class Cargo(private val cargoExecutable: Path) {
 
     @Throws(ExecutionException::class)
     private fun CargoCommandLine.execute(
-        owner: Disposable,
+        project: Project,
+        owner: Disposable = project,
         ignoreExitCode: Boolean = false,
         stdIn: ByteArray? = null,
         listener: ProcessListener? = null
-    ): ProcessOutput = toGeneralCommandLine(this).execute(owner, ignoreExitCode, stdIn, listener)
+    ): ProcessOutput = toGeneralCommandLine(project, this).execute(owner, ignoreExitCode, stdIn, listener)
 
     private var _http: HttpConfigurable? = null
     private val http: HttpConfigurable
@@ -322,3 +330,5 @@ class Cargo(private val cargoExecutable: Path) {
         }
     }
 }
+
+private val RUST_1_36: SemVer = SemVer.parseFromText("1.36.0")!!
diff --git a/src/main/kotlin/org/rust/cargo/toolchain/Rustfmt.kt b/src/main/kotlin/org/rust/cargo/toolchain/Rustfmt.kt
index 90866d37e..b8a03afce 100644
--- a/src/main/kotlin/org/rust/cargo/toolchain/Rustfmt.kt
+++ b/src/main/kotlin/org/rust/cargo/toolchain/Rustfmt.kt
@@ -69,10 +69,11 @@ class Rustfmt(private val rustfmtExecutable: Path) {
         cargoProject: CargoProject,
         owner: Disposable = cargoProject.project
     ) {
-        cargoProject.project.computeWithCancelableProgress("Reformatting Cargo Project with Rustfmt...") {
-            cargoProject.project.toolchain
+        val project = cargoProject.project
+        project.computeWithCancelableProgress("Reformatting Cargo Project with Rustfmt...") {
+            project.toolchain
                 ?.cargoOrWrapper(cargoProject.workingDirectory)
-                ?.toGeneralCommandLine(CargoCommandLine.forProject(cargoProject, "fmt", listOf("--all")))
+                ?.toGeneralCommandLine(project, CargoCommandLine.forProject(cargoProject, "fmt", listOf("--all")))
                 ?.execute(owner, false)
         }
     }
diff --git a/src/main/kotlin/org/rust/ide/actions/RsCreateCrateAction.kt b/src/main/kotlin/org/rust/ide/actions/RsCreateCrateAction.kt
index 3fb71a0da..2a64827f4 100644
--- a/src/main/kotlin/org/rust/ide/actions/RsCreateCrateAction.kt
+++ b/src/main/kotlin/org/rust/ide/actions/RsCreateCrateAction.kt
@@ -53,7 +53,7 @@ class RsCreateCrateAction : RunCargoCommandActionBase(CargoIcons.ICON) {
         val targetDir = runWriteAction {
             root.createChildDirectory(this, name)
         }
-        cargo.init(project, targetDir, binary, "none")
+        cargo.init(project, project, targetDir, binary, "none")
 
         val manifest = targetDir.findChild(RustToolchain.CARGO_TOML)
         manifest?.let {
diff --git a/src/main/kotlin/org/rust/ide/newProject/RsDirectoryProjectGenerator.kt b/src/main/kotlin/org/rust/ide/newProject/RsDirectoryProjectGenerator.kt
index a34deade2..b515952f8 100644
--- a/src/main/kotlin/org/rust/ide/newProject/RsDirectoryProjectGenerator.kt
+++ b/src/main/kotlin/org/rust/ide/newProject/RsDirectoryProjectGenerator.kt
@@ -41,7 +41,7 @@ class RsDirectoryProjectGenerator : DirectoryProjectGeneratorBase<ConfigurationD
 
     override fun generateProject(project: Project, baseDir: VirtualFile, data: ConfigurationData, module: Module) {
         val (settings, createBinary) = data
-        val generatedFiles = settings.toolchain?.rawCargo()?.init(module, baseDir, createBinary) ?: return
+        val generatedFiles = settings.toolchain?.rawCargo()?.init(project, module, baseDir, createBinary) ?: return
 
         // Open new files
         if (!isHeadlessEnvironment) {
diff --git a/src/test/kotlin/org/rust/cargo/toolchain/CargoTest.kt b/src/test/kotlin/org/rust/cargo/toolchain/CargoTest.kt
index c854de1e0..e5ea4f5d2 100644
--- a/src/test/kotlin/org/rust/cargo/toolchain/CargoTest.kt
+++ b/src/test/kotlin/org/rust/cargo/toolchain/CargoTest.kt
@@ -8,13 +8,15 @@ package org.rust.cargo.toolchain
 import com.intellij.execution.configurations.GeneralCommandLine
 import com.intellij.openapi.util.SystemInfo
 import com.intellij.util.net.HttpConfigurable
+import org.rust.MockRustcVersion
 import org.rust.RsTestBase
+import org.rust.cargo.project.settings.rustSettings
 import java.nio.file.Paths
 
 class CargoTest : RsTestBase() {
 
     fun `test run arguments preserved`() = checkCommandLine(
-        cargo.toColoredCommandLine(CargoCommandLine("run", wd, listOf("--bin", "parity", "--", "--prune", "archive"))), """
+        cargo.toColoredCommandLine(project, CargoCommandLine("run", wd, listOf("--bin", "parity", "--", "--prune", "archive"))), """
         cmd: /usr/bin/cargo run --color=always --bin parity -- --prune archive
         env: RUST_BACKTRACE=short, TERM=ansi
         """, """
@@ -23,7 +25,7 @@ class CargoTest : RsTestBase() {
     """)
 
     fun `test basic command`() = checkCommandLine(
-        cargo.toGeneralCommandLine(CargoCommandLine("test", wd, listOf("--all"))), """
+        cargo.toGeneralCommandLine(project, CargoCommandLine("test", wd, listOf("--all"))), """
         cmd: /usr/bin/cargo test --all
         env: RUST_BACKTRACE=short, TERM=ansi
         """, """
@@ -42,7 +44,7 @@ class CargoTest : RsTestBase() {
         }
         val cargo = cargo.apply { setHttp(http) }
         checkCommandLine(
-            cargo.toGeneralCommandLine(CargoCommandLine("check", wd)), """
+            cargo.toGeneralCommandLine(project, CargoCommandLine("check", wd)), """
             cmd: /usr/bin/cargo check
             env: RUST_BACKTRACE=short, TERM=ansi, http_proxy=http://user:pwd@host:3268/
             """, """
@@ -52,7 +54,7 @@ class CargoTest : RsTestBase() {
     }
 
     fun `test adds colors for common commands`() = checkCommandLine(
-        cargo.toColoredCommandLine(CargoCommandLine("run", wd, listOf("--release", "--", "foo"))), """
+        cargo.toColoredCommandLine(project, CargoCommandLine("run", wd, listOf("--release", "--", "foo"))), """
         cmd: /usr/bin/cargo run --color=always --release -- foo
         env: RUST_BACKTRACE=short, TERM=ansi
         """, """
@@ -61,7 +63,7 @@ class CargoTest : RsTestBase() {
     """)
 
     fun `test don't add color for unknown command`() = checkCommandLine(
-        cargo.toColoredCommandLine(CargoCommandLine("tree", wd)), """
+        cargo.toColoredCommandLine(project, CargoCommandLine("tree", wd)), """
         cmd: /usr/bin/cargo tree
         env: RUST_BACKTRACE=short, TERM=ansi
         """, """
@@ -70,7 +72,7 @@ class CargoTest : RsTestBase() {
     """)
 
     fun `test adds nightly channel`() = checkCommandLine(
-        cargo.toColoredCommandLine(CargoCommandLine("run", wd, listOf("--release", "--", "foo"), channel = RustChannel.NIGHTLY)), """
+        cargo.toColoredCommandLine(project, CargoCommandLine("run", wd, listOf("--release", "--", "foo"), channel = RustChannel.NIGHTLY)), """
         cmd: /usr/bin/cargo +nightly run --color=always --release -- foo
         env: RUST_BACKTRACE=short, TERM=ansi
         """, """
@@ -78,7 +80,43 @@ class CargoTest : RsTestBase() {
         env: RUST_BACKTRACE=short, TERM=ansi
     """)
 
-    private fun checkCommandLine(cmd: GeneralCommandLine, expected: String, expectedWin: String) {
+    @MockRustcVersion("1.36.0")
+    fun `test adds --offline option`() = withOfflineMode {
+        checkCommandLine(cargo.toColoredCommandLine(project, CargoCommandLine("run", wd, listOf("--release", "--", "foo"))), """
+            cmd: /usr/bin/cargo --offline run --color=always --release -- foo
+            env: RUST_BACKTRACE=short, TERM=ansi
+            """, """
+            cmd: C:/usr/bin/cargo.exe --offline run --color=always --release -- foo
+            env: RUST_BACKTRACE=short, TERM=ansi
+        """)
+    }
+
+    @MockRustcVersion("1.35.0-nightly")
+    fun `test adds -Zoffline`() = withOfflineMode {
+        checkCommandLine(cargo.toColoredCommandLine(project, CargoCommandLine("run", wd, listOf("--release", "--", "foo"))), """
+            cmd: /usr/bin/cargo -Zoffline run --color=always --release -- foo
+            env: RUST_BACKTRACE=short, TERM=ansi
+            """, """
+            cmd: C:/usr/bin/cargo.exe -Zoffline run --color=always --release -- foo
+            env: RUST_BACKTRACE=short, TERM=ansi
+        """)
+    }
+
+    private fun withOfflineMode(action: () -> Unit) {
+        val oldStatus = project.rustSettings.useOffline
+        try {
+            project.rustSettings.modify { it.useOffline = true }
+            action()
+        } finally {
+            project.rustSettings.modify { it.useOffline = oldStatus }
+        }
+    }
+
+    private fun checkCommandLine(
+        cmd: GeneralCommandLine,
+        expected: String,
+        expectedWin: String
+    ) {
         val cleaned = (if (SystemInfo.isWindows) expectedWin else expected).trimIndent()
         val actual = cmd.debug().trim()
         check(cleaned == actual) {