Skip to content

Commit

Permalink
add Language Server interface for VSCode
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeloffner committed Dec 19, 2024
1 parent ff3bf4e commit 8f59758
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 2 deletions.
3 changes: 3 additions & 0 deletions core/src/main/java/lucee/runtime/config/ConfigWebFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
import lucee.runtime.listener.MixedAppListener;
import lucee.runtime.listener.ModernAppListener;
import lucee.runtime.listener.SerializationSettings;
import lucee.runtime.lsp.LSPEndpointFactory;
import lucee.runtime.monitor.ActionMonitor;
import lucee.runtime.monitor.ActionMonitorCollector;
import lucee.runtime.monitor.ActionMonitorFatory;
Expand Down Expand Up @@ -689,6 +690,8 @@ synchronized static void load(ConfigServerImpl cs, ConfigImpl config, ConfigWebI

_loadStartupHook(cs, config, root, log);
if (LOG) LogUtil.logGlobal(ThreadLocalPageContext.getConfig(cs == null ? config : cs), Log.LEVEL_DEBUG, ConfigWebFactory.class.getName(), "loaded startup hook");

if (config instanceof ConfigServer) LSPEndpointFactory.init(config, false);
}

config.setLoadTime(System.currentTimeMillis());
Expand Down
244 changes: 244 additions & 0 deletions core/src/main/java/lucee/runtime/lsp/LSPEndpointFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package lucee.runtime.lsp;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.servlet.http.Cookie;

import lucee.print;
import lucee.commons.io.SystemUtil;
import lucee.commons.io.log.Log;
import lucee.loader.engine.CFMLEngine;
import lucee.loader.engine.CFMLEngineFactory;
import lucee.loader.util.Util;
import lucee.runtime.Component;
import lucee.runtime.PageContext;
import lucee.runtime.config.Config;
import lucee.runtime.config.ConfigServer;
import lucee.runtime.config.ConfigWeb;
import lucee.runtime.op.Caster;
import lucee.runtime.thread.ThreadUtil;

public class LSPEndpointFactory {
private static final int DEFAULT_LSP_PORT = 2089; // Common LSP port
private static final String DEFAULT_COMPONENT = "org.lucee.cfml.lsp.LSPEndpoint";
private static final long TIMEOUT = 3000;
private static final String DEFAULT_LOG = "debug";
private ServerSocket serverSocket;
private ExecutorService executor;
private volatile boolean running = true;
private CFMLEngine engine;
private int port;
private String cfcPath;
private Log log;
private static LSPEndpointFactory instance;

private LSPEndpointFactory(Config config) {
// setup config and utils
engine = CFMLEngineFactory.getInstance();
log = getLog(config);
port = engine.getCastUtil().toIntValue(SystemUtil.getSystemPropOrEnvVar("lucee.lsp.port", null), DEFAULT_LSP_PORT);
cfcPath = engine.getCastUtil().toString(SystemUtil.getSystemPropOrEnvVar("lucee.lsp.component", null), DEFAULT_COMPONENT);
if (Util.isEmpty(cfcPath, true)) cfcPath = DEFAULT_COMPONENT;

log.debug("lsp", "LSP server port: " + port);
log.debug("lsp", "LSP server component endpoint: " + cfcPath);
}

public static LSPEndpointFactory init(Config config, boolean forceRestart) throws IOException {
if (Caster.toBooleanValue(SystemUtil.getSystemPropOrEnvVar("lucee.lsp.enabled", null), false)) {
print.e("---- LSPEndpointFactory ----");
synchronized (SystemUtil.createToken("LSPEndpointFactory", "init")) {
if (forceRestart) {
print.e("- restart ----");
if (instance != null) {
instance.stop();
}
instance = new LSPEndpointFactory(config).start();
}
else {
if (instance == null) {
print.e("- start ----");
instance = new LSPEndpointFactory(config).start();
}
}
}
print.e("- init");
}
return instance;
}

private LSPEndpointFactory start() throws IOException {
log.debug("lsp", "starting LSP server");
try {
serverSocket = new ServerSocket(port);
}
catch (IOException e) {
error("lsp", e);
throw e;
}
executor = Executors.newCachedThreadPool();

// Start listening thread
Thread listenerThread = new Thread(() -> {
while (running) {
try {
Socket clientSocket = serverSocket.accept();
executor.submit(() -> handleClient(clientSocket));
}
catch (IOException e) {
if (running) {
error("lsp", e);
}
}
}
}, "LSP-Listener");

listenerThread.setDaemon(true);
listenerThread.start();

log.debug("lsp", "LSP server started");
return this;
}

private void stop() throws IOException {
running = false;
if (executor != null) {
executor.shutdown();
}
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
}

public static LSPEndpointFactory getInstance(Config config) {
return instance;
}

private void handleClient(Socket clientSocket) {

log.debug("lsp", "LSP server handle client");
try {
// Get input/output streams
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
OutputStream out = clientSocket.getOutputStream();
StringBuilder buffer = new StringBuilder();

while (!clientSocket.isClosed()) {
// Read incoming data into buffer
char[] cbuf = new char[1024];
int len;
while ((len = reader.read(cbuf)) != -1) {
buffer.append(cbuf, 0, len);

// Process complete messages in the buffer
while (true) {
// Look for Content-Length header
String content = buffer.toString();
int headerIndex = content.indexOf("Content-Length: ");
if (headerIndex == -1) break;

// Parse content length
int lengthStart = headerIndex + 16;
int lengthEnd = content.indexOf("\r\n", lengthStart);
if (lengthEnd == -1) break;

int contentLength = Integer.parseInt(content.substring(lengthStart, lengthEnd));

// Find start of JSON content
int contentStart = content.indexOf("\r\n\r\n", lengthEnd);
if (contentStart == -1) break;
contentStart += 4;

// Check if we have the complete message
if (buffer.length() < contentStart + contentLength) break;

// Extract the JSON message
String jsonMessage = content.substring(contentStart, contentStart + contentLength);

// Here you would call your component to handle the message
String response = processMessage(jsonMessage);

// Send response using LSP format
if (response != null) {
String header = "Content-Length: " + response.length() + "\r\n\r\n";
out.write(header.getBytes());
out.write(response.getBytes());
out.flush();
}

// Remove processed message from buffer
buffer.delete(0, contentStart + contentLength);
}
}
}
}
catch (Exception e) {
error("lsp", e);
}
finally {
engine.getIOUtil().closeSilent(clientSocket);
}
}

private String processMessage(String jsonMessage) {
try {
log.info("lsp", "Received message: " + jsonMessage);
PageContext pc = createPageContext((ConfigWeb) engine.getThreadConfig());
Component cfc = engine.getCreationUtil().createComponentFromName(pc, cfcPath);

String response = engine.getCastUtil().toString(cfc.call(pc, "execute", new Object[] { jsonMessage }));
log.info("lsp", "response from component [" + cfcPath + "]: " + response);

return response;
}
catch (Exception e) {
error("lsp", e);
return null;
}
}

private void error(String type, Exception e) {
// TODO remove the print out
System.err.println(type);
e.printStackTrace();
log.error(type, e);
}

public static Log getLog(Config config) {
if (config == null) config = CFMLEngineFactory.getInstance().getThreadConfig();
if (config instanceof ConfigServer) {
// we only log to config Server if there is no web context
Config cw = CFMLEngineFactory.getInstance().getThreadConfig();
if (cw != null) config = cw;
}
if (config == null) return null;
try {
Log log = config.getLog("lsp");
if (log == null) log = config.getLog(DEFAULT_LOG);
if (log != null) return log;
}
catch (Exception e) {
Log log = config.getLog(DEFAULT_LOG);
log.error("lsp", e);
return log;
}
return null;
}

public static PageContext createPageContext(final ConfigWeb cw) {
return ThreadUtil.createPageContext(cw, new ByteArrayOutputStream(), "", "/", "", new Cookie[0], null, null, null, null, true, TIMEOUT);
}

public static void releasePageContext(PageContext pc) {
CFMLEngineFactory.getInstance().releasePageContext(pc, true);
}

}
2 changes: 1 addition & 1 deletion loader/build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<project default="core" basedir="." name="Lucee"
xmlns:resolver="antlib:org.apache.maven.resolver.ant">

<property name="version" value="6.1.2.12-SNAPSHOT"/>
<property name="version" value="6.1.2.13-SNAPSHOT"/>

<taskdef uri="antlib:org.apache.maven.resolver.ant" resource="org/apache/maven/resolver/ant/antlib.xml">
<classpath>
Expand Down
2 changes: 1 addition & 1 deletion loader/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<groupId>org.lucee</groupId>
<artifactId>lucee</artifactId>
<version>6.1.2.12-SNAPSHOT</version>
<version>6.1.2.13-SNAPSHOT</version>
<packaging>jar</packaging>

<name>Lucee Loader Build</name>
Expand Down

0 comments on commit 8f59758

Please sign in to comment.