Skip to content

Commit fc5c27a

Browse files
authored
Client Refactoring (#40)
1 parent 3fc5e96 commit fc5c27a

27 files changed

+448
-406
lines changed

client/src/extension.ts

Lines changed: 25 additions & 343 deletions
Original file line numberDiff line numberDiff line change
@@ -1,373 +1,55 @@
11
import * as vscode from "vscode";
2-
import * as path from "path";
3-
import * as net from "net";
4-
import * as child_process from "child_process";
5-
import { LanguageClient, LanguageClientOptions, ServerOptions, State } from "vscode-languageclient/node";
6-
import { LiquidJavaLogger, createLogger } from "./logging";
7-
import { applyItalicOverlay } from "./decorators";
8-
import { connectToPort, findJavaExecutable, getAvailablePort, killProcess } from "./utils";
9-
import { SERVER_JAR, DEBUG_MODE, DEBUG_PORT } from "./constants";
10-
import { LiquidJavaWebviewProvider } from "./webview/provider";
11-
import { LJDiagnostic } from "./types";
12-
import { createMermaidDiagram } from "./webview/fsm";
13-
import { StateMachine } from "./types/fsm";
14-
15-
let serverProcess: child_process.ChildProcess;
16-
let client: LanguageClient;
17-
let socket: net.Socket;
18-
let outputChannel: vscode.OutputChannel;
19-
let logger: LiquidJavaLogger;
20-
let statusBarItem: vscode.StatusBarItem;
21-
let currentDiagnostics: LJDiagnostic[];
22-
let webviewProvider: LiquidJavaWebviewProvider;
23-
let currentFile: string | undefined;
24-
let currentStateMachine: StateMachine | undefined;
2+
import { registerLogger } from "./services/logger";
3+
import { applyItalicOverlay } from "./services/decorators";
4+
import { findJavaExecutable } from "./utils/utils";
5+
import { extension } from "./state";
6+
import { registerCommands } from "./services/commands";
7+
import { registerStatusBar, updateStatusBar } from "./services/status-bar";
8+
import { registerWebview } from "./services/webview";
9+
import { registerHover } from "./services/hover";
10+
import { registerEvents } from "./services/events";
11+
import { runLanguageServer } from "./lsp/server";
12+
import { runClient, stopClient } from "./lsp/client";
2513

2614
/**
2715
* Activates the LiquidJava extension
2816
* @param context The extension context
2917
*/
3018
export async function activate(context: vscode.ExtensionContext) {
31-
initLogging(context);
32-
initStatusBar(context);
33-
initCommandPalette(context);
34-
initWebview(context);
35-
initFileEvents(context);
36-
initHover();
37-
38-
logger.client.info("Activating LiquidJava extension...");
19+
registerLogger(context);
20+
registerStatusBar(context);
21+
registerCommands(context);
22+
registerWebview(context);
23+
registerEvents(context);
24+
registerHover();
25+
26+
extension.logger.client.info("Activating LiquidJava extension...");
3927

4028
await applyItalicOverlay();
4129

4230
// find java executable path
4331
const javaExecutablePath = findJavaExecutable();
4432
if (!javaExecutablePath) {
4533
vscode.window.showErrorMessage("LiquidJava - Java Runtime Not Found in JAVA_HOME or PATH");
46-
logger.client.error("Java Runtime not found in JAVA_HOME or PATH - Not activating extension");
34+
extension.logger.client.error("Java Runtime not found in JAVA_HOME or PATH - Not activating extension");
4735
updateStatusBar("stopped");
4836
return;
4937
}
50-
logger.client.info("Using Java at: " + javaExecutablePath);
38+
extension.logger.client.info("Using Java at: " + javaExecutablePath);
5139

5240
// start server
53-
logger.client.info("Starting LiquidJava language server...");
41+
extension.logger.client.info("Starting LiquidJava language server...");
5442
const port = await runLanguageServer(context, javaExecutablePath);
5543

5644
// start client
57-
logger.client.info("Starting LiquidJava client...");
45+
extension.logger.client.info("Starting LiquidJava client...");
5846
await runClient(context, port);
5947
}
6048

6149
/**
6250
* Deactivates the LiquidJava extension
6351
*/
6452
export async function deactivate() {
65-
logger?.client.info("Deactivating LiquidJava extension...");
66-
await stopExtension("Extension was deactivated");
67-
}
68-
69-
/**
70-
* Initializes logging for the extension with an output channel
71-
* @param context The extension context
72-
*/
73-
function initLogging(context: vscode.ExtensionContext) {
74-
outputChannel = vscode.window.createOutputChannel("LiquidJava");
75-
logger = createLogger(outputChannel);
76-
context.subscriptions.push(outputChannel);
77-
context.subscriptions.push(logger);
78-
context.subscriptions.push(vscode.commands.registerCommand("liquidjava.showLogs", () => outputChannel.show(true)));
79-
}
80-
81-
/**
82-
* Initializes the status bar for the extension
83-
* @param context The extension context
84-
*/
85-
function initStatusBar(context: vscode.ExtensionContext) {
86-
statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
87-
statusBarItem.tooltip = "LiquidJava Commands";
88-
statusBarItem.command = "liquidjava.showCommands";
89-
updateStatusBar("loading")
90-
statusBarItem.show();
91-
context.subscriptions.push(statusBarItem);
92-
}
93-
94-
/**
95-
* Initializes the command palette for the extension
96-
* @param context The extension context
97-
*/
98-
function initCommandPalette(context: vscode.ExtensionContext) {
99-
context.subscriptions.push(
100-
vscode.commands.registerCommand("liquidjava.showCommands", async () => {
101-
const commands = [
102-
{ label: "$(output) Show Logs", command: "liquidjava.showLogs" },
103-
{ label: "$(window) Show View", command: "liquidjava.showView" },
104-
// TODO: add more commands here, e.g., start, stop, restart, verify, etc.
105-
];
106-
const placeHolder = "Select a LiquidJava Command";
107-
const selected = await vscode.window.showQuickPick(commands, { placeHolder });
108-
if (selected) vscode.commands.executeCommand(selected.command);
109-
})
110-
);
111-
}
112-
113-
/**
114-
* Initializes the webview panel for the extension
115-
* @param context The extension context
116-
*/
117-
function initWebview(context: vscode.ExtensionContext) {
118-
webviewProvider = new LiquidJavaWebviewProvider(context.extensionUri);
119-
120-
// webview provider
121-
context.subscriptions.push(
122-
vscode.window.registerWebviewViewProvider(LiquidJavaWebviewProvider.viewType, webviewProvider)
123-
);
124-
// show view command
125-
context.subscriptions.push(
126-
vscode.commands.registerCommand("liquidjava.showView", () => {
127-
vscode.commands.executeCommand("liquidJavaView.focus");
128-
})
129-
);
130-
// listen for messages from the webview
131-
context.subscriptions.push(
132-
webviewProvider.onDidReceiveMessage(message => {
133-
console.log("received message", message);
134-
if (message.type === "ready") {
135-
webviewProvider.sendMessage({ type: "file", file: currentFile });
136-
webviewProvider.sendMessage({ type: "diagnostics", diagnostics: currentDiagnostics });
137-
if (currentStateMachine) webviewProvider.sendMessage({ type: "fsm", sm: currentStateMachine });
138-
}
139-
})
140-
);
141-
}
142-
143-
/**
144-
* Initializes hover provider for LiquidJava diagnostics
145-
*/
146-
function initHover() {
147-
vscode.languages.registerHoverProvider('java', {
148-
provideHover(document, position) {
149-
// if webview is visible, do not show hover
150-
if (webviewProvider?.isVisible()) return null;
151-
152-
// get lj diagnostic at the current position
153-
const diagnostics = vscode.languages.getDiagnostics(document.uri);
154-
const diagnostic = diagnostics.find(d => d.range.contains(position) && d.source === 'liquidjava');
155-
if (!diagnostic) return null;
156-
157-
// create hover content with link to open webview
158-
const hoverContent = new vscode.MarkdownString();
159-
hoverContent.isTrusted = true;
160-
hoverContent.appendMarkdown(`\n\n[Open LiquidJava view](command:liquidjava.showView) for more details.`);
161-
return new vscode.Hover(hoverContent);
162-
}
163-
});
164-
}
165-
166-
167-
/**
168-
* Initializes file system event listeners
169-
* @param context The extension context
170-
*/
171-
function initFileEvents(context: vscode.ExtensionContext) {
172-
// listen for active text editor changes
173-
context.subscriptions.push(
174-
vscode.window.onDidChangeActiveTextEditor(editor => {
175-
if (!editor || editor.document.languageId !== "java") return;
176-
handleActiveFileChange(editor);
177-
178-
}),
179-
vscode.workspace.onDidSaveTextDocument(document => {
180-
if (document.uri.scheme !== 'file' || document.languageId !== "java") return;
181-
requestStateMachine(document)
182-
})
183-
);
184-
}
185-
186-
/**
187-
* Requests the state machine for the given document from the language server
188-
* @param document The text document
189-
*/
190-
async function requestStateMachine(document: vscode.TextDocument) {
191-
const sm: StateMachine = await client?.sendRequest("liquidjava/fsm", { uri: document.uri.toString() });
192-
webviewProvider?.sendMessage({ type: "fsm", sm });
193-
currentStateMachine = sm;
53+
extension.logger?.client.info("Deactivating LiquidJava extension...");
54+
await stopClient("Extension was deactivated");
19455
}
195-
196-
/**
197-
* Updates the status bar with the current state
198-
* @param state The current state ("loading", "stopped", "passed", "failed")
199-
*/
200-
function updateStatusBar(state: "loading" | "stopped" | "passed" | "failed") {
201-
const icons = {
202-
loading: "$(sync~spin)",
203-
stopped: "$(circle-slash)",
204-
passed: "$(check)",
205-
failed: "$(x)",
206-
};
207-
const color = state === "stopped" ? "errorForeground" : "statusBar.foreground";
208-
statusBarItem.color = new vscode.ThemeColor(color);
209-
statusBarItem.text = icons[state] + " LiquidJava";
210-
}
211-
212-
/**
213-
* Runs the LiquidJava language server
214-
* @param context The extension context
215-
* @param javaExecutablePath The path to the Java executable
216-
* @returns A promise to the port number the server is running on
217-
*/
218-
async function runLanguageServer(context: vscode.ExtensionContext, javaExecutablePath: string): Promise<number> {
219-
const port = DEBUG_MODE ? DEBUG_PORT : await getAvailablePort();
220-
if (DEBUG_MODE) {
221-
logger.client.info("DEBUG MODE: Using fixed port " + port);
222-
return port;
223-
}
224-
logger.client.info("Running language server on port " + port);
225-
226-
const jarPath = path.resolve(context.extensionPath, "dist", "server", SERVER_JAR);
227-
const args = ["-jar", jarPath, port.toString()];
228-
const options = {
229-
cwd: vscode.workspace.workspaceFolders[0].uri.fsPath, // root path
230-
};
231-
logger.client.info("Creating language server process...");
232-
serverProcess = child_process.spawn(javaExecutablePath, args, options);
233-
234-
// listen to process events
235-
serverProcess.stdout.on("data", (data) => {
236-
const message = data.toString().trim();
237-
logger.server.info(message);
238-
});
239-
serverProcess.stderr.on("data", (data) => {
240-
logger.server.error(data.toString().trim())
241-
});
242-
serverProcess.on("error", (err) => {
243-
logger.server.error(`Failed to start: ${err}`)
244-
});
245-
serverProcess.on("close", (code) => {
246-
logger.server.info(`Process exited with code ${code}`);
247-
client?.stop();
248-
});
249-
return port;
250-
}
251-
252-
/**
253-
* Starts the client and connects it to the language server
254-
* @param context The extension context
255-
* @param port The port number the server is running on
256-
*/
257-
async function runClient(context: vscode.ExtensionContext, port: number) {
258-
const serverOptions: ServerOptions = () => {
259-
return new Promise(async (resolve, reject) => {
260-
try {
261-
socket = await connectToPort(port);
262-
resolve({
263-
writer: socket,
264-
reader: socket,
265-
});
266-
} catch (error) {
267-
await stopExtension("Failed to connect to server");
268-
reject(error);
269-
}
270-
});
271-
};
272-
const clientOptions: LanguageClientOptions = {
273-
documentSelector: [{ language: "java" }],
274-
};
275-
client = new LanguageClient("liquidJavaServer", "LiquidJava Server", serverOptions, clientOptions);
276-
client.onDidChangeState((e) => {
277-
if (e.newState === State.Stopped) {
278-
stopExtension("Extension stopped");
279-
}
280-
});
281-
282-
context.subscriptions.push(client); // client teardown
283-
context.subscriptions.push({
284-
dispose: () => stopExtension("Extension was disposed"), // server teardown
285-
});
286-
287-
try {
288-
await client.start();
289-
logger.client.info("Extension is ready");
290-
291-
client.onNotification("liquidjava/diagnostics", (diagnostics: LJDiagnostic[]) => {
292-
handleLJDiagnostics(diagnostics);
293-
});
294-
295-
const editor = vscode.window.activeTextEditor;
296-
if (editor && editor.document.languageId === "java") {
297-
handleActiveFileChange(editor);
298-
}
299-
} catch (e) {
300-
vscode.window.showErrorMessage("LiquidJava failed to initialize: " + e.toString());
301-
logger.client.error("Failed to initialize: " + e.toString());
302-
await stopExtension("Failed to initialize");
303-
}
304-
305-
// update status bar on file save
306-
context.subscriptions.push(
307-
vscode.workspace.onDidSaveTextDocument(() => {
308-
if (client) {
309-
updateStatusBar("loading");
310-
}
311-
})
312-
);
313-
}
314-
315-
/**
316-
* Stops the LiquidJava extension
317-
* @param reason The reason for stopping the extension
318-
*/
319-
async function stopExtension(reason: string) {
320-
if (!client && !serverProcess && !socket) {
321-
logger.client.info("Extension already stopped");
322-
return;
323-
}
324-
logger.client.info("Stopping LiquidJava extension: " + reason);
325-
updateStatusBar("stopped");
326-
327-
// stop client
328-
try {
329-
await client?.stop();
330-
} catch (e) {
331-
logger.client.error("Error stopping client: " + e);
332-
} finally {
333-
client = undefined;
334-
}
335-
336-
// close socket
337-
try {
338-
socket?.destroy();
339-
} catch (e) {
340-
logger.client.error("Error closing socket: " + e);
341-
} finally {
342-
socket = undefined;
343-
}
344-
345-
// kill server process
346-
await killProcess(serverProcess);
347-
serverProcess = undefined;
348-
}
349-
350-
/**
351-
* Handles LiquidJava diagnostics received from the language server
352-
* @param diagnostics The array of diagnostics received
353-
*/
354-
function handleLJDiagnostics(diagnostics: LJDiagnostic[]) {
355-
const containsError = diagnostics.some(d => d.category === "error");
356-
if (containsError) {
357-
updateStatusBar("failed");
358-
} else {
359-
updateStatusBar("passed");
360-
}
361-
webviewProvider?.sendMessage({ type: "diagnostics", diagnostics });
362-
currentDiagnostics = diagnostics;
363-
}
364-
365-
/**
366-
* Handles active file change events
367-
* @param editor The active text editor
368-
*/
369-
function handleActiveFileChange(editor: vscode.TextEditor) {
370-
currentFile = editor.document.uri.fsPath;
371-
webviewProvider?.sendMessage({ type: "file", file: currentFile });
372-
requestStateMachine(editor.document);
373-
}

0 commit comments

Comments
 (0)