| /******************************************************************************* |
| * Copyright (c) 2015 QNX Software Systems and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| *******************************************************************************/ |
| package org.eclipse.cdt.internal.qt.core; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.net.URL; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| |
| import javax.script.Bindings; |
| import javax.script.Invocable; |
| import javax.script.ScriptContext; |
| import javax.script.ScriptEngine; |
| import javax.script.ScriptEngineManager; |
| import javax.script.ScriptException; |
| |
| import org.eclipse.cdt.qt.core.IQMLAnalyzer; |
| import org.eclipse.cdt.qt.core.QMLTernCompletion; |
| import org.eclipse.cdt.qt.core.qmljs.IQmlASTNode; |
| |
| @SuppressWarnings("nls") |
| public class QMLAnalyzer implements IQMLAnalyzer { |
| |
| private QMLModuleResolver moduleResolver; |
| private ScriptEngine engine; |
| private Boolean supported; |
| private Invocable invoke; |
| private Object tern; |
| |
| @Override |
| public void load() throws ScriptException, IOException, NoSuchMethodException { |
| moduleResolver = new QMLModuleResolver(this); |
| engine = new ScriptEngineManager().getEngineByName("nashorn"); |
| if (engine == null) { |
| synchronized (this) { |
| supported = false; |
| notifyAll(); |
| } |
| throw new ScriptException( |
| "Nashorn script engine is not available in Java 15 and above. The QML Analyzer is not supported."); |
| } |
| invoke = (Invocable) engine; |
| |
| loadDep("/tern-qml/node_modules/acorn/dist/acorn.js"); |
| loadDep("/tern-qml/node_modules/acorn/dist/acorn_loose.js"); |
| loadDep("/tern-qml/node_modules/acorn/dist/walk.js"); |
| |
| loadDep("/tern-qml/node_modules/tern/lib/signal.js"); |
| loadDep("/tern-qml/node_modules/tern/lib/tern.js"); |
| loadDep("/tern-qml/node_modules/tern/lib/def.js"); |
| loadDep("/tern-qml/node_modules/tern/lib/comment.js"); |
| loadDep("/tern-qml/node_modules/tern/lib/infer.js"); |
| |
| load("/acorn-qml/inject.js"); |
| load("/acorn-qml/index.js"); |
| load("/acorn-qml/loose/inject.js"); |
| load("/acorn-qml/loose/index.js"); |
| load("/acorn-qml/walk/index.js"); |
| |
| load("/tern-qml/qml.js"); |
| load("/tern-qml/qml-nsh.js"); |
| |
| Bindings options = (Bindings) engine.eval("new Object()"); |
| options.put("ecmaVersion", 5); |
| |
| Bindings plugins = (Bindings) engine.eval("new Object()"); |
| plugins.put("qml", true); |
| options.put("plugins", plugins); |
| |
| Bindings defs = (Bindings) engine.eval("new Array()"); |
| load("/tern-qml/ecma5-defs.js"); |
| invoke.invokeMethod(defs, "push", engine.get("ecma5defs")); |
| options.put("defs", defs); |
| |
| ResolveDirectory resolveDirectory = (file, pathString) -> { |
| String filename = (String) file.get("name"); |
| String fileDirectory = new File(filename).getParent(); |
| if (fileDirectory == null) { |
| fileDirectory = ""; |
| } |
| if (pathString == null) { |
| return fixPathString(fileDirectory); |
| } |
| Path fileDirectoryPath = Paths.get(fileDirectory); |
| Path path = Paths.get(pathString); |
| if (!path.isAbsolute()) { |
| path = fileDirectoryPath.toAbsolutePath().resolve(path); |
| } |
| return fixPathString(path.normalize().toString()); |
| }; |
| options.put("resolveDirectory", invoke.invokeFunction("resolveDirectory", resolveDirectory)); |
| options.put("resolveModule", invoke.invokeFunction("resolveModule", moduleResolver)); |
| |
| synchronized (this) { |
| tern = invoke.invokeFunction("newTernServer", options); |
| supported = tern != null; |
| notifyAll(); |
| } |
| } |
| |
| @FunctionalInterface |
| public interface ResolveDirectory { |
| public String resolveDirectory(Bindings file, String path); |
| } |
| |
| private Object load(String file) throws ScriptException, IOException { |
| URL scriptURL = Activator.getDefault().getBundle().getEntry(file); |
| if (scriptURL == null) { |
| throw new FileNotFoundException(file); |
| } |
| engine.getContext().setAttribute(ScriptEngine.FILENAME, file, ScriptContext.ENGINE_SCOPE); |
| return engine.eval(new BufferedReader(new InputStreamReader(scriptURL.openStream(), StandardCharsets.UTF_8))); |
| } |
| |
| private Object loadDep(String file) throws ScriptException, IOException { |
| try { |
| return load(file); |
| } catch (FileNotFoundException e) { |
| return load(file.replace("/tern-qml/node_modules/", "/tern-qml/dist/")); |
| } |
| } |
| |
| @Override |
| public boolean isSupported() { |
| synchronized (this) { |
| while (supported == null) { |
| try { |
| wait(); |
| } catch (InterruptedException e) { |
| Activator.log(e); |
| return false; |
| } |
| } |
| return supported; |
| } |
| } |
| |
| private void waitUntilLoaded() throws ScriptException { |
| if (!isSupported()) { |
| throw new ScriptException( |
| "Nashorn script engine is not available in Java 15 and above. The QML Analyzer is not supported."); |
| } |
| } |
| |
| @FunctionalInterface |
| public interface RequestCallback { |
| void callback(Object err, Object data); |
| } |
| |
| private String fixPathString(String fileName) { |
| fileName = fileName.replaceAll("\\\\", "/"); |
| if (fileName.startsWith("/")) { |
| fileName = fileName.substring(1); |
| } |
| return fileName; |
| } |
| |
| @Override |
| public void addFile(String fileName, String code) throws NoSuchMethodException, ScriptException { |
| waitUntilLoaded(); |
| invoke.invokeMethod(tern, "addFile", fixPathString(fileName), code); |
| } |
| |
| @Override |
| public void deleteFile(String fileName) throws NoSuchMethodException, ScriptException { |
| waitUntilLoaded(); |
| invoke.invokeMethod(tern, "delFile", fixPathString(fileName)); |
| } |
| |
| private static class ASTCallback implements RequestCallback { |
| private IQmlASTNode ast; |
| |
| @Override |
| public void callback(Object err, Object data) { |
| if (err != null) { |
| throw new RuntimeException(err.toString()); |
| } else { |
| try { |
| ast = QmlASTNodeHandler.createQmlASTProxy((Bindings) ((Bindings) data).get("ast")); |
| } catch (Throwable e) { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| |
| public IQmlASTNode getAST() { |
| return ast; |
| } |
| } |
| |
| @Override |
| public IQmlASTNode parseFile(String fileName, String text) throws NoSuchMethodException, ScriptException { |
| waitUntilLoaded(); |
| fileName = fixPathString(fileName); |
| |
| Bindings query = engine.createBindings(); |
| query.put("type", "parseFile"); |
| query.put("file", fileName); |
| Bindings request = engine.createBindings(); |
| request.put("query", query); |
| |
| if (text != null) { |
| Bindings file = engine.createBindings(); |
| file.put("type", "full"); |
| file.put("name", fileName); |
| file.put("text", text); |
| Bindings files = (Bindings) engine.eval("new Array()"); |
| invoke.invokeMethod(files, "push", file); |
| request.put("files", files); |
| } |
| |
| ASTCallback callback = new ASTCallback(); |
| invoke.invokeMethod(tern, "request", request, invoke.invokeFunction("requestCallback", callback)); |
| return callback.getAST(); |
| } |
| |
| @Override |
| public IQmlASTNode parseString(String text) throws NoSuchMethodException, ScriptException { |
| return parseString(text, "qml", false, false); |
| } |
| |
| @Override |
| public IQmlASTNode parseString(String text, String mode, boolean locations, boolean ranges) |
| throws NoSuchMethodException, ScriptException { |
| waitUntilLoaded(); |
| Bindings options = engine.createBindings(); |
| options.put("mode", mode); |
| options.put("locations", locations); |
| options.put("ranges", ranges); |
| |
| ASTCallback callback = new ASTCallback(); |
| invoke.invokeMethod(tern, "parseString", text, options, invoke.invokeFunction("requestCallback", callback)); |
| return callback.getAST(); |
| } |
| |
| protected <T> T[] toJavaArray(Bindings binding, Class<T[]> clazz) throws NoSuchMethodException, ScriptException { |
| return clazz.cast(invoke.invokeMethod(engine.get("Java"), "to", binding, |
| clazz.getCanonicalName() + (clazz.isArray() ? "" : "[]"))); |
| } |
| |
| @Override |
| public Collection<QMLTernCompletion> getCompletions(String fileName, String text, int pos) |
| throws NoSuchMethodException, ScriptException { |
| return getCompletions(fileName, text, pos, true); |
| } |
| |
| @Override |
| public Collection<QMLTernCompletion> getCompletions(String fileName, String text, int pos, boolean includeKeywords) |
| throws NoSuchMethodException, ScriptException { |
| waitUntilLoaded(); |
| fileName = fixPathString(fileName); |
| |
| Bindings query = engine.createBindings(); |
| query.put("type", "completions"); |
| query.put("lineCharPositions", true); |
| query.put("file", fileName); |
| query.put("end", pos); |
| query.put("types", true); |
| query.put("docs", false); |
| query.put("urls", false); |
| query.put("origins", true); |
| query.put("filter", true); |
| query.put("caseInsensitive", true); |
| query.put("guess", false); |
| query.put("sort", true); |
| query.put("expandWordForward", false); |
| query.put("includeKeywords", includeKeywords); |
| |
| Bindings request = engine.createBindings(); |
| request.put("query", query); |
| if (text != null) { |
| Bindings file = engine.createBindings(); |
| file.put("type", "full"); |
| file.put("name", fileName); |
| file.put("text", text); |
| Bindings files = (Bindings) engine.eval("new Array()"); |
| invoke.invokeMethod(files, "push", file); |
| request.put("files", files); |
| } |
| |
| List<QMLTernCompletion> completions = new ArrayList<>(); |
| |
| RequestCallback callback = (err, data) -> { |
| if (err != null) { |
| throw new RuntimeException(err.toString()); |
| } else { |
| try { |
| Bindings comps = (Bindings) ((Bindings) data).get("completions"); |
| for (Bindings completion : toJavaArray(comps, Bindings[].class)) { |
| completions.add(new QMLTernCompletion((String) completion.get("name"), |
| (String) completion.get("type"), (String) completion.get("origin"))); |
| } |
| } catch (Throwable e) { |
| throw new RuntimeException(e); |
| } |
| } |
| }; |
| |
| invoke.invokeMethod(tern, "request", request, invoke.invokeFunction("requestCallback", callback)); |
| |
| return completions; |
| } |
| |
| @Override |
| public List<Bindings> getDefinition(String identifier, String fileName, String text, int pos) |
| throws NoSuchMethodException, ScriptException { |
| waitUntilLoaded(); |
| fileName = fixPathString(fileName); |
| |
| Bindings query = engine.createBindings(); |
| query.put("type", "definition"); |
| query.put("file", fileName); |
| query.put("end", pos); |
| query.put("types", true); |
| query.put("docs", false); |
| query.put("urls", false); |
| query.put("origins", true); |
| query.put("caseInsensitive", true); |
| query.put("lineCharPositions", true); |
| query.put("expandWordForward", false); |
| query.put("includeKeywords", true); |
| query.put("guess", false); |
| Bindings request = engine.createBindings(); |
| request.put("query", query); |
| if (text != null) { |
| Bindings file = engine.createBindings(); |
| file.put("type", "full"); |
| file.put("name", fileName); |
| file.put("text", text); |
| Bindings files = (Bindings) engine.eval("new Array()"); |
| invoke.invokeMethod(files, "push", file); |
| request.put("files", files); |
| } |
| |
| List<Bindings> definitions = new ArrayList<>(); |
| |
| RequestCallback callback = (err, data) -> { |
| if (err != null) { |
| throw new RuntimeException(err.toString()); |
| } else { |
| definitions.add((Bindings) data); |
| } |
| }; |
| |
| invoke.invokeMethod(tern, "request", request, invoke.invokeFunction("requestCallback", callback)); |
| return definitions; |
| } |
| |
| } |