Unverified Commit 95e4be41 authored by Neil C Smith's avatar Neil C Smith Committed by GitHub
Browse files

Merge pull request #2276 from apache/master

Sync master to release121 for 12.1-beta2
parents 08d65f16 de911d2b
......@@ -29,8 +29,8 @@ Apache NetBeans is an open source development environment, tooling platform, and
* TravisCI:
* [![Build Status](https://travis-ci.org/apache/netbeans.svg?branch=master)](https://travis-ci.org/apache/netbeans)
* Apache Jenkins:
* Linux: [![Build Status](https://builds.apache.org/view/M-R/view/NetBeans/job/netbeans-linux/badge/icon)](https://builds.apache.org/view/M-R/view/NetBeans/job/netbeans-linux/)
* Windows: [![Build Status](https://builds.apache.org/view/M-R/view/NetBeans/job/netbeans-windows/badge/icon)](https://builds.apache.org/view/M-R/view/NetBeans/job/netbeans-windows/)
* Linux: [![Build Status](https://ci-builds.apache.org/job/Netbeans/job/netbeans-linux/badge/icon)](https://ci-builds.apache.org/job/Netbeans/job/netbeans-linux/)
* Windows: [![Build Status](https://ci-builds.apache.org/job/Netbeans/job/netbeans-windows/badge/icon)](https://ci-builds.apache.org/job/Netbeans/job/netbeans-windows)
### Requirements
......
......@@ -87,12 +87,14 @@ public class UnconfiguredHint implements Runnable {
return ;
}
List<ErrorDescription> errors = new ArrayList<>();
String ccls = Utils.settings().get(Utils.KEY_CCLS_PATH, null);
if (ccls == null || !new File(ccls).canExecute() || !new File(ccls).isFile()) {
errors.add(ErrorDescriptionFactory.createErrorDescription(Severity.WARNING, "ccls not configured!", Collections.singletonList(new ConfigureCCLS()), doc, 0));
String ccls = Utils.getCCLSPath();
String clangd = Utils.getCLANGDPath();
if ((ccls == null || !new File(ccls).canExecute() || !new File(ccls).isFile()) &&
(clangd == null || !new File(clangd).canExecute() || !new File(clangd).isFile())) {
errors.add(ErrorDescriptionFactory.createErrorDescription(Severity.WARNING, "Neither ccls nor clangd configured!", Collections.singletonList(new ConfigureCCLS()), doc, 0));
} else {
Project prj = FileOwnerQuery.getOwner(file);
if (LanguageServerImpl.getProjectSettings(prj) == null) {
if (prj != null && LanguageServerImpl.getCompileCommandsDir(prj) == null) {
errors.add(ErrorDescriptionFactory.createErrorDescription(Severity.WARNING, "compile commands not configured", doc, 0));
}
}
......
......@@ -18,8 +18,19 @@
*/
package org.netbeans.modules.cpplite.editor;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import org.openide.filesystems.FileUtil;
import org.openide.util.NbPreferences;
import org.openide.util.Parameters;
/**
*
......@@ -27,8 +38,91 @@ import org.openide.util.NbPreferences;
*/
public class Utils {
public static final String KEY_CCLS_PATH = "ccls";
public static final String KEY_CLANGD_PATH = "clangd";
private static final String[] CCLS_NAMES = new String[] {"ccls"};
private static final String[] CLANGD_NAMES = new String[] {"clangd-10", "clangd", "clangd-9"};
public static Preferences settings() {
return NbPreferences.forModule(Utils.class);
}
private static List<String> cclsAutodetectedPaths;
public static synchronized String getCCLSPath() {
String path = settings().get(KEY_CCLS_PATH, null);
if (path == null || path.isEmpty()) {
if (cclsAutodetectedPaths == null) {
cclsAutodetectedPaths = findFileOnUsersPath(CCLS_NAMES);
}
if (!cclsAutodetectedPaths.isEmpty()) {
path = cclsAutodetectedPaths.get(0);
}
}
if (path == null || path.isEmpty()) {
return null;
}
return path;
}
private static List<String> clangdAutodetectedPaths;
public static synchronized String getCLANGDPath() {
String path = settings().get(KEY_CLANGD_PATH, null);
if (path == null || path.isEmpty()) {
if (clangdAutodetectedPaths == null) {
clangdAutodetectedPaths = findFileOnUsersPath(CLANGD_NAMES);
}
if (!clangdAutodetectedPaths.isEmpty()) {
path = clangdAutodetectedPaths.get(0);
}
}
if (path == null || path.isEmpty()) {
return null;
}
return path;
}
//TODO: copied from webcommon/javascript.nodejs/src/org/netbeans/modules/javascript/nodejs/util/FileUtils.java:
/**
* Find all the files (absolute path) with the given "filename" on user's PATH.
* <p>
* This method is suitable for *nix as well as windows.
* @param filenames the name of a file to find, more names can be provided.
* @return list of absolute paths of found files (order preserved according to input names).
* @see #findFileOnUsersPath(String)
*/
public static List<String> findFileOnUsersPath(String... filenames) {
Parameters.notNull("filenames", filenames); // NOI18N
String path = System.getenv("PATH"); // NOI18N
LOGGER.log(Level.FINE, "PATH: [{0}]", path);
if (path == null) {
return Collections.<String>emptyList();
}
// on linux there are usually duplicities in PATH
Set<String> dirs = new LinkedHashSet<>(Arrays.asList(path.split(File.pathSeparator)));
LOGGER.log(Level.FINE, "PATH dirs: {0}", dirs);
List<String> found = new ArrayList<>(dirs.size() * filenames.length);
for (String filename : filenames) {
Parameters.notNull("filename", filename); // NOI18N
for (String dir : dirs) {
File file = new File(dir, filename);
if (file.isFile()) {
String absolutePath = FileUtil.normalizeFile(file).getAbsolutePath();
LOGGER.log(Level.FINE, "File ''{0}'' found", absolutePath);
// not optimal but should be ok
if (!found.contains(absolutePath)) {
LOGGER.log(Level.FINE, "File ''{0}'' added to found files", absolutePath);
found.add(absolutePath);
}
}
}
}
LOGGER.log(Level.FINE, "Found files: {0}", found);
return found;
}
private static final Logger LOGGER = Logger.getLogger(Utils.class.getName());
}
......@@ -18,12 +18,14 @@
*/
package org.netbeans.modules.cpplite.editor.lsp;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ProcessBuilder.Redirect;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
......@@ -31,6 +33,8 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.PreferenceChangeEvent;
import java.util.prefs.PreferenceChangeListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.editor.mimelookup.MimeRegistrations;
import org.netbeans.api.project.Project;
......@@ -38,11 +42,11 @@ import org.netbeans.modules.cpplite.editor.Utils;
import org.netbeans.modules.cpplite.editor.file.MIMETypes;
import org.netbeans.modules.lsp.client.spi.ServerRestarter;
import org.netbeans.modules.lsp.client.spi.LanguageServerProvider;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.netbeans.modules.cpplite.editor.spi.CProjectConfigurationProvider;
import org.netbeans.modules.cpplite.editor.spi.CProjectConfigurationProvider.ProjectConfiguration;
import org.openide.filesystems.FileUtil;
import org.openide.modules.Places;
/**
*
......@@ -74,32 +78,45 @@ public class LanguageServerImpl implements LanguageServerProvider {
Utils.settings().addPreferenceChangeListener(new PreferenceChangeListener() {
@Override
public void preferenceChange(PreferenceChangeEvent evt) {
if (evt.getKey() == null || Utils.KEY_CCLS_PATH.equals(evt.getKey())) {
if (evt.getKey() == null || Utils.KEY_CCLS_PATH.equals(evt.getKey()) || Utils.KEY_CLANGD_PATH.equals(evt.getKey())) {
prj2Server.remove(prj);
restarter.restart();
Utils.settings().removePreferenceChangeListener(this);
}
}
});
String ccls = Utils.settings().get(Utils.KEY_CCLS_PATH, null);
if (ccls != null) {
String ccls = Utils.getCCLSPath();
String clangd = Utils.getCLANGDPath();
if (ccls != null || clangd != null) {
return prj2Server.computeIfAbsent(prj, (Project p) -> {
try {
List<String> command = new ArrayList<>();
command.add(ccls);
List<String> cat = getProjectSettings(prj);
if (cat != null) {
StringBuilder initOpt = new StringBuilder();
initOpt.append("--init={\"compilationDatabaseCommand\":\"");
String sep = "";
for (String c : cat) {
initOpt.append(sep);
initOpt.append(c);
sep = " ";
CProjectConfigurationProvider config = getProjectSettings(prj);
config.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
prj2Server.remove(prj);
restarter.restart();
config.removeChangeListener(this);
}
});
File compileCommandDirs = getCompileCommandsDir(config);
if (compileCommandDirs != null) {
if (ccls != null) {
command.add(ccls);
StringBuilder initOpt = new StringBuilder();
initOpt.append("--init={\"compilationDatabaseDirectory\":\"");
initOpt.append(compileCommandDirs.getAbsolutePath());
initOpt.append("\"}");
command.add(initOpt.toString());
} else {
command.add(clangd);
command.add("--compile-commands-dir=" + compileCommandDirs.getAbsolutePath());
command.add("--clang-tidy");
command.add("--completion-style=detailed");
}
initOpt.append("\"}");
command.add(initOpt.toString());
Process process = new ProcessBuilder(command).redirectError(Redirect.INHERIT).start();
return LanguageServerDescription.create(new CopyInput(process.getInputStream(), System.err), new CopyOutput(process.getOutputStream(), System.err), process);
}
......@@ -113,25 +130,60 @@ public class LanguageServerImpl implements LanguageServerProvider {
return null;
}
public static List<String> getProjectSettings(Project prj) {
public static File getCompileCommandsDir(Project prj) {
return getCompileCommandsDir(getProjectSettings(prj));
}
private static CProjectConfigurationProvider getProjectSettings(Project prj) {
CProjectConfigurationProvider configProvider = prj.getLookup().lookup(CProjectConfigurationProvider.class);
if (configProvider == null) {
configProvider = new CProjectConfigurationProvider() {
@Override
public ProjectConfiguration getProjectConfiguration() {
return new ProjectConfiguration(new File(FileUtil.toFile(prj.getProjectDirectory()), "compile_commands.json").getAbsolutePath());
}
@Override
public void addChangeListener(ChangeListener listener) {
}
@Override
public void removeChangeListener(ChangeListener listener) {
}
};
}
return configProvider;
}
private static int tempDirIndex = 0;
if (configProvider != null) {
ProjectConfiguration config = configProvider.getProjectConfiguration();
if (config != null && config.commandJsonCommand != null) {
return configProvider.getProjectConfiguration().commandJsonCommand;
} else if (config != null && configProvider.getProjectConfiguration().commandJsonPath != null) {
//TODO: Linux independent!
return Arrays.asList("cat", configProvider.getProjectConfiguration().commandJsonPath);
private static File getCompileCommandsDir(CProjectConfigurationProvider configProvider) {
ProjectConfiguration config = configProvider.getProjectConfiguration();
if (config.commandJsonCommand != null || configProvider.getProjectConfiguration().commandJsonPath != null) {
File tempFile = Places.getCacheSubfile("cpplite/compile_commands/" + tempDirIndex++ + "/compile_commands.json");
if (config.commandJsonCommand != null) {
try {
new ProcessBuilder(config.commandJsonCommand).redirectOutput(tempFile).redirectError(Redirect.INHERIT).start().waitFor();
} catch (IOException | InterruptedException ex) {
LOG.log(Level.WARNING, null, ex);
return null;
}
} else {
File commandsPath = new File(configProvider.getProjectConfiguration().commandJsonPath);
if (commandsPath.canRead()) {
try (InputStream in = new FileInputStream(commandsPath);
OutputStream out = new FileOutputStream(tempFile)) {
FileUtil.copy(in, out);
} catch (IOException ex) {
LOG.log(Level.WARNING, null, ex);
return null;
}
}
}
return null;
} else if (prj.getProjectDirectory().getFileObject("compile_commands.json") != null) {
//TODO: Linux independent!
return Arrays.asList("cat", FileUtil.toFile(prj.getProjectDirectory().getFileObject("compile_commands.json")).getAbsolutePath());
} else {
return null;
return tempFile.getParentFile();
}
return null;
}
private static class CopyInput extends InputStream {
private final InputStream delegate;
......
......@@ -16,5 +16,9 @@
# under the License.
CPPLitePanel.jLabel1.text=CCLS Location:
CPPLitePanel.jButton1.text=...
CPPLitePanel.cclsPath.text=
CPPLitePanel.jLabel2.text=clangd Location:
CPPLitePanel.jLabel3.text=<html><body>Please provide a path to either the <a href="https://github.com/MaskRay/ccls">ccls</a> or the <a href="https://clangd.llvm.org/">clangd</a> language protocol servers.\n<br>\nThese will be used by the editor to provide features like code completion.
CPPLitePanel.clangdPath.text=
CPPLitePanel.cclsBrowse.text=...
CPPLitePanel.clangdBrowse.text=...
......@@ -37,21 +37,50 @@
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<Component id="jLabel1" min="-2" max="-2" attributes="0"/>
<EmptySpace min="-2" max="-2" attributes="0"/>
<Component id="cclsPath" max="32767" attributes="0"/>
<EmptySpace min="-2" max="-2" attributes="0"/>
<Component id="jButton1" min="-2" max="-2" attributes="0"/>
<Group type="102" attributes="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<Group type="103" groupAlignment="0" attributes="0">
<Component id="jLabel1" alignment="0" min="-2" max="-2" attributes="0"/>
<Component id="jLabel2" alignment="0" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="cclsPath" max="32767" attributes="0"/>
<Component id="clangdPath" max="32767" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="clangdBrowse" min="-2" max="-2" attributes="0"/>
<Component id="cclsBrowse" min="-2" max="-2" attributes="0"/>
</Group>
</Group>
<Group type="102" attributes="0">
<Component id="jLabel3" min="-2" max="-2" attributes="0"/>
<EmptySpace min="0" pref="0" max="32767" attributes="0"/>
</Group>
</Group>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="103" groupAlignment="3" attributes="0">
<Component id="jLabel1" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="cclsPath" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="jButton1" alignment="3" min="-2" max="-2" attributes="0"/>
<Group type="102" alignment="1" attributes="0">
<Component id="jLabel3" min="-2" max="-2" attributes="0"/>
<EmptySpace type="separate" max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="jLabel1" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="cclsPath" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="cclsBrowse" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="jLabel2" min="-2" max="-2" attributes="0"/>
<Component id="clangdPath" min="-2" max="-2" attributes="0"/>
<Component id="clangdBrowse" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="32767" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
......@@ -71,10 +100,44 @@
</Property>
</Properties>
</Component>
<Component class="javax.swing.JButton" name="jButton1">
<Component class="javax.swing.JButton" name="cclsBrowse">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/netbeans/modules/cpplite/editor/lsp/options/Bundle.properties" key="CPPLitePanel.jButton1.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
<ResourceString bundle="org/netbeans/modules/cpplite/editor/lsp/options/Bundle.properties" key="CPPLitePanel.cclsBrowse.text" 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="cclsBrowseActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="jLabel2">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/netbeans/modules/cpplite/editor/lsp/options/Bundle.properties" key="CPPLitePanel.jLabel2.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
</Component>
<Component class="javax.swing.JTextField" name="clangdPath">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/netbeans/modules/cpplite/editor/lsp/options/Bundle.properties" key="CPPLitePanel.clangdPath.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
</Component>
<Component class="javax.swing.JButton" name="clangdBrowse">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/netbeans/modules/cpplite/editor/lsp/options/Bundle.properties" key="CPPLitePanel.clangdBrowse.text" 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="clangdBrowseActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="jLabel3">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/netbeans/modules/cpplite/editor/lsp/options/Bundle.properties" key="CPPLitePanel.jLabel3.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
</Component>
......
......@@ -18,6 +18,9 @@
*/
package org.netbeans.modules.cpplite.editor.lsp.options;
import java.io.File;
import javax.swing.JFileChooser;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.netbeans.modules.cpplite.editor.Utils;
......@@ -29,7 +32,7 @@ final class CPPLitePanel extends javax.swing.JPanel {
CPPLitePanel(CPPLiteOptionsPanelController controller) {
this.controller = controller;
initComponents();
cclsPath.getDocument().addDocumentListener(new DocumentListener() {
DocumentListener pathsModified = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
controller.changed();
......@@ -42,8 +45,9 @@ final class CPPLitePanel extends javax.swing.JPanel {
public void changedUpdate(DocumentEvent e) {
controller.changed();
}
});
// TODO listen to changes in form fields and call controller.changed()
};
cclsPath.getDocument().addDocumentListener(pathsModified);
clangdPath.getDocument().addDocumentListener(pathsModified);
}
/**
......@@ -56,40 +60,101 @@ final class CPPLitePanel extends javax.swing.JPanel {
jLabel1 = new javax.swing.JLabel();
cclsPath = new javax.swing.JTextField();
jButton1 = new javax.swing.JButton();
cclsBrowse = new javax.swing.JButton();
jLabel2 = new javax.swing.JLabel();
clangdPath = new javax.swing.JTextField();
clangdBrowse = new javax.swing.JButton();
jLabel3 = new javax.swing.JLabel();
org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(CPPLitePanel.class, "CPPLitePanel.jLabel1.text")); // NOI18N
cclsPath.setText(org.openide.util.NbBundle.getMessage(CPPLitePanel.class, "CPPLitePanel.cclsPath.text")); // NOI18N
org.openide.awt.Mnemonics.setLocalizedText(jButton1, org.openide.util.NbBundle.getMessage(CPPLitePanel.class, "CPPLitePanel.jButton1.text")); // NOI18N
org.openide.awt.Mnemonics.setLocalizedText(cclsBrowse, org.openide.util.NbBundle.getMessage(CPPLitePanel.class, "CPPLitePanel.cclsBrowse.text")); // NOI18N
cclsBrowse.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
cclsBrowseActionPerformed(evt);
}
});
org.openide.awt.Mnemonics.setLocalizedText(jLabel2, org.openide.util.NbBundle.getMessage(CPPLitePanel.class, "CPPLitePanel.jLabel2.text")); // NOI18N
clangdPath.setText(org.openide.util.NbBundle.getMessage(CPPLitePanel.class, "CPPLitePanel.clangdPath.text")); // NOI18N
org.openide.awt.Mnemonics.setLocalizedText(clangdBrowse, org.openide.util.NbBundle.getMessage(CPPLitePanel.class, "CPPLitePanel.clangdBrowse.text")); // NOI18N
clangdBrowse.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
clangdBrowseActionPerformed(evt);
}
});
org.openide.awt.Mnemonics.setLocalizedText(jLabel3, org.openide.util.NbBundle.getMessage(CPPLitePanel.class, "CPPLitePanel.jLabel3.text")); // NOI18N
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addComponent(jLabel1)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(cclsPath)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jButton1))
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jLabel1)
.addComponent(jLabel2))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(cclsPath)
.addComponent(clangdPath))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(clangdBrowse)
.addComponent(cclsBrowse)))
.addGroup(layout.createSequentialGroup()
.addComponent(jLabel3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(0, 0, Short.MAX_VALUE)))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(jLabel1)
.addComponent(cclsPath, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(jButton1))
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addComponent(jLabel3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(18, 18, 18)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(jLabel1)
.addComponent(cclsPath, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(cclsBrowse))