extension.ts - The CodeChat Visual Studio Code extension¶
This extension creates a webview (see activation/deactivation), then uses CodeChat services to render editor text in that webview.
Remote operation¶
This extension doesn’t fully work when running remotely. Specifically, the web browser in VSCode can’t talk with the CodeChat Server via a websocket, since the server runs on the remote host while the web browser (a WebView) runs locally. While the solutions on that page seem helpful, they don’t support websocket connections (see the portMapping dropdown text in WebViewOptions). The workaround: use an external browser (running on the remote host).
Requirements¶
Node.js packages¶
Third-party packages¶
Local packages¶
Globals¶
These globals are truly global: only one is needed for this entire plugin.
The Thrift network connection to the CodeChat Server.
The Thrift client using this connection.
Where the webclient resides: html for a webview panel embedded in VSCode; browser to use an external browser.
True if the subscriptions to IDE change notifications have been registered.
A unique instance of these variables is required for each CodeChat panel. However, this code doesn’t have a good UI way to deal with multiple panels, so only one is supported at this time.
The id of this render client, assigned by the CodeChat Server.
The webview panel used to display the CodeChat Client
A timer used to wait for additional events (keystrokes, etc.) before performing a render.
Activation/deactivation¶
This is invoked when the extension is activated. It either creates a new CodeChat instance or reveals the currently running one.
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand(
"extension.codeChatDeactivate",
deactivate
),
vscode.commands.registerCommand(
"extension.codeChatActivate",
async () => {
console.log("CodeChat extension starting.");
if (!subscribed) {
subscribed = true;
Render when the text is changed by listening for the correct event.
Render when the active editor changes.
Get the CodeChat Client’s location from the VSCode configuration.
const codechat_client_location_str = vscode.workspace
.getConfiguration("CodeChat.CodeChatServer")
.get("ClientLocation");
assert(typeof codechat_client_location_str === "string");
switch (codechat_client_location_str) {
case "html":
codechat_client_location =
ttypes.CodeChatClientLocation.html;
break;
case "browser":
codechat_client_location =
ttypes.CodeChatClientLocation.browser;
break;
default:
assert(false);
}
Create or reveal the webview panel; if this is an external browser, we’ll open it after the client is created.
As below, don’t take the focus when revealing.
Create a webview panel.
Without this, the focus becomes this webview; setting this allows the code window open before this command was executed to retain the focus and be immediately rendered.
Put this in the a column beside the current column.
See WebViewOptions.
Note: Per the docs, there’s a way to map from ports on the extension host machine (which may be running remotely) to local ports the webview sees (since webviews always run locally). However, this doesn’t support websockets, and should also be in place when using an external browser. Therefore, we don’t supply portMapping.
TODO: do I need to dispose of this and the following event handlers? I’m assuming that it will be done automatically when the object is disposed.
Shut down the render client when the webview panel closes.
Render when the webview panel is shown.
Provide a simple status display while the CodeChat System is starting up.
If we have an ID, then the GUI is already running; don’t replace it.
Start the server.
The client should never exist if there’s no connection.
Try to connect to the CodeChat Server. The createConnection function wraps net.createConnection then returns a Connection object.
This must use the CodeChat service port.
thrift_connection = thrift.createConnection(
"localhost",
ttypes.THRIFT_PORT,
{
transport: thrift.TBufferedTransport,
protocol: thrift.TBinaryProtocol,
}
);
let was_error: boolean = false;
thrift_connection.on("error", function (err) {
console.log(
`CodeChat extension: error in Thrift connection: ${err.message}`
);
was_error = true;
show_error(
`Error communicating with the CodeChat Server: ${err.message}. Re-run the CodeChat extension to restart it.`
);
End the connection, to hopefully avoid the socketing entering the TIME-WAIT state.
The close event will be emitted next; that will handle cleanup.
If there was an error, the event handler above already provided the message. Note: the parameter hadError only applies to transmission errors, not to any other errors which trigger the error callback. Therefore, I’m using the was_error flag instead to catch non-transmission errors.
Since the connection is closed, we can’t gracefully shut down the client via stop_client(). Simply mark it as undefined so it will be re-created.
If this was invoked while a connection is still pending, let that connection run its course.
On deactivation, close everything down.
CodeChat services¶
Get the render client from the CodeChat Server and place it in the web view. Then, start a render.
Get a client if needed.
Get a render client if needed.
if (codechat_client_id === undefined) {
console.log("CodeChat extension: requesting a render client.");
thrift_client.get_client(
codechat_client_location,
function (err, render_client_return) {
if (err !== null) {
show_error(
`Communication error getting render client: ${err}`
);
stop_client();
} else if (render_client_return.error === "") {
For a browser location, the panel shouldn’t exist and the HTML should be empty. Otherwise, assign the HTML to the panel.
Save the ID just provided.
Do an initial render.
If the render client already exists, simply perform a render.
This is called after an event such as an edit, or when the CodeChat panel becomes visible. Wait a bit in case any other events occur, then request a render.
Render after some inactivity: cancel any existing timer, then …
… schedule a render after 300 ms.
idle_timer = setTimeout(() => {
if (can_render()) {
console.log("CodeChat extension: starting render.");
thrift_client!.start_render(
vscode.window.activeTextEditor!.document.getText(),
vscode.window.activeTextEditor!.document.fileName,
codechat_client_id!,
vscode.window.activeTextEditor!.document.isDirty,
(err, start_render_return) => {
if (err !== null) {
show_error(
`Communication error when rendering: ${err}`
);
} else if (start_render_return !== "") {
show_error(
`Error when rendering: ${start_render_return}`
);
}
}
);
}
}, 300);
}
}
Gracefully shut down the render client if possible. Shut down the client as well.
Make a local copy to use for calling .end(). If this function is called twice, then thrift_connection will be set to false; if the callback invoked from the first call hasn’t fun, then this local copy will still work.
const local_thrift_connection = thrift_connection;
assert(codechat_client_id !== undefined);
thrift_client.stop_client(
codechat_client_id,
function (err, stop_client_return) {
if (err !== null) {
show_error(
`Communication error when stopping the client: ${err}`
);
} else if (stop_client_return !== "") {
show_error(
`Error when stopping the client: ${stop_client_return}`
);
}
Close the Thrift connection in case the server is shutting down. Ideally, the server would return some sort of “shutting down now” response in stop_client_return, but it’s difficult for the server to know this.
See above – assume the server will soon shut down.
Even though the callbacks to stop_client haven’t completed yet, set this now to prevent further use of the client, which is stopping.
Shut the timer down after the client is undefined, to ensure it can’t be started again by a call to start_render().
Supporting functions¶
Provide an error message in the panel if possible.
If the panel was displaying other content, reset it for errors.
if (!webview_panel.webview.html.startsWith("<h1>CodeChat</h1>")) {
webview_panel.webview.html = "<h1>CodeChat</h1>";
}
webview_panel.webview.html += `<p style="white-space: pre-wrap;">${escape(
message
)}</p><p>See the <a href="https://codechat-system.readthedocs.io/en/latest/docs/common_problems.html" target="_blank" rel="noreferrer noopener">docs</a>.</p>`;
} else {
vscode.window.showErrorMessage(
message +
"\nSee https://codechat-system.readthedocs.io/en/latest/docs/common_problems.html."
);
}
}
Only render if the window and editor are active, we have a valid render client, and the webview is visible.
If rendering in an external browser, the CodeChat panel doesn’t need to be visible.
Get the command from the VSCode configuration.
const codechat_server_command = vscode.workspace
.getConfiguration("CodeChat.CodeChatServer")
.get("Command");
assert(typeof codechat_server_command === "string");
let stdout = "";
let stderr = "";
return new Promise((resolve, reject) => {
const server_process = child_process.spawn(codechat_server_command, [
"start",
]);
server_process.on("error", (err: NodeJS.ErrnoException) => {
const msg =
err.code === "ENOENT"
? `Error - cannot find the file ${err.path}`
: err;
reject(new Error(`While starting the CodeChat Server: ${msg}.`));
});
server_process.on("exit", (code, signal) => {
const exit_str = code ? `code ${code}` : `signal ${signal}`;
if (code === 0) {
resolve("");
} else {
reject(
new Error(
`${stdout}\n${stderr}\n\nCodeChat Server exited with ${exit_str}.\n`
)
);
}
});
assert(server_process.stdout !== null);
server_process.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
assert(server_process.stderr !== null);
server_process.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
});
}