This is an automated email from the ASF dual-hosted git repository.

vieiro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans.git


The following commit(s) were added to refs/heads/master by this push:
     new dd6da592e1 Rust options panel and Cargo executable selection
     new 4d5259a4e7 Merge pull request #5651 from vieiro/rust-options
dd6da592e1 is described below

commit dd6da592e153a12456b4b55f0ff9bccb2bdae2c8
Author: Antonio <[email protected]>
AuthorDate: Sun Mar 5 16:15:23 2023 +0100

    Rust options panel and Cargo executable selection
    
    - Rust Option panel with Cargo subpanel.
    - Cargo path is used in Cargo commands.
    - When cargo path is not found a Notification is shown to the user.
---
 .github/workflows/main.yml                         |   3 +
 nbbuild/cluster.properties                         |   1 +
 rust/rust.cargo/nbproject/project.xml              |   8 +
 .../modules/rust/cargo/impl/CargoBuildImpl.java    |  47 +++-
 rust/rust.kit/nbproject/project.xml                |   8 +
 rust/rust.options/build.xml                        |  25 ++
 rust/rust.options/licenseinfo.xml                  |  30 +++
 rust/rust.options/manifest.mf                      |   5 +
 rust/rust.options/nbproject/project.properties     |  20 ++
 rust/rust.options/nbproject/project.xml            |  97 +++++++
 .../modules/rust/options/Bundle.properties         |  22 ++
 .../modules/rust/options/api/CargoOptions.java     |  41 +++
 .../modules/rust/options/cargo/Bundle.properties   |  24 ++
 .../options/cargo/CargoOptionsPanelController.java | 101 +++++++
 .../modules/rust/options/cargo/CargoPanel.form     | 150 +++++++++++
 .../modules/rust/options/cargo/CargoPanel.java     | 299 +++++++++++++++++++++
 .../rust/options/impl/CargoOptionsImpl.java        | 148 ++++++++++
 .../modules/rust/options/package-info.java         |  30 +++
 .../modules/rust/options/rust-logo-big.png         | Bin 0 -> 2044 bytes
 .../rust/options/impl/CargoOptionsImplTest.java    |  68 +++++
 .../rust/project/RustProjectActionProvider.java    |   2 +
 21 files changed, 1122 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3da769f0f7..05afea0513 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1874,6 +1874,9 @@ jobs:
       - name: rust/rust.grammar
         run: ant $OPTS -f rust/rust.grammar test
 
+      - name: rust/rust.options
+        run: ant $OPTS -f rust/rust.options test
+
       - name: Create Test Summary
         uses: test-summary/action@v2
         if: failure()
diff --git a/nbbuild/cluster.properties b/nbbuild/cluster.properties
index 70cec32db5..04ceee4740 100644
--- a/nbbuild/cluster.properties
+++ b/nbbuild/cluster.properties
@@ -1091,6 +1091,7 @@ nb.cluster.rust=\
         rust.cargo,\
         rust.grammar,\
         rust.kit,\
+        rust.options,\
         rust.project,\
         rust.project.api,\
         rust.sources
diff --git a/rust/rust.cargo/nbproject/project.xml 
b/rust/rust.cargo/nbproject/project.xml
index f1797ae8ed..804a7bb7e2 100644
--- a/rust/rust.cargo/nbproject/project.xml
+++ b/rust/rust.cargo/nbproject/project.xml
@@ -95,6 +95,14 @@
                         <specification-version>1.106</specification-version>
                     </run-dependency>
                 </dependency>
+                <dependency>
+                    
<code-name-base>org.netbeans.modules.rust.options</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>1.11</specification-version>
+                    </run-dependency>
+                </dependency>
                 <dependency>
                     
<code-name-base>org.netbeans.modules.rust.project.api</code-name-base>
                     <build-prerequisite/>
diff --git 
a/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/CargoBuildImpl.java 
b/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/CargoBuildImpl.java
index 2400aa5e2b..1d0ad7f579 100644
--- 
a/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/CargoBuildImpl.java
+++ 
b/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/CargoBuildImpl.java
@@ -22,8 +22,10 @@ import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Future;
@@ -41,7 +43,10 @@ import org.openide.windows.IOProvider;
 import org.openide.windows.InputOutput;
 import org.netbeans.modules.rust.cargo.api.Cargo;
 import org.netbeans.modules.rust.cargo.api.RustPackage;
+import org.netbeans.modules.rust.options.api.CargoOptions;
 import org.openide.LifecycleManager;
+import org.openide.util.Exceptions;
+import org.openide.util.NbBundle;
 
 /**
  * CargoBuildImpl is used to invoke a set of predefined "cargo" commands.
@@ -84,8 +89,8 @@ public class CargoBuildImpl implements Cargo {
             File workingDirectory = 
FileUtil.toFile(cargotoml.getFileObject()).getParentFile();
             pb.setWorkingDirectory(workingDirectory.getAbsolutePath());
             pb.setRedirectErrorStream(false);
-            // TODO: Parametrize the "cargo" path
-            pb.setExecutable("cargo"); // NOI18N
+            Path cargo = CargoOptions.getCargoLocation(false);
+            pb.setExecutable(cargo.toString());
             pb.setArguments(arguments);
 
             console.getOut().println(String.format("%n$ cargo %s", 
String.join(" ", arguments))); // NOI18N
@@ -97,6 +102,10 @@ public class CargoBuildImpl implements Cargo {
     /**
      * A Callable used to invoke an array of commands.
      */
+    @NbBundle.Messages({
+        "MSG_WORKING_DIRECTORY=Working directory:",
+        "MSG_CARGO_PATH=Cargo: "
+    })
     public static class SequentialCargoProcesses implements Callable<Integer> {
 
         private final CargoTOML cargotoml;
@@ -111,6 +120,10 @@ public class CargoBuildImpl implements Cargo {
 
         @Override
         public Integer call() throws Exception {
+            Path cargo = CargoOptions.getCargoLocation(true);
+            if (cargo == null) {
+                return -1;
+            }
             // Get a proper console for the input/output
             String projectName = cargotoml.getPackageName();
             String commandNames = 
Arrays.stream(commands).map(CargoCommand::getDisplayName).collect(Collectors.joining(","));
 // NOI18N
@@ -118,6 +131,14 @@ public class CargoBuildImpl implements Cargo {
             InputOutput console = IOProvider.getDefault().getIO(ioName, false);
             console.select();
 
+            File workingDirectory = 
FileUtil.toFile(cargotoml.getFileObject()).getParentFile();
+            console.getOut().format("# %s %s%n", // NOI18N
+                    NbBundle.getMessage(CargoBuildImpl.class, 
"MSG_WORKING_DIRECTORY"), 
+                    workingDirectory.getAbsolutePath()); 
+            console.getOut().format("# %s %s%n",  // NOI18N
+                    NbBundle.getMessage(CargoBuildImpl.class, 
"MSG_CARGO_PATH"), 
+                    cargo); 
+
             ExecutionDescriptor ed = new ExecutionDescriptor()
                     .inputOutput(IOProvider.getDefault().getIO(ioName, false))
                     .inputVisible(true)
@@ -133,7 +154,12 @@ public class CargoBuildImpl implements Cargo {
                 CargoProcess process = new CargoProcess(cargotoml, command, 
options, console);
                 ExecutionService service = 
ExecutionService.newService(process, ed, ioName);
                 Future<Integer> resultCodeFuture = service.run();
-                resultCode = resultCodeFuture.get();
+                try {
+                    resultCode = resultCodeFuture.get();
+                } catch (Exception e) {
+                    console.getErr().println(String.format("Cargo execution 
failed: %s%n", e.getMessage()));
+                    Exceptions.printStackTrace(e);
+                }
                 if (resultCode != 0) {
                     console.getErr().println(String.format("Command \"cargo 
%s\" failed with exit status %d", // NOI18N
                             String.join(" ", command.arguments),
@@ -155,11 +181,15 @@ public class CargoBuildImpl implements Cargo {
     @Override
     public void cargo(CargoTOML cargotoml, CargoCommand[] commands, String... 
options) throws IOException {
         if (cargotoml == null) {
-            throw new NullPointerException("Missing Cargo.tomml file"); // 
NOI18N
+            throw new NullPointerException("Missing Cargo.toml file"); // 
NOI18N
         }
         if (commands.length == 0) {
             return;
         }
+        Path cargo = CargoOptions.getCargoLocation(true);
+        if (cargo == null) {
+            return;
+        }
         // Let's save stuff just in case
         LifecycleManager.getDefault().saveAll();
         requestProcessor.submit(new SequentialCargoProcesses(cargotoml, 
commands, options));
@@ -180,17 +210,20 @@ public class CargoBuildImpl implements Cargo {
 
         @Override
         public List<RustPackage> call() throws Exception {
+            Path cargo = CargoOptions.getCargoLocation(true);
+            if (cargo == null) {
+                return Collections.emptyList();
+            }
             org.netbeans.api.extexecution.base.ProcessBuilder pb = 
org.netbeans.api.extexecution.base.ProcessBuilder.getLocal();
             File workingDirectory = new File(System.getProperty("user.home")); 
// NOI18N
             pb.setWorkingDirectory(workingDirectory.getAbsolutePath());
             pb.setRedirectErrorStream(false);
-            // TODO: Parametrize the "cargo" path
-            pb.setExecutable("cargo"); // NOI18N
+            pb.setExecutable(cargo.toString());
             String[] arguments = {
                 "search", // NOI18N
                 text, // TODO: What happens with spaces?
                 "--limit", // NOI18N
-                "15", // NOI18N
+                "30", // NOI18N
                 "--color", // NOI18N
                 "never", // NOI18N
             };
diff --git a/rust/rust.kit/nbproject/project.xml 
b/rust/rust.kit/nbproject/project.xml
index 7ed8b5d1fb..2d2ab684a1 100644
--- a/rust/rust.kit/nbproject/project.xml
+++ b/rust/rust.kit/nbproject/project.xml
@@ -37,6 +37,14 @@
                         <specification-version>1.11</specification-version>
                     </run-dependency>
                 </dependency>
+                <dependency>
+                    
<code-name-base>org.netbeans.modules.rust.options</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>1.11</specification-version>
+                    </run-dependency>
+                </dependency>
                 <dependency>
                     
<code-name-base>org.netbeans.modules.rust.project</code-name-base>
                     <run-dependency>
diff --git a/rust/rust.options/build.xml b/rust/rust.options/build.xml
new file mode 100644
index 0000000000..824d7310de
--- /dev/null
+++ b/rust/rust.options/build.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<project basedir="." default="netbeans" name="rust/rust.options">
+    <description>Builds, tests, and runs the project 
org.netbeans.modules.rust.options</description>
+    <import file="../../nbbuild/templates/projectized.xml"/>
+</project>
diff --git a/rust/rust.options/licenseinfo.xml 
b/rust/rust.options/licenseinfo.xml
new file mode 100644
index 0000000000..beb0827d2c
--- /dev/null
+++ b/rust/rust.options/licenseinfo.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<licenseinfo>
+    <fileset>
+        <file>src/org/netbeans/modules/rust/options/rust-logo-big.png</file>
+        <license ref="Apache-2.0-ASF" />
+        <comment type="COMMENT_UNSUPPORTED">
+            Icon for Rust Option Dialog.
+        </comment>
+    </fileset>
+</licenseinfo>
diff --git a/rust/rust.options/manifest.mf b/rust/rust.options/manifest.mf
new file mode 100644
index 0000000000..ea3e6fbe69
--- /dev/null
+++ b/rust/rust.options/manifest.mf
@@ -0,0 +1,5 @@
+Manifest-Version: 1.0
+OpenIDE-Module: org.netbeans.modules.rust.options
+OpenIDE-Module-Implementation-Version: 1
+OpenIDE-Module-Localizing-Bundle: 
org/netbeans/modules/rust/options/Bundle.properties
+
diff --git a/rust/rust.options/nbproject/project.properties 
b/rust/rust.options/nbproject/project.properties
new file mode 100644
index 0000000000..6744a59b1e
--- /dev/null
+++ b/rust/rust.options/nbproject/project.properties
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+javac.source=1.8
+javac.compilerargs=-Xlint -Xlint:-serial
+spec.version.base=1.11
diff --git a/rust/rust.options/nbproject/project.xml 
b/rust/rust.options/nbproject/project.xml
new file mode 100644
index 0000000000..85ae51931b
--- /dev/null
+++ b/rust/rust.options/nbproject/project.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<project xmlns="http://www.netbeans.org/ns/project/1";>
+    <type>org.netbeans.modules.apisupport.project</type>
+    <configuration>
+        <data xmlns="http://www.netbeans.org/ns/nb-module-project/3";>
+            <code-name-base>org.netbeans.modules.rust.options</code-name-base>
+            <module-dependencies>
+                <dependency>
+                    
<code-name-base>org.netbeans.modules.options.api</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.65</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    
<code-name-base>org.netbeans.modules.rust.project.api</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>1.11</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.awt</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.88</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.util</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>9.28</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.util.lookup</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>8.54</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.util.ui</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>9.29</specification-version>
+                    </run-dependency>
+                </dependency>
+            </module-dependencies>
+            <test-dependencies>
+                <test-type>
+                    <name>unit</name>
+                    <test-dependency>
+                        
<code-name-base>org.netbeans.libs.junit4</code-name-base>
+                        <compile-dependency/>
+                    </test-dependency>
+                    <test-dependency>
+                        
<code-name-base>org.netbeans.modules.nbjunit</code-name-base>
+                        <recursive/>
+                        <compile-dependency/>
+                    </test-dependency>
+                </test-type>
+            </test-dependencies>
+            <public-packages>
+                <package>org.netbeans.modules.rust.options.api</package>
+            </public-packages>
+        </data>
+    </configuration>
+</project>
diff --git 
a/rust/rust.options/src/org/netbeans/modules/rust/options/Bundle.properties 
b/rust/rust.options/src/org/netbeans/modules/rust/options/Bundle.properties
new file mode 100644
index 0000000000..6a861df55d
--- /dev/null
+++ b/rust/rust.options/src/org/netbeans/modules/rust/options/Bundle.properties
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+OpenIDE-Module-Display-Category=Rust
+OpenIDE-Module-Long-Description=\
+    Rust Option Panel.
+OpenIDE-Module-Name=Rust Options
+OpenIDE-Module-Short-Description=Rust Options
diff --git 
a/rust/rust.options/src/org/netbeans/modules/rust/options/api/CargoOptions.java 
b/rust/rust.options/src/org/netbeans/modules/rust/options/api/CargoOptions.java
new file mode 100644
index 0000000000..883c24e4cf
--- /dev/null
+++ 
b/rust/rust.options/src/org/netbeans/modules/rust/options/api/CargoOptions.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.rust.options.api;
+
+import java.nio.file.Path;
+import org.netbeans.modules.rust.options.impl.CargoOptionsImpl;
+
+/**
+ * Returns the options for Rust and Cargo.
+ */
+public final class CargoOptions {
+
+    /**
+     * Returns the Path where cargo is installed, or null.
+     *
+     * @param verifying If true then the path is checked for validity (the path
+     * exists and is executable) and if it is incorrect then a notification is
+     * shown to the user.
+     * @return The Path where cargo is installed, or null.
+     */
+    public static final Path getCargoLocation(boolean verifying) {
+        return CargoOptionsImpl.getCargoLocation(verifying);
+    }
+
+}
diff --git 
a/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/Bundle.properties
 
b/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/Bundle.properties
new file mode 100644
index 0000000000..210f11172f
--- /dev/null
+++ 
b/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/Bundle.properties
@@ -0,0 +1,24 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+CargoPanel.lblCargoLocation.text=Cargo executable location:
+CargoPanel.txtCargoPath.text=
+CargoPanel.cmdBrowse.text=Browse...
+CargoPanel.txtCargoPath.toolTipText=The full path pointing to cargo or 
cargo.exe executable
+CargoPanel.cmdBrowse.toolTipText=Browse cargo in your filesystem
+CargoPanel.lblCargoVersion.text=
+CargoPanel.cmdGetVersion.text=Get version
diff --git 
a/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/CargoOptionsPanelController.java
 
b/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/CargoOptionsPanelController.java
new file mode 100644
index 0000000000..0474a39e18
--- /dev/null
+++ 
b/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/CargoOptionsPanelController.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.rust.options.cargo;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+import org.netbeans.spi.options.OptionsPanelController;
+import org.openide.util.HelpCtx;
+import org.openide.util.Lookup;
+
[email protected](
+        location = "Rust",
+        displayName = "#AdvancedOption_DisplayName_Cargo",
+        keywords = "#AdvancedOption_Keywords_Cargo",
+        keywordsCategory = "Rust/Cargo",
+        position = 1000
+)
[email protected]({"AdvancedOption_DisplayName_Cargo=Cargo", 
"AdvancedOption_Keywords_Cargo=rust cargo"})
+public final class CargoOptionsPanelController extends OptionsPanelController {
+
+    private CargoPanel panel;
+    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
+    private boolean changed;
+
+    public void update() {
+        getPanel().load();
+        changed = false;
+    }
+
+    public void applyChanges() {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                getPanel().store();
+                changed = false;
+            }
+        });
+    }
+
+    public void cancel() {
+        // need not do anything special, if no changes have been persisted yet
+    }
+
+    public boolean isValid() {
+        return getPanel().valid();
+    }
+
+    public boolean isChanged() {
+        return changed;
+    }
+
+    public HelpCtx getHelpCtx() {
+        return null; // new HelpCtx("...ID") if you have a help set
+    }
+
+    public JComponent getComponent(Lookup masterLookup) {
+        return getPanel();
+    }
+
+    public void addPropertyChangeListener(PropertyChangeListener l) {
+        pcs.addPropertyChangeListener(l);
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener l) {
+        pcs.removePropertyChangeListener(l);
+    }
+
+    private CargoPanel getPanel() {
+        if (panel == null) {
+            panel = new CargoPanel(this);
+        }
+        return panel;
+    }
+
+    void changed() {
+        if (!changed) {
+            changed = true;
+            pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, false, 
true);
+        }
+        pcs.firePropertyChange(OptionsPanelController.PROP_VALID, null, null);
+    }
+
+}
diff --git 
a/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/CargoPanel.form 
b/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/CargoPanel.form
new file mode 100644
index 0000000000..eeecf5ba0a
--- /dev/null
+++ 
b/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/CargoPanel.form
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+
+<Form version="1.5" maxVersion="1.9" 
type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" 
value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" 
type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" 
value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" 
type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" 
value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" 
value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" 
type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" 
value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" 
value="2"/>
+    <AuxValue name="designerSize" type="java.awt.Dimension" 
value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,3,113,0,0,2,-22"/>
+  </AuxValues>
+
+  <Layout 
class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
+  <SubComponents>
+    <Component class="javax.swing.JLabel" name="lblCargoLocation">
+      <Properties>
+        <Property name="labelFor" type="java.awt.Component" 
editor="org.netbeans.modules.form.ComponentChooserEditor">
+          <ComponentRef name="txtCargoPath"/>
+        </Property>
+        <Property name="text" type="java.lang.String" 
editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString 
bundle="org/netbeans/modules/rust/options/cargo/Bundle.properties" 
key="CargoPanel.lblCargoLocation.text" 
replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, 
&quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Constraints>
+        <Constraint 
layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" 
value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="0" gridY="0" gridWidth="4" gridHeight="1" 
fill="2" ipadX="0" ipadY="0" insetsTop="8" insetsLeft="8" insetsBottom="8" 
insetsRight="8" anchor="18" weightX="0.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Component class="javax.swing.JTextField" name="txtCargoPath">
+      <Properties>
+        <Property name="text" type="java.lang.String" 
editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString 
bundle="org/netbeans/modules/rust/options/cargo/Bundle.properties" 
key="CargoPanel.txtCargoPath.text" 
replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, 
&quot;{key}&quot;)"/>
+        </Property>
+        <Property name="toolTipText" type="java.lang.String" 
editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString 
bundle="org/netbeans/modules/rust/options/cargo/Bundle.properties" 
key="CargoPanel.txtCargoPath.toolTipText" 
replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, 
&quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" 
listener="java.awt.event.ActionListener" 
parameters="java.awt.event.ActionEvent" handler="txtCargoPathActionPerformed"/>
+      </Events>
+      <Constraints>
+        <Constraint 
layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" 
value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" 
fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="8" insetsBottom="0" 
insetsRight="0" anchor="256" weightX="1.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Component class="javax.swing.JButton" name="cmdBrowse">
+      <Properties>
+        <Property name="text" type="java.lang.String" 
editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString 
bundle="org/netbeans/modules/rust/options/cargo/Bundle.properties" 
key="CargoPanel.cmdBrowse.text" 
replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, 
&quot;{key}&quot;)"/>
+        </Property>
+        <Property name="toolTipText" type="java.lang.String" 
editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString 
bundle="org/netbeans/modules/rust/options/cargo/Bundle.properties" 
key="CargoPanel.cmdBrowse.toolTipText" 
replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, 
&quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" 
listener="java.awt.event.ActionListener" 
parameters="java.awt.event.ActionEvent" handler="cmdBrowseActionPerformed"/>
+      </Events>
+      <Constraints>
+        <Constraint 
layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" 
value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="2" gridY="1" gridWidth="1" gridHeight="1" 
fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="8" insetsBottom="0" 
insetsRight="0" anchor="256" weightX="0.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Component class="javax.swing.JButton" name="cmdGetVersion">
+      <Properties>
+        <Property name="text" type="java.lang.String" 
editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString 
bundle="org/netbeans/modules/rust/options/cargo/Bundle.properties" 
key="CargoPanel.cmdGetVersion.text" 
replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, 
&quot;{key}&quot;)"/>
+        </Property>
+        <Property name="enabled" type="boolean" value="false"/>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" 
listener="java.awt.event.ActionListener" 
parameters="java.awt.event.ActionEvent" handler="cmdGetVersionActionPerformed"/>
+      </Events>
+      <Constraints>
+        <Constraint 
layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" 
value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="3" gridY="1" gridWidth="1" gridHeight="1" 
fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="8" insetsBottom="0" 
insetsRight="8" anchor="10" weightX="0.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Component class="javax.swing.JLabel" name="lblCargoVersion">
+      <Properties>
+        <Property name="font" type="java.awt.Font" 
editor="org.netbeans.modules.form.editors2.FontEditor">
+          <FontInfo relative="true">
+            <Font bold="true" component="lblCargoVersion" property="font" 
relativeSize="true" size="0"/>
+          </FontInfo>
+        </Property>
+        <Property name="text" type="java.lang.String" 
editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString 
bundle="org/netbeans/modules/rust/options/cargo/Bundle.properties" 
key="CargoPanel.lblCargoVersion.text" 
replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, 
&quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Constraints>
+        <Constraint 
layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" 
value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="0" gridY="2" gridWidth="4" gridHeight="1" 
fill="2" ipadX="0" ipadY="0" insetsTop="8" insetsLeft="8" insetsBottom="8" 
insetsRight="8" anchor="11" weightX="1.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Container class="javax.swing.JPanel" name="jPanel1">
+      <Properties>
+        <Property name="preferredSize" type="java.awt.Dimension" 
editor="org.netbeans.beaninfo.editors.DimensionEditor">
+          <Dimension value="[8, 8]"/>
+        </Property>
+      </Properties>
+      <Constraints>
+        <Constraint 
layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" 
value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="0" gridY="3" gridWidth="4" gridHeight="1" 
fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" 
insetsRight="0" anchor="10" weightX="1.0" weightY="1.0"/>
+        </Constraint>
+      </Constraints>
+
+      <Layout>
+        <DimensionLayout dim="0">
+          <Group type="103" groupAlignment="0" attributes="0">
+              <EmptySpace min="0" pref="746" max="32767" attributes="0"/>
+          </Group>
+        </DimensionLayout>
+        <DimensionLayout dim="1">
+          <Group type="103" groupAlignment="0" attributes="0">
+              <EmptySpace min="0" pref="799" max="32767" attributes="0"/>
+          </Group>
+        </DimensionLayout>
+      </Layout>
+    </Container>
+  </SubComponents>
+</Form>
diff --git 
a/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/CargoPanel.java 
b/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/CargoPanel.java
new file mode 100644
index 0000000000..e120b546d7
--- /dev/null
+++ 
b/rust/rust.options/src/org/netbeans/modules/rust/options/cargo/CargoPanel.java
@@ -0,0 +1,299 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.rust.options.cargo;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import javax.swing.JFileChooser;
+import javax.swing.SwingWorker;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.filechooser.FileFilter;
+import org.netbeans.modules.rust.options.impl.CargoOptionsImpl;
+import org.openide.util.NbBundle;
+import org.openide.util.Utilities;
+
+final class CargoPanel extends javax.swing.JPanel implements DocumentListener {
+
+    private final CargoOptionsPanelController controller;
+    private JFileChooser fileChooser;
+    private SwingWorker<String, String> versionWorker;
+
+    CargoPanel(CargoOptionsPanelController controller) {
+        this.controller = controller;
+        initComponents();
+        txtCargoPath.getDocument().addDocumentListener(this);
+    }
+
+    private SwingWorker<String, String> newWorker() {
+        return new SwingWorker<String, String>() {
+            @Override
+            protected String doInBackground() throws Exception {
+                String cargo = txtCargoPath.getText();
+                return getVersionOf(cargo);
+            }
+
+            @Override
+            protected void done() {
+                versionWorker = null;
+                String version;
+                try {
+                    version = get();
+                    lblCargoVersion.setText(version);
+                } catch (Exception ex) {
+                    lblCargoVersion.setText("");
+                }
+            }
+        };
+    }
+
+    private String getVersionOf(String aPossibleCargoPath) throws Exception {
+        File cargo = Paths.get(aPossibleCargoPath).toFile();
+        if (cargo.isFile() && cargo.canExecute()) {
+            return getVersionOf(cargo);
+        }
+        return null;
+    }
+
+    private String getVersionOf(File cargo) throws Exception {
+        Process p = Runtime.getRuntime().exec(new 
String[]{cargo.getAbsolutePath(), "-V"});
+        int ok = p.waitFor();
+        if (ok == 0) {
+            String version = null;
+            try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(p.getInputStream()))) {
+                do {
+                    String line = reader.readLine();
+                    if (line == null) {
+                        break;
+                    }
+                    version = line;
+                } while (true);
+            }
+            p.destroy();
+            return version;
+        }
+        return null;
+    }
+
+    /**
+     * This method is called from within the constructor to initialize the 
form.
+     * WARNING: Do NOT modify this code. The content of this method is always
+     * regenerated by the Form Editor.
+     */
+    // <editor-fold defaultstate="collapsed" desc="Generated 
Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+        java.awt.GridBagConstraints gridBagConstraints;
+
+        lblCargoLocation = new javax.swing.JLabel();
+        txtCargoPath = new javax.swing.JTextField();
+        cmdBrowse = new javax.swing.JButton();
+        cmdGetVersion = new javax.swing.JButton();
+        lblCargoVersion = new javax.swing.JLabel();
+        jPanel1 = new javax.swing.JPanel();
+
+        setLayout(new java.awt.GridBagLayout());
+
+        lblCargoLocation.setLabelFor(txtCargoPath);
+        org.openide.awt.Mnemonics.setLocalizedText(lblCargoLocation, 
org.openide.util.NbBundle.getMessage(CargoPanel.class, 
"CargoPanel.lblCargoLocation.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 0;
+        gridBagConstraints.gridwidth = 4;
+        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.insets = new java.awt.Insets(8, 8, 8, 8);
+        add(lblCargoLocation, gridBagConstraints);
+
+        
txtCargoPath.setText(org.openide.util.NbBundle.getMessage(CargoPanel.class, 
"CargoPanel.txtCargoPath.text")); // NOI18N
+        
txtCargoPath.setToolTipText(org.openide.util.NbBundle.getMessage(CargoPanel.class,
 "CargoPanel.txtCargoPath.toolTipText")); // NOI18N
+        txtCargoPath.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                txtCargoPathActionPerformed(evt);
+            }
+        });
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 1;
+        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 0);
+        add(txtCargoPath, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(cmdBrowse, 
org.openide.util.NbBundle.getMessage(CargoPanel.class, 
"CargoPanel.cmdBrowse.text")); // NOI18N
+        
cmdBrowse.setToolTipText(org.openide.util.NbBundle.getMessage(CargoPanel.class, 
"CargoPanel.cmdBrowse.toolTipText")); // NOI18N
+        cmdBrowse.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                cmdBrowseActionPerformed(evt);
+            }
+        });
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 2;
+        gridBagConstraints.gridy = 1;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE;
+        gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 0);
+        add(cmdBrowse, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(cmdGetVersion, 
org.openide.util.NbBundle.getMessage(CargoPanel.class, 
"CargoPanel.cmdGetVersion.text")); // NOI18N
+        cmdGetVersion.setEnabled(false);
+        cmdGetVersion.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                cmdGetVersionActionPerformed(evt);
+            }
+        });
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 3;
+        gridBagConstraints.gridy = 1;
+        gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 8);
+        add(cmdGetVersion, gridBagConstraints);
+
+        
lblCargoVersion.setFont(lblCargoVersion.getFont().deriveFont(lblCargoVersion.getFont().getStyle()
 | java.awt.Font.BOLD));
+        org.openide.awt.Mnemonics.setLocalizedText(lblCargoVersion, 
org.openide.util.NbBundle.getMessage(CargoPanel.class, 
"CargoPanel.lblCargoVersion.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 2;
+        gridBagConstraints.gridwidth = 4;
+        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(8, 8, 8, 8);
+        add(lblCargoVersion, gridBagConstraints);
+
+        jPanel1.setPreferredSize(new java.awt.Dimension(8, 8));
+
+        javax.swing.GroupLayout jPanel1Layout = new 
javax.swing.GroupLayout(jPanel1);
+        jPanel1.setLayout(jPanel1Layout);
+        jPanel1Layout.setHorizontalGroup(
+            
jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGap(0, 746, Short.MAX_VALUE)
+        );
+        jPanel1Layout.setVerticalGroup(
+            
jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGap(0, 799, Short.MAX_VALUE)
+        );
+
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 3;
+        gridBagConstraints.gridwidth = 4;
+        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.weighty = 1.0;
+        add(jPanel1, gridBagConstraints);
+    }// </editor-fold>//GEN-END:initComponents
+
+    private void txtCargoPathActionPerformed(java.awt.event.ActionEvent evt) 
{//GEN-FIRST:event_txtCargoPathActionPerformed
+        controller.changed();
+    }//GEN-LAST:event_txtCargoPathActionPerformed
+
+    private void cmdBrowseActionPerformed(java.awt.event.ActionEvent evt) 
{//GEN-FIRST:event_cmdBrowseActionPerformed
+
+        showFileChooser();
+
+    }//GEN-LAST:event_cmdBrowseActionPerformed
+
+    private void cmdGetVersionActionPerformed(java.awt.event.ActionEvent evt) 
{//GEN-FIRST:event_cmdGetVersionActionPerformed
+        if (versionWorker == null) {
+            versionWorker = newWorker();
+            versionWorker.execute();
+        }
+    }//GEN-LAST:event_cmdGetVersionActionPerformed
+
+    @NbBundle.Messages({
+        "TXT_EXECUTABLE=Executable files"
+    })
+    void showFileChooser() {
+        if (fileChooser == null) {
+            String cargoName = "cargo" + (Utilities.isWindows() ? ".exe" : 
""); // NOI18N
+            fileChooser = new JFileChooser(System.getProperty("user.home")); 
// NOI18N
+            fileChooser.setFileFilter(new FileFilter() {
+                @Override
+                public boolean accept(File f) {
+                    boolean valid = f.isDirectory();
+                    valid = valid
+                            || (f.isFile() && f.exists() && f.canExecute() && 
cargoName.equals(f.getName()));
+                    return valid;
+                }
+
+                @Override
+                public String getDescription() {
+                    return Bundle.TXT_EXECUTABLE();
+                }
+            });
+            fileChooser.setDialogType(JFileChooser.OPEN_DIALOG);
+            fileChooser.setFileHidingEnabled(false);
+        }
+        fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+        int result = fileChooser.showDialog(this, null);
+        if (result == JFileChooser.APPROVE_OPTION) {
+            
txtCargoPath.setText(fileChooser.getSelectedFile().getAbsolutePath());
+        }
+    }
+
+    void load() {
+        Path cargoLocation = CargoOptionsImpl.getCargoLocation(true);
+        txtCargoPath.setText(cargoLocation == null ? "" : 
cargoLocation.toString());
+    }
+
+    void store() {
+        CargoOptionsImpl.setCargoLocation(txtCargoPath.getText());
+        Path cargoLocation = CargoOptionsImpl.getCargoLocation(true);
+        txtCargoPath.setText(cargoLocation == null ? "" : 
cargoLocation.toString());
+    }
+
+    boolean valid() {
+        File cargo = new File(txtCargoPath.getText());
+        return cargo.exists() && cargo.isFile() && cargo.canExecute();
+    }
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JButton cmdBrowse;
+    private javax.swing.JButton cmdGetVersion;
+    private javax.swing.JPanel jPanel1;
+    private javax.swing.JLabel lblCargoLocation;
+    private javax.swing.JLabel lblCargoVersion;
+    private javax.swing.JTextField txtCargoPath;
+    // End of variables declaration//GEN-END:variables
+
+    @Override
+    public void insertUpdate(DocumentEvent e) {
+        documentChanged(e);
+    }
+
+    @Override
+    public void removeUpdate(DocumentEvent e) {
+        documentChanged(e);
+    }
+
+    @Override
+    public void changedUpdate(DocumentEvent e) {
+        documentChanged(e);
+    }
+
+    private void documentChanged(DocumentEvent e) {
+        controller.changed();
+        File file = Paths.get(txtCargoPath.getText()).toFile();
+        boolean executable = file.exists() && file.canExecute();
+        cmdGetVersion.setEnabled(executable);
+    }
+
+}
diff --git 
a/rust/rust.options/src/org/netbeans/modules/rust/options/impl/CargoOptionsImpl.java
 
b/rust/rust.options/src/org/netbeans/modules/rust/options/impl/CargoOptionsImpl.java
new file mode 100644
index 0000000000..c4fad5c3e8
--- /dev/null
+++ 
b/rust/rust.options/src/org/netbeans/modules/rust/options/impl/CargoOptionsImpl.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.rust.options.impl;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.prefs.Preferences;
+import javax.swing.ImageIcon;
+import javax.swing.SwingUtilities;
+import org.netbeans.api.options.OptionsDisplayer;
+import org.netbeans.modules.rust.project.api.RustProjectAPI;
+import org.openide.awt.NotificationDisplayer;
+import org.openide.util.ImageUtilities;
+import org.openide.util.NbBundle;
+import org.openide.util.NbPreferences;
+import org.openide.util.Utilities;
+
+/**
+ * CargoOptions implementation.
+ */
+public final class CargoOptionsImpl {
+
+    private static final String CARGO_LOCATION_KEY = "cargo-location"; // 
NOI18N
+
+    /**
+     * Finds "cargo" (or "cargo.exe" on Windows) in any of the directories in
+     * the PATH environment variable.
+     *
+     * @return
+     */
+    private static File findCargoInPath() {
+        String path = System.getenv("PATH"); // NOI18N
+        path = path == null ? "" : path;
+        String[] parts = path.split(File.pathSeparator);
+        String extension = Utilities.isWindows() ? ".exe" : ""; // NOI18N
+        for (String part : parts) {
+            File file = new File(part, "cargo" + extension); // NOI18N
+            if (file.canExecute()) {
+                return file;
+            }
+        }
+        return null;
+    }
+
+    private static Preferences getPreferences() {
+        return NbPreferences.forModule(CargoOptionsImpl.class);
+    }
+
+    /**
+     * Returns the full path to the "cargo" executable, or null if none exists.
+     * If no preferences are present then we try 
"$HOME/.cargo/bin/cargo[.exe]",
+     * and set the property.
+     *
+     * @param verifying true to verify that the path is indeed executable
+     * @return The full path to the cargo executable, or null.
+     */
+    public static Path getCargoLocation(boolean verifying) {
+        String cargo = getPreferences().get(CARGO_LOCATION_KEY, null);
+        if (cargo == null) {
+            File cargoExecutable = findCargoInPath();
+            if (cargoExecutable != null) {
+                cargo = cargoExecutable.getAbsolutePath();
+                setCargoLocation(cargo);
+                return Paths.get(cargo);
+            }
+        }
+        // Check if cargo is valid, if not then reset from preferences
+        if (cargo != null && verifying) {
+            File cargoExecutable = new File(cargo);
+            if (cargoExecutable.canExecute()) {
+                return Paths.get(cargo);
+            }
+            // Reset from prefernces
+            deleteCargoLocation();
+            cargo = null;
+        }
+        // Warn the user if cargo cannot be found
+        if (verifying) {
+            showCargoNotFoundNotification();
+        }
+        return cargo == null ? null : Paths.get(cargo);
+    }
+
+    /**
+     * Removes the previously saved cargo location.
+     */
+    public static void deleteCargoLocation() {
+        getPreferences().remove(CARGO_LOCATION_KEY);
+    }
+
+    /**
+     * Sets a new cargo location. It is ignored if this is not a valid cargo
+     * location.
+     *
+     * @param location The location (possibly an absolute path).
+     */
+    public static void setCargoLocation(String location) {
+        if (location == null) {
+            deleteCargoLocation();
+        } else {
+            File cargo = new File(location);
+            if (cargo.canExecute()) {
+                getPreferences().put(CARGO_LOCATION_KEY, 
cargo.getAbsolutePath());
+            }
+        }
+    }
+
+    /**
+     * Opens the "Rust" options dialog, focused in the "Cargo" tab.
+     */
+    public static void showRustCargoOptions() {
+        SwingUtilities.invokeLater(() -> {
+            OptionsDisplayer.getDefault().open("Rust/Cargo"); // NOI18N
+        });
+    }
+
+    @NbBundle.Messages({
+        "MISSING_CARGO_TITLE=Cargo command was not found.",
+        "MISSING_CARGO_DETAILS=Cargo could not be found in your PATH. Please 
select the cargo executable location"
+    })
+    public static void showCargoNotFoundNotification() {
+        NotificationDisplayer.Priority priority = 
NotificationDisplayer.Priority.HIGH;
+        String title = NbBundle.getMessage(CargoOptionsImpl.class, 
"MISSING_CARGO_TITLE"); // NOI18N
+        String details = NbBundle.getMessage(CargoOptionsImpl.class, 
"MISSING_CARGO_DETAILS"); // NOI18N
+        ImageIcon icon = new 
ImageIcon(ImageUtilities.loadImage(RustProjectAPI.ICON));
+        NotificationDisplayer.getDefault().notify(title, icon, details, 
(actionEvent) -> {
+            showRustCargoOptions();
+        });
+    }
+
+}
diff --git 
a/rust/rust.options/src/org/netbeans/modules/rust/options/package-info.java 
b/rust/rust.options/src/org/netbeans/modules/rust/options/package-info.java
new file mode 100644
index 0000000000..48dfafcc7d
--- /dev/null
+++ b/rust/rust.options/src/org/netbeans/modules/rust/options/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
[email protected](
+        id = "Rust",
+        categoryName = "#OptionsCategory_Name_Rust",
+        iconBase = "org/netbeans/modules/rust/options/rust-logo-big.png", 
+        keywords = "#OptionsCategory_Keywords_Rust", 
+        keywordsCategory = "Rust",
+        position = 10000)
[email protected](value = {"OptionsCategory_Name_Rust=Rust", 
"OptionsCategory_Keywords_Rust=rust cargo"})
+package org.netbeans.modules.rust.options;
+
+import org.netbeans.spi.options.OptionsPanelController;
+import org.openide.util.NbBundle;
diff --git 
a/rust/rust.options/src/org/netbeans/modules/rust/options/rust-logo-big.png 
b/rust/rust.options/src/org/netbeans/modules/rust/options/rust-logo-big.png
new file mode 100644
index 0000000000..a70a0eb130
Binary files /dev/null and 
b/rust/rust.options/src/org/netbeans/modules/rust/options/rust-logo-big.png 
differ
diff --git 
a/rust/rust.options/test/unit/src/org/netbeans/modules/rust/options/impl/CargoOptionsImplTest.java
 
b/rust/rust.options/test/unit/src/org/netbeans/modules/rust/options/impl/CargoOptionsImplTest.java
new file mode 100644
index 0000000000..0be626e687
--- /dev/null
+++ 
b/rust/rust.options/test/unit/src/org/netbeans/modules/rust/options/impl/CargoOptionsImplTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.rust.options.impl;
+
+import java.nio.file.Path;
+import java.util.Objects;
+import static org.junit.Assert.assertTrue;
+import org.junit.Test;
+
+/**
+ * Tests the CargoOptionsImpl.
+ */
+public class CargoOptionsImplTest {
+
+    @Test
+    public void testShouldNullPathDeleteCargoLocation() {
+        System.out.println("testShouldNullPathDeleteCargoLocation");
+        CargoOptionsImpl.setCargoLocation(null);
+    }
+
+    @Test
+    public void testShouldAutomaticallySetPathWhenAlreadyInstalled() {
+        
System.out.println("testShouldAutomaticallySetPathWhenAlreadyInstalled");
+
+        // Given that we're deleting the cargo path from preferences.
+        CargoOptionsImpl.deleteCargoLocation();
+
+        // When we get it again, probably from the default 
$HOME/.cargo/bin/cargo path
+        Path cargo = CargoOptionsImpl.getCargoLocation(true);
+
+        // Then if cargo is not null
+        if (cargo != null) {
+            // It must exist and be executable
+            assertTrue(cargo.toFile().canExecute());
+        }
+    }
+
+    @Test
+    public void testShouldFindCargoConsistently() {
+        System.out.println("testShouldFindCargoConsistently");
+
+        // Given a cargo found with verification
+        Path cargo = CargoOptionsImpl.getCargoLocation(true);
+
+        // When we get cargo without verification
+        Path cargo2 = CargoOptionsImpl.getCargoLocation(false);
+
+        // Then these must be equal
+        assertTrue(Objects.equals(cargo, cargo2));
+    }
+
+}
diff --git 
a/rust/rust.project/src/org/netbeans/modules/rust/project/RustProjectActionProvider.java
 
b/rust/rust.project/src/org/netbeans/modules/rust/project/RustProjectActionProvider.java
index e9214a259f..1d61265039 100644
--- 
a/rust/rust.project/src/org/netbeans/modules/rust/project/RustProjectActionProvider.java
+++ 
b/rust/rust.project/src/org/netbeans/modules/rust/project/RustProjectActionProvider.java
@@ -73,8 +73,10 @@ public final class RustProjectActionProvider implements 
ActionProvider {
                     break;
                 case COMMAND_RUN:
                     commands = new CargoCommand[]{CargoCommand.CARGO_RUN};
+                    break;
                 default:
                     LOG.log(Level.WARNING, String.format("Invoked action %s 
but cannot find a CargoBuild mode for it", command));
+                    return;
             }
             try {
                 build.cargo(project.getCargoTOML(), commands);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists

Reply via email to