Skip to content
Open
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
14 changes: 7 additions & 7 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@intersystems-community/intersystems-servermanager": "^3.10.2",
"@intersystems-community/intersystems-servermanager": "^3.10.3-beta.1",
"@types/semver": "^7.7.0",
"@types/vscode": "1.93.0"
}
Expand Down
185 changes: 94 additions & 91 deletions client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,21 @@ import {
selectParameterType,
setSelection,
} from "./commands";
import { makeRESTRequest, ServerSpec } from "./makeRESTRequest";
import { makeRESTRequest } from "./makeRESTRequest";
import { ISCEmbeddedContentProvider, requestForwardingMiddleware } from "./requestForwarding";
import type { ServerSpec, ProtocolMethods } from "../../common/out/types";
import type { Disposable } from "vscode-languageclient";

export let client: LanguageClient;
type TypedLanguageClient = {
onRequest<K extends keyof ProtocolMethods>(
method: K,
handler: (
params: Parameters<ProtocolMethods[K]>[0]
) => ReturnType<ProtocolMethods[K]>
): Disposable;
} & Omit<LanguageClient, 'onRequest'>;

export let client: TypedLanguageClient;

/**
* Cache for cookies from REST requests to InterSystems servers.
Expand All @@ -61,22 +72,12 @@ export function getCookies(server: ServerSpec): string[] {
return cookiesCache.get(`${server.username}@${server.host}:${server.port}${server.pathPrefix}`) ?? [];
}

let objectScriptApi: any;
let objectScriptApi: serverManager.VSCodeObjectScriptAPI;
let serverManagerApi: serverManager.ServerManagerAPI;

/** Resolved connection information for each workspace folder */
const wsFolderServerSpecs: Map<string, ServerSpec> = new Map();

type MakeRESTRequestParams = {
method: "GET" | "POST";
api: number;
path: string;
server: ServerSpec;
data?: any;
checksum?: string;
params?: any;
};

export async function activate(context: ExtensionContext) {
// Get the main extension exported API
const objectScriptExt = extensions.getExtension("intersystems-community.vscode-objectscript")!;
Expand Down Expand Up @@ -145,12 +146,12 @@ export async function activate(context: ExtensionContext) {
"InterSystems Language Server",
serverOptions,
clientOptions,
);
) as TypedLanguageClient;

// Send custom notifications when the connection or password changes
objectScriptApi.onDidChangeConnection()(() => {
wsFolderServerSpecs.clear();
client.sendNotification("intersystems/server/connectionChange");
client.sendNotification('intersystems/server/connectionChange');
});
const serverManagerExt = extensions.getExtension("intersystems-community.servermanager");
if (serverManagerExt !== undefined) {
Expand All @@ -160,36 +161,37 @@ export async function activate(context: ExtensionContext) {
for (const [k, v] of wsFolderServerSpecs.entries()) {
if (v.serverName == serverName) wsFolderServerSpecs.delete(k);
}
client.sendNotification("intersystems/server/passwordChange", serverName);
client.sendNotification('intersystems/server/passwordChange', serverName);
});
}

const textDecoder = new TextDecoder();

context.subscriptions.push(
// Register custom request handlers
client.onRequest("intersystems/server/resolveFromUri", async (uri: string) => {
client.onRequest('intersystems/server/resolveFromUri', async (uri) => {
const uriObj = Uri.parse(uri);
const wsFolderUriString = workspace.getWorkspaceFolder(uriObj)?.uri.toString();
const serverSpec = objectScriptApi.serverForUri(uriObj);
const { auth, ...serverForURI }: serverManager.ServerForUri = objectScriptApi.serverForUri(uriObj);
if (
// Server was resolved
serverSpec.host !== "" &&
serverForURI.host !== "" &&
// Connection isn't unauthenticated
serverSpec.username != undefined &&
serverSpec.username != "" &&
serverSpec.username.toLowerCase() != "unknownuser" &&
auth.username != undefined &&
auth.username != "" &&
auth.username.toLowerCase() != "unknownuser" &&
// A password is missing
typeof serverSpec.password === "undefined" &&
typeof auth.password === "undefined" &&
// A supported version of the Server Manager is installed
serverManagerExt != undefined &&
gt(serverManagerExt.packageJSON.version, "3.0.0")
) {
// The main extension didn't provide a password, so we must
// get it from the server manager's authentication provider.
const scopes = [serverSpec.serverName, serverSpec.username];
const scopes = [serverForURI.serverName, auth.username];
try {
const account = serverManagerApi?.getAccount
? serverManagerApi.getAccount({ name: serverSpec.serverName, ...serverSpec })
? serverManagerApi.getAccount({ name: serverForURI.serverName, ...serverForURI })
: undefined;
let session = await authentication.getSession(serverManager.AUTHENTICATION_PROVIDER, scopes, {
silent: true,
Expand All @@ -202,8 +204,10 @@ export async function activate(context: ExtensionContext) {
});
}
if (session) {
serverSpec.username = session.scopes[1];
serverSpec.password = session.accessToken;
auth.resolve({
username: session.scopes[1],
accessToken: session.accessToken,
});
}
} catch (error) {
// The user did not consent to sharing authentication information
Expand All @@ -213,23 +217,28 @@ export async function activate(context: ExtensionContext) {
}
}
if (
typeof serverSpec.username == "string" &&
serverSpec.username.toLowerCase() == "unknownuser" &&
typeof serverSpec.password == "undefined"
typeof auth.username == "string" &&
auth.username.toLowerCase() == "unknownuser" &&
typeof auth.password == "undefined"
) {
// UnknownUser without a password means "unauthenticated"
serverSpec.username = undefined;
auth.clear() as void;
}
const serverSpec: ServerSpec = {
...serverForURI,
username: auth.username,
credentials: auth.credentials,
};
if (wsFolderUriString && !wsFolderServerSpecs.has(wsFolderUriString)) {
wsFolderServerSpecs.set(wsFolderUriString, serverSpec);
}
return serverSpec;
}),
client.onRequest("intersystems/uri/localToVirtual", (uri: string): string => {
client.onRequest('intersystems/uri/localToVirtual', (uri) => {
const newuri: Uri = objectScriptApi.serverDocumentUriForUri(Uri.parse(uri));
return newuri.toString();
}),
client.onRequest("intersystems/uri/forDocument", (document: string): string | null => {
client.onRequest('intersystems/uri/forDocument', (document) => {
if (lte(objectScriptExt.packageJSON.version, "1.0.10")) {
// If the active version of vscode-objectscript doesn't expose
// DocumentContentProvider.getUri(), just return the empty string.
Expand All @@ -238,78 +247,72 @@ export async function activate(context: ExtensionContext) {
const uri: Uri | null = objectScriptApi.getUriForDocument(document);
return uri == null ? null : uri.toString();
}),
client.onRequest("intersystems/uri/forTypeHierarchyClasses", (classes: string[]): string[] => {
client.onRequest('intersystems/uri/forTypeHierarchyClasses', (classes) => {
// vscode-objectscript version 1.0.11+ has been available for long enough that
// it's safe to assume that users have upgraded to at least 1.0.11
return classes.map((cls: string) => {
const uri: Uri = objectScriptApi.getUriForDocument(`${cls}.cls`);
return uri.toString();
});
}),
client.onRequest(
"intersystems/server/makeRESTRequest",
async (args: MakeRESTRequestParams): Promise<any | undefined> => {
// As of version 2.0.0, REST requests are made on the client side
return makeRESTRequest(
args.method,
args.api,
args.path,
args.server,
args.data,
args.checksum,
args.params,
).then((respdata) => {
if (respdata) {
// Can't return the entire AxiosResponse object because it's not JSON.stringify-able due to circularity
return { data: respdata.data };
} else {
return undefined;
}
});
},
),
client.onRequest(
"intersystems/uri/getText",
async (params: { uri: string; server: ServerSpec }): Promise<string[]> => {
try {
const uri = Uri.parse(params.uri);
if (uri.scheme == "objectscript") {
// Can't use the FileSystem with a DocumentContentProvider, so fetch the text directly from the server
const uriParams = new URLSearchParams(uri.query);
const fileName =
uriParams.has("csp") && ["", "1"].includes(uriParams.get("csp") ?? "")
? uri.path.slice(1)
: uri.path.split("/").slice(1).join(".");
const docParams =
params.server.apiVersion >= 4 &&
client.onRequest('intersystems/server/makeRESTRequest', async (args) => {
// As of version 2.0.0, REST requests are made on the client side
return makeRESTRequest(
args.method,
args.api,
args.path,
args.server,
args.data,
args.checksum,
args.params,
).then((respdata) => {
if (respdata) {
// Can't return the entire AxiosResponse object because it's not JSON.stringify-able due to circularity
return { data: respdata.data };
} else {
return undefined;
}
});
}),
client.onRequest('intersystems/uri/getText', async (params) => {
try {
const uri = Uri.parse(params.uri);
if (uri.scheme == "objectscript") {
// Can't use the FileSystem with a DocumentContentProvider, so fetch the text directly from the server
const uriParams = new URLSearchParams(uri.query);
const fileName =
uriParams.has("csp") && ["", "1"].includes(uriParams.get("csp") ?? "")
? uri.path.slice(1)
: uri.path.split("/").slice(1).join(".");
const docParams =
params.server.apiVersion >= 4 &&
workspace
.getConfiguration(
"objectscript",
workspace.workspaceFolders?.find((f) => f.name.toLowerCase() == uri.authority.toLowerCase()),
)
.get<boolean>("multilineMethodArgs")
? { format: "udl-multiline" }
: undefined;
const resp = await makeRESTRequest(
"GET",
1,
`/doc/${fileName}`,
params.server,
undefined,
undefined,
docParams,
);
return resp?.data?.result?.content || [];
} else {
// Read the contents of the file at uri
return textDecoder.decode(await workspace.fs.readFile(uri)).split(/\r?\n/);
}
} catch {
// The file wasn't found or wasn't valid utf-8
return [];
? { format: "udl-multiline" }
: undefined;
const resp = await makeRESTRequest(
"GET",
1,
`/doc/${fileName}`,
params.server,
undefined,
undefined,
docParams,
);
return resp?.data?.result?.content || [];
} else {
// Read the contents of the file at uri
return textDecoder.decode(await workspace.fs.readFile(uri)).split(/\r?\n/);
}
},
),
} catch {
// The file wasn't found or wasn't valid utf-8
return [];
}
}),

// Register commands
commands.registerCommand("intersystems.language-server.overrideClassMembers", overrideClassMembers),
Expand Down
Loading