diff --git a/.gitignore b/.gitignore index fc0d887..700005d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,36 @@ -.settings -/bin -/.project -target \ No newline at end of file +build/ +.out/ +bin/ +.bin/ +target/ + +/gradle.properties +/archive/ +/META-INF + +.classpath +.project +.settings + +.idea/ +*.iml +*.iws +*.ipr +.idea_modules/ +**/out/ + +*.tmp +*.bak +*.swp +*~ + +.gradle + +.DS_Store* +.AppleDouble +.LSOverride + +.directory +.Trash* + +**/adhoctest/ \ No newline at end of file diff --git a/README.md b/README.md index a660c7e..17f6676 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,17 @@ Once https://github.com/mickaelistria/eclipse-bluesky/issues/63 will work in Pho HTML syntax coloration (managed with TextMate) and HTML completion, mark occurrences, etc is not a part of this plugin. I suggest you that you install https://github.com/mickaelistria/eclipse-bluesky which provides those features. + +Development in Eclipse +====================== + +1. Use "Eclipse for Committers" (Photon M6 as of this writing). + +2. In Eclipse, "File" / "Import..." / "Existing Maven Projects". Point at the `lsp4e-freemarker` project root directory, add all the Maven projects it finds. + +3. Now go to "Window" / "Preferences" / "Plug-in Development" / "Target Platform", and Select "lsp4e-freemarker" (this only appears if you have imported the "target-platform" Maven project earlier). + After this, there shouldn't be more errors in the project (no dependency classes that aren't found). + +4. To try the plugin, right click on the `org.eclipse.lsp4j.freemarker` project, then "Run as" / "Eclipse Application". + (TODO: Currently that will fail with `Application "org.eclipse.ui.ide.workbench" could not be found in the registry`. I have worked that around by adding + `` to the target platform, but of course there must be a better way.) diff --git a/org.eclipse.lsp4e.freemarker/META-INF/MANIFEST.MF b/org.eclipse.lsp4e.freemarker/META-INF/MANIFEST.MF index 5ac8e68..948e723 100644 --- a/org.eclipse.lsp4e.freemarker/META-INF/MANIFEST.MF +++ b/org.eclipse.lsp4e.freemarker/META-INF/MANIFEST.MF @@ -14,8 +14,7 @@ Require-Bundle: org.eclipse.lsp4e, org.eclipse.jface, org.eclipse.ui.workbench.texteditor, org.eclipse.ui.editors, - org.eclipse.ui.genericeditor, - org.eclipse.jdt.launching + org.eclipse.ui.genericeditor Bundle-Activator: org.eclipse.lsp4e.freemarker.FreemarkerPlugin Bundle-ActivationPolicy: lazy Eclipse-BundleShape: dir diff --git a/org.eclipse.lsp4e.freemarker/plugin.xml b/org.eclipse.lsp4e.freemarker/plugin.xml index e68868b..4fa382f 100644 --- a/org.eclipse.lsp4e.freemarker/plugin.xml +++ b/org.eclipse.lsp4e.freemarker/plugin.xml @@ -34,7 +34,7 @@ diff --git a/org.eclipse.lsp4e.freemarker/server/freemarker-languageserver-all.jar b/org.eclipse.lsp4e.freemarker/server/freemarker-languageserver-all.jar index 0a7226b..b4be85f 100644 Binary files a/org.eclipse.lsp4e.freemarker/server/freemarker-languageserver-all.jar and b/org.eclipse.lsp4e.freemarker/server/freemarker-languageserver-all.jar differ diff --git a/org.eclipse.lsp4e.freemarker/src/org/eclipse/lsp4e/freemarker/FreemarkerLanguageServer.java b/org.eclipse.lsp4e.freemarker/src/org/eclipse/lsp4e/freemarker/FreemarkerLanguageServer.java deleted file mode 100644 index ae77b33..0000000 --- a/org.eclipse.lsp4e.freemarker/src/org/eclipse/lsp4e/freemarker/FreemarkerLanguageServer.java +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Copyright (c) 2018 Angelo ZERR. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Angelo Zerr - initial API and implementation - */ -package org.eclipse.lsp4e.freemarker; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.reflect.Field; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -import org.eclipse.core.runtime.FileLocator; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Platform; -import org.eclipse.core.runtime.Status; -import org.eclipse.jdt.internal.launching.StandardVMType; -import org.eclipse.jdt.launching.IVMInstall; -import org.eclipse.jdt.launching.JavaRuntime; -import org.eclipse.lsp4e.server.ProcessStreamConnectionProvider; -import org.osgi.framework.Bundle; - -/** - * LSP4e Freemarker Language server. - * - */ -public class FreemarkerLanguageServer extends ProcessStreamConnectionProvider { - - public FreemarkerLanguageServer() { - super(computeCommands(), computeWorkingDir()); - } - - private static String computeWorkingDir() { - return System.getProperty("user.dir"); - } - - private static List computeCommands() { - List commands = new ArrayList<>(); - // Try to use the configured Install JRE. - IVMInstall install = JavaRuntime.getDefaultVMInstall(); - if (install != null) { - File vmInstallLocation = install.getInstallLocation(); - File javaExecutableLocation = StandardVMType.findJavaExecutable(vmInstallLocation); - // ex: C:\Program Files\Java\jre1.8.0_77\bin\javaw.exe - commands.add(javaExecutableLocation.getAbsolutePath()); - } else { - commands.add("java"); - } - commands.add("-jar"); - commands.add(computeFreemarkerLanguageServerJarPath()); - return commands; - } - - private static String computeFreemarkerLanguageServerJarPath() { - Bundle bundle = Platform.getBundle(FreemarkerPlugin.PLUGIN_ID); - URL fileURL = bundle.getEntry("server/freemarker-languageserver-all.jar"); - try { - URL resolvedFileURL = FileLocator.toFileURL(fileURL); - - // We need to use the 3-arg constructor of URI in order to properly escape file - // system chars - URI resolvedURI = new URI(resolvedFileURL.getProtocol(), resolvedFileURL.getPath(), null); - File file = new File(resolvedURI); - if (Platform.OS_WIN32.equals(Platform.getOS())) { - return "\"" + file.getAbsolutePath() + "\""; - } else { - return file.getAbsolutePath(); - } - } catch (URISyntaxException | IOException exception) { - FreemarkerPlugin.log(new Status(IStatus.ERROR, FreemarkerPlugin.PLUGIN_ID, - "Cannot get the FreeMarker LSP Server jar.", exception)); //$NON-NLS-1$ - } - return ""; - } - - class GetErrorThread extends Thread { - - private final Process process; - private String message = null; - - public GetErrorThread(Process process) { - this.process = process; - } - - @Override - public void run() { - try (final BufferedReader b = new BufferedReader(new InputStreamReader(getErrorStream()))) { - String line; - if ((line = b.readLine()) != null) { - message = line; - synchronized (FreemarkerLanguageServer.this) { - FreemarkerLanguageServer.this.notifyAll(); - } - } - } catch (IOException e) { - message = e.getMessage(); - } - } - - public void check() throws IOException { - if (message != null) { - throw new IOException(message); - } - if (!process.isAlive()) { - throw new IOException("Process is not alive"); //$NON-NLS-1$ - } - } - - } - - @Override - public void start() throws IOException { - // Start the process - super.start(); - // Get the process by Java reflection - Process p = getProcess(); - if (p != null) { - // Sart a thread which read error stream to check that the java command is - // working. - GetErrorThread t = new GetErrorThread(p); - try { - t.start(); - // wait a little to execute java command line... - synchronized (FreemarkerLanguageServer.this) { - try { - this.wait(500); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - // check if there is an error or if process is not alived. - try { - t.check(); - } catch (IOException e) { - throw new IOException("Unable to start language server: " + this.toString(), e); //$NON-NLS-1$ - } - } finally { - t.interrupt(); - } - } - } - - private Process getProcess() { - try { - Field f = ProcessStreamConnectionProvider.class.getDeclaredField("process"); - f.setAccessible(true); - Process p = (Process) f.get(this); - return p; - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - @Override - protected ProcessBuilder createProcessBuilder() { - ProcessBuilder builder = super.createProcessBuilder(); - // override redirect to PIPE to read error stream with GetErrorThread - builder.redirectError(ProcessBuilder.Redirect.PIPE); - return builder; - } - - @Override - public String toString() { - return "FreeMarker (" + super.toString() + ")"; - } - -} diff --git a/org.eclipse.lsp4e.freemarker/src/org/eclipse/lsp4e/freemarker/FreemarkerStreamConnectionProvider.java b/org.eclipse.lsp4e.freemarker/src/org/eclipse/lsp4e/freemarker/FreemarkerStreamConnectionProvider.java new file mode 100644 index 0000000..c966379 --- /dev/null +++ b/org.eclipse.lsp4e.freemarker/src/org/eclipse/lsp4e/freemarker/FreemarkerStreamConnectionProvider.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2018 Angelo ZERR, Daniel Dekany. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Angelo Zerr - initial API and implementation + */ +package org.eclipse.lsp4e.freemarker; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.concurrent.Future; + +import org.eclipse.core.runtime.Platform; +import org.osgi.framework.Bundle; + +/** + * Starts the FreeMarker LSP server inside the current JVM, and connects to it. + */ +public class FreemarkerStreamConnectionProvider extends LocalStreamConnectionProvider { + + public FreemarkerStreamConnectionProvider() { + super(FreemarkerPlugin.getDefault().getLog(), FreemarkerPlugin.getPluginId()); + } + + private static final String LANGUAGE_SERVER_JAR_ENTRY_NAME = "server/freemarker-languageserver-all.jar"; //$NON-NLS-1$ + private static final String LANGUAGE_SERVER_LAUNCHER_CLASS_NAME = "freemarker.ext.languageserver.FreemarkerServerLauncher"; //$NON-NLS-1$ + + @Override + protected LocalServer launchServer(InputStream clientToServerStream, OutputStream serverToClientStream) + throws IOException { + URL[] classPath = getFreemarkerLanguageServerClassPath(); + logInfo("Using class path: " + Arrays.toString(classPath)); + + URLClassLoader dynamicJarClassLoader = new URLClassLoader(classPath); + + Method launcherMethod; + try { + Class launcherClass = dynamicJarClassLoader.loadClass(LANGUAGE_SERVER_LAUNCHER_CLASS_NAME); + launcherMethod = launcherClass.getMethod("launch", //$NON-NLS-1$ + new Class[] { InputStream.class, OutputStream.class }); + } catch (Exception e) { + throw new RuntimeException( + "Couldn't get launcher class and method via Java reflection (using class path: " + + Arrays.toString(classPath) + "); see cause exception", e); //$NON-NLS-2$ + } + Future launchedFuture; + try { + launchedFuture = (Future) launcherMethod.invoke(null, clientToServerStream, serverToClientStream); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new RuntimeException("Error when calling launcher method; see cause exception", e); //$NON-NLS-1$ + } + + return new LocalServer(launchedFuture) { + @Override + public void stop() { + super.stop(); + try { + dynamicJarClassLoader.close(); + } catch (IOException e) { + logError("Error when closing the dynamic jar class-loader", e); //$NON-NLS-1$ + } + } + }; + } + + private URL[] getFreemarkerLanguageServerClassPath() { + Bundle bundle = Platform.getBundle(FreemarkerPlugin.PLUGIN_ID); + if (bundle == null) { + throw new RuntimeException("Bundle " + FreemarkerPlugin.PLUGIN_ID + " not found"); //$NON-NLS-1$ + } + + URL languageServerJarURL = bundle.getEntry(LANGUAGE_SERVER_JAR_ENTRY_NAME); + if (languageServerJarURL == null) { + throw new RuntimeException( + "Entity " + LANGUAGE_SERVER_JAR_ENTRY_NAME + " not found in bundle " + FreemarkerPlugin.PLUGIN_ID); //$NON-NLS-1$ + } + + // TODO: Add freemarker.jar from the user project here, if it's found and has + // high enough version, otherwise add freemarker.jar from this plugin. + // (Currently, freemarker.jar is bundled into the language server jar.) + + return new URL[] { languageServerJarURL }; + } + +} diff --git a/org.eclipse.lsp4e.freemarker/src/org/eclipse/lsp4e/freemarker/LocalStreamConnectionProvider.java b/org.eclipse.lsp4e.freemarker/src/org/eclipse/lsp4e/freemarker/LocalStreamConnectionProvider.java new file mode 100644 index 0000000..2628bd9 --- /dev/null +++ b/org.eclipse.lsp4e.freemarker/src/org/eclipse/lsp4e/freemarker/LocalStreamConnectionProvider.java @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2018 Angelo ZERR, Daniel Dekany. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Angelo Zerr - initial API and implementation + */ +package org.eclipse.lsp4e.freemarker; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.Objects; +import java.util.concurrent.Future; + +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.lsp4e.server.StreamConnectionProvider; + +/** + * A {@link StreamConnectionProvider} that connects to a LSP server that runs in + * the client's JVM, not as a separate process. + */ +// This meant to be independent of the Freemareker plugin project, so it shouldn't use local dependencies, like +// FreemarkerPlugin.log(...). +public abstract class LocalStreamConnectionProvider implements StreamConnectionProvider { + + private static final int PIPE_BUFFER_SIZE = 8192; + + // Certainly nobody wants to internationalize error messages like this... + private static final String STOP_ERROR_MESSAGE = "Error while stopping the local LSP service."; //$NON-NLS-1$ + + private PipedOutputStream clientToServerStream; + private PipedInputStream serverToClientStream; + private PipedInputStream clientToServerStreamReverse; + private PipedOutputStream serverToClientStreamReverse; + private LocalServer localServer; + + private final ILog log; + private final String pluginId; + + /** + * @param log The log used by the embedding plug-in. (It's assumed that the + * server is local because it's embedded into an Eclipse + * plug-in.) + * @param pluginId The ID of the embedding Eclipse plug-in. Used for + * {@link IStatus#getPlugin()} for example. + */ + protected LocalStreamConnectionProvider(ILog log, String pluginId) { + this.log = log; + this.pluginId = pluginId; + } + + @Override + public synchronized void start() throws IOException { + clientToServerStream = new PipedOutputStream(); + serverToClientStream = new PipedInputStream(PIPE_BUFFER_SIZE); + clientToServerStreamReverse = new PipedInputStream(clientToServerStream, PIPE_BUFFER_SIZE); + serverToClientStreamReverse = new PipedOutputStream(serverToClientStream); + localServer = launchServer(clientToServerStreamReverse, serverToClientStreamReverse); + } + + protected abstract LocalServer launchServer(InputStream clientToServerStream, OutputStream serverToClientStream) + throws IOException; + + @Override + public InputStream getInputStream() { + return serverToClientStream; + } + + @Override + public OutputStream getOutputStream() { + return clientToServerStream; + } + + @Override + public InputStream getErrorStream() { + return null; + } + + @Override + public synchronized void stop() { + if (localServer == null) { + return; + } + + try { + localServer.stop(); + } catch (Exception e) { + logError(STOP_ERROR_MESSAGE, e); + } + localServer = null; + + try { + clientToServerStream.close(); + } catch (IOException e) { + logError(STOP_ERROR_MESSAGE, e); + } + clientToServerStream = null; + + try { + clientToServerStreamReverse.close(); + } catch (IOException e) { + logError(STOP_ERROR_MESSAGE, e); + } + clientToServerStreamReverse = null; + + try { + serverToClientStreamReverse.close(); + } catch (IOException e) { + logError(STOP_ERROR_MESSAGE, e); + } + serverToClientStreamReverse = null; + + try { + serverToClientStream.close(); + } catch (IOException e) { + logError(STOP_ERROR_MESSAGE, e); + } + serverToClientStream = null; + } + + /** + * See similar {@link LocalStreamConnectionProvider} constructor parameter. + */ + protected ILog getLog() { + return log; + } + + /** + * See similar {@link LocalStreamConnectionProvider} constructor parameter. + */ + protected String getPluginId() { + return pluginId; + } + + /** + * Convenience method to log an error. + */ + protected void logError(String message, Throwable e) { + getLog().log(new Status(IStatus.ERROR, getPluginId(), message, e)); + } + + /** + * Convenience method to log an info message. + */ + protected void logInfo(String message) { + getLog().log(new Status(IStatus.INFO, getPluginId(), message)); + } + + /** + * Represents a locally launched server, that can be stopped. + */ + public static abstract class LocalServer { + + private final Future launcherFuture; + + /** + * @param launcherFuture The future returned by + * {@link org.eclipse.lsp4j.jsonrpc.Launcher#startListening()} + */ + public LocalServer(Future launcherFuture) { + Objects.requireNonNull(launcherFuture, "launcherFuture"); + this.launcherFuture = launcherFuture; + } + + /** + * Override this if you have resource to release. + */ + public void stop() { + // TODO I'm not sure if I can stop the language server like this... will have to + // look into the org.eclipse.lsp4j.jsonrpc.Launcher#startListening() + // implementation, as it has no JavaDoc. + launcherFuture.cancel(true); + } + } + +} diff --git a/pom.xml b/pom.xml index 14d9a4e..7852a09 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,10 @@ https://github.com/angelozerr/lsp4e-freemarker 2018 + + + 1.1.0 + @@ -53,13 +57,13 @@ org.eclipse.tycho tycho-maven-plugin - 1.0.0 + ${tycho.version} true org.eclipse.tycho target-platform-configuration - 1.0.0 + ${tycho.version} true p2 @@ -77,7 +81,7 @@ org.eclipse.tycho tycho-source-plugin - 1.0.0 + ${tycho.version} plugin-source @@ -88,13 +92,13 @@ org.eclipse.tycho tycho-p2-plugin - 1.0.0 + ${tycho.version} attach-p2-metadata diff --git a/target-platform/target-platform.target b/target-platform/target-platform.target index 412209a..102ba3e 100644 --- a/target-platform/target-platform.target +++ b/target-platform/target-platform.target @@ -1,5 +1,5 @@ - +