Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## [0.0.16]

- Added custom command `custom.listOpenedFiles` to get the list of all files currently opened
- Added custom command `custom.currentEditorContent` to get the content of the current (in-focus) editor as a string (if any)
- Added custom command `custom.registerEventHandler` so that an external HTTP server can handle events from VSCode API

## [0.0.15]

- Added custom error message when registering an external formatter via `custom.registerExternalFormatter`.
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ As the extension progresses, I plan to add more _special_ commands (i.e. command
- `custom.listInstalledExtensions`: get the list of installed extension IDs
- `custom.getExtensionInfo`: get details of an installed extension by passing the extension ID
- `custom.registerExternalFormatter`: registers an external formatter via a HTTP endpoint. The HTTP endpoint will receive a JSON body with the following properties`{"file": "<document file path>", "snippet": "<content to be formatted>", "language": "<language id of the current file>"}` and it should return in the body the formatted code snippet (or the original if the code can't be formatted).

- `custom.listOpenedFiles`: gets the list of all files currently opened (`string[]`)
- `custom.currentEditorContent`: to get the content of the current (in-focus) editor as a string (`string` or `null`)
- `custom.registerEventHandler`: so that an external HTTP server can handle events from VSCode API. Events supported right now are:
- `vscode.window.onDidChangeActiveTextEditor`: notifies the full path of the opened file whenever the active text editor changes
## To implement in the near future:
- Add the ability to set a breakpoint at the specified file/line combination

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "This extension allows you to remotely control Visual Studio Code via a REST endpoint, taking automation to the next level.",
"publisher": "dpar39",
"license": "MIT",
"version": "0.0.15",
"version": "0.0.16",
"engines": {
"vscode": "^1.55.0"
},
Expand Down
80 changes: 80 additions & 0 deletions src/services/eventHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as vscode from "vscode";
import * as http from "http";
import * as https from "https";

function sendEvent(
url: URL,
eventName: string,
eventData: any,
httpMethod: string = "POST",
onErrorMessage = "",
): Promise<void> {
const payload = JSON.stringify({ name: eventName, data: eventData || null });
const options = {
hostname: url.hostname,
port: url.port,
path: url.pathname,
method: httpMethod,
headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention
"Content-Type": "application/json",
// eslint-disable-next-line @typescript-eslint/naming-convention
"Content-Length": Buffer.byteLength(payload),
},
};

return new Promise((accept, reject) => {
const httpModule = url.protocol.startsWith("https") ? https : http;
const req = httpModule.request(options, (res) => {
let data = "";
``;
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
accept();
});
res.on("error", (err) => {
reject(err);
vscode.window.showErrorMessage(`Error notifying of event ${eventName}: ${err}`, "OK");
});
});
req.on("error", (err) => {
req.destroy();
vscode.window.showErrorMessage(`${onErrorMessage} - ${err}`, "OK");
reject(err);
});
req.write(payload);
req.end();
});
}

let eventHandlerRegistrations: vscode.Disposable[] = [];
export async function registerEventHandler(
eventHandlerEndpoint: string,
eventTypes: string[],
httpMethod: string,
onErrorMessage: string,
) {
httpMethod = httpMethod || "POST";
for (let ehr of eventHandlerRegistrations) {
ehr.dispose();
}
eventHandlerRegistrations = [];
const url = new URL(eventHandlerEndpoint);
if (eventTypes.includes("vscode.window.onDidChangeActiveTextEditor")) {
eventHandlerRegistrations.push(
vscode.window.onDidChangeActiveTextEditor((editor) => {
sendEvent(
url,
"vscode.window.onDidChangeActiveTextEditor",
editor?.document.uri.fsPath,
httpMethod,
onErrorMessage,
);
}),
);
}
if (eventTypes.includes("foo")) {
}
}
13 changes: 13 additions & 0 deletions src/services/requestProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from "path";

import { quickPick } from "./quickPick";
import { registerExternalFormatter } from "./formatter";
import { registerEventHandler } from "./eventHandler";

function createObject(arg: any): any {
if (typeof arg === "object" && arg.hasOwnProperty("__type__")) {
Expand Down Expand Up @@ -145,6 +146,18 @@ export async function processRemoteControlRequest(command: string, args: any[]):
return await registerExternalFormatter(args[0], args[1], args[2], args[3]);
}

if (command === "custom.registerEventHandler") {
return await registerEventHandler(args[0], args[1], args[2], args[3]);
}

if (command === "custom.listOpenedFiles") {
return vscode.window.visibleTextEditors.map((editor) => editor.document.uri.fsPath);
}

if (command === "custom.currentFileContent") {
return vscode.window.activeTextEditor?.document.getText() || null;
}

// try to run an arbitrary command with the arguments provided as is
return await vscode.commands.executeCommand(command, ...args);
}
79 changes: 76 additions & 3 deletions src/test/suite/extension.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,58 @@
import * as assert from "assert";
import { EXTENSION_ID } from "../../extension";
import * as fs from "fs";
import { IncomingMessage, Server, ServerResponse, createServer } from "http";

// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import { makeRequest } from "./sendPostRequest";
import { AddressInfo } from "net";

class MockHttpServer {
private _server: Server;

constructor(
onRequestCallback: (data: any | undefined) => void,
onErrorCallback?: (err: Error) => void,
) {
this._server = createServer((req: IncomingMessage, res: ServerResponse) => {
let body: Buffer[] = [];
req.on("data", (chunk) => {
body.push(chunk);
});
req.on("end", () => {
if (body.length > 0) {
onRequestCallback(JSON.parse(Buffer.concat(body).toString()));
} else {
onErrorCallback?.(new Error("No data received"));
}
// eslint-disable-next-line @typescript-eslint/naming-convention
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({}));
});

req.on("error", (err) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Server error.");
onErrorCallback?.(err);
});
});
}

start(): Promise<string> {
return new Promise((resolve, reject) => {
this._server.listen(0, () => {
const port = (this._server.address() as AddressInfo)!.port as number;
resolve(`http://localhost:${port}`);
});
this._server.on("error", (err) => {
console.error(`Error starting mock HTTP server: ${err}`);
reject(err);
});
});
}
}

suite("Extension Test Suite", () => {
suiteSetup(async () => {});
Expand Down Expand Up @@ -43,14 +91,39 @@ suite("Extension Test Suite", () => {

test("test can open document and get its content", async () => {
const xx = await makeRequest("custom.goToFileLineCharacter", ["demo.py:17:28"]);
const content: string = (await makeRequest("custom.eval", [
"vscode.window.activeTextEditor?.document.getText()",
])) as string;
const content: string = (await makeRequest("custom.currentFileContent")) as string;
const workspaceFolders = (await makeRequest("custom.workspaceFolders")) as any[];
const workspaceAbsPath = workspaceFolders[0].uri.slice("file://".length);
const expectedContent = fs.readFileSync(workspaceAbsPath + "/demo.py", {
encoding: "utf-8",
});
assert(content === expectedContent);
});

test("get all opened files", async () => {
const xx = await makeRequest("custom.goToFileLineCharacter", ["demo.py:17:28"]);
const openedFiles: string[] = (await makeRequest("custom.listOpenedFiles")) as string[];
assert(openedFiles.length > 0);
assert(openedFiles.some((file) => file.endsWith("workspace1/demo.py")));
});

test("can get vscode.window.onDidChangeActiveTextEditor events", (done) => {
const server = new MockHttpServer((data) => {
console.log("Received event data:", data);
assert(data.name === "vscode.window.onDidChangeActiveTextEditor");
if (data.data !== null) {
assert(data.data.endsWith("samples.http"));
done();
}
});
server.start().then(async (serverUrl) => {
await makeRequest("custom.registerEventHandler", [
serverUrl,
["vscode.window.onDidChangeActiveTextEditor"],
"POST",
"Failed to register event handler",
]);
await makeRequest("custom.goToFileLineCharacter", ["samples.http:1:1"]);
});
});
});