Skip to content

Commit

Permalink
Adding support for clangd to the lightweight C/C++ support
Browse files Browse the repository at this point in the history
  • Loading branch information
jlahoda authored Aug 10, 2020
1 parent 2a4a18a commit 1cf4221
Show file tree
Hide file tree
Showing 12 changed files with 470 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,14 @@ private synchronized void updateFocused() {
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));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,111 @@
*/
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;

/**
*
* @author lahvac
*/
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());

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,35 @@
*/
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;
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;
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;

/**
*
Expand Down Expand Up @@ -74,32 +78,45 @@ public LanguageServerDescription startServer(Lookup lookup) {
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);
}
Expand All @@ -113,25 +130,60 @@ public void preferenceChange(PreferenceChangeEvent evt) {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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=...
Loading

0 comments on commit 1cf4221

Please sign in to comment.