diff --git a/src/extension.ts b/src/extension.ts index 26941969..f384bf84 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -446,21 +446,6 @@ export async function checkConnection( serverVersion: info.result.content.version, healthshare: hasHS ? "yes" : "no", }); - - // Update CSP web app cache if required - const key = `${api.serverId}:${api.config.ns}`.toLowerCase(); - if (!cspApps.has(key)) { - cspApps.set(key, await api.getCSPApps().then((data) => data.result.content || [])); - } - if (!otherDocExts.has(key)) { - otherDocExts.set( - key, - await api - .actionQuery("SELECT Extention FROM %Library.RoutineMgr_DocumentTypes()", []) - .then((data) => data.result?.content?.map((e) => e.Extention) ?? []) - .catch(() => []) - ); - } if (!api.externalServer) { await setConnectionState(configName, true); } @@ -756,6 +741,44 @@ function setExplorerContextKeys(): void { ); } +/** Cache the lists of web apps and abstract document types for all server-namespaces in `wsFolders` */ +async function updateWebAndAbstractDocsCaches(wsFolders: readonly vscode.WorkspaceFolder[]): Promise { + if (!wsFolders?.length) return; + const keys: Set = new Set(); + const connections: { key: string; api: AtelierAPI }[] = []; + // Filter out any duplicate connections + for (const wsFolder of wsFolders) { + const api = new AtelierAPI(wsFolder.uri); + if (!api.active) continue; + const key = `${api.serverId}:${api.config.ns}`.toLowerCase(); + if (keys.has(key)) continue; + keys.add(key); + connections.push({ key, api }); + } + return Promise.allSettled( + connections.map(async (connection) => { + if (!cspApps.has(connection.key)) { + cspApps.set( + connection.key, + await connection.api + .getCSPApps() + .then((data) => data.result.content ?? []) + .catch(() => []) + ); + } + if (!otherDocExts.has(connection.key)) { + otherDocExts.set( + connection.key, + await connection.api + .actionQuery("SELECT Extention FROM %Library.RoutineMgr_DocumentTypes()", []) + .then((data) => data.result?.content?.map((e) => e.Extention) ?? []) + .catch(() => []) + ); + } + }) + ); +} + /** The URIs of all classes that have been opened. Used when `objectscript.openClassContracted` is true */ let openedClasses: string[]; @@ -847,9 +870,9 @@ export async function activate(context: vscode.ExtensionContext): Promise { continue; } } - await Promise.allSettled( - vscode.workspace.workspaceFolders?.map((wsFolder) => addWsServerRootFolderData(wsFolder.uri)) || [] - ); + + await updateWebAndAbstractDocsCaches(vscode.workspace.workspaceFolders); + await addWsServerRootFolderData(vscode.workspace.workspaceFolders); xmlContentProvider = new XmlContentProvider(); @@ -1373,22 +1396,6 @@ export async function activate(context: vscode.ExtensionContext): Promise { }, supportsMultipleEditorsPerDocument: false, }), - vscode.workspace.onDidChangeWorkspaceFolders(async ({ added }) => { - // Make sure we have a resolved connection spec for the targets of all added folders - const toCheck = new Map(); - added.map((workspaceFolder) => { - const uri = workspaceFolder.uri; - const { configName } = connectionTarget(uri); - toCheck.set(configName, uri); - }); - for await (const oneToCheck of toCheck) { - const configName = oneToCheck[0]; - const uri = oneToCheck[1]; - const serverName = notIsfs(uri) ? config("conn", configName).server : configName; - await resolveConnectionSpec(serverName); - } - await Promise.allSettled(added.map((wsFolder) => addWsServerRootFolderData(wsFolder.uri))); - }), vscode.workspace.onDidChangeConfiguration(async ({ affectsConfiguration }) => { if (affectsConfiguration("objectscript.conn") || affectsConfiguration("intersystems.servers")) { if (affectsConfiguration("intersystems.servers")) { @@ -1545,7 +1552,23 @@ export async function activate(context: vscode.ExtensionContext): Promise { "vscode-objectscript.webSocketTerminal", new WebSocketTerminalProfileProvider() ), - vscode.workspace.onDidChangeWorkspaceFolders((e) => { + vscode.workspace.onDidChangeWorkspaceFolders(async (e) => { + // Make sure we have a resolved connection spec for the targets of all added folders + const toCheck = new Map(); + e.added.map((workspaceFolder) => { + const uri = workspaceFolder.uri; + const { configName } = connectionTarget(uri); + toCheck.set(configName, uri); + }); + for await (const oneToCheck of toCheck) { + const configName = oneToCheck[0]; + const uri = oneToCheck[1]; + const serverName = notIsfs(uri) ? config("conn", configName).server : configName; + await resolveConnectionSpec(serverName); + } + // await this so the next step can take advantage of the caching + await updateWebAndAbstractDocsCaches(e.added); + addWsServerRootFolderData(e.added); // Show the proposed API prompt if required proposedApiPrompt(proposed.length > 0, e.added); // Warn about SystemMode diff --git a/src/providers/FileSystemProvider/FileSystemProvider.ts b/src/providers/FileSystemProvider/FileSystemProvider.ts index edef9c47..f26e64fd 100644 --- a/src/providers/FileSystemProvider/FileSystemProvider.ts +++ b/src/providers/FileSystemProvider/FileSystemProvider.ts @@ -243,7 +243,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { if (!api.active) throw vscode.FileSystemError.Unavailable("Server connection is inactive"); let entryPromise: Promise; let result: Entry; - const redirectedUri = redirectDotvscodeRoot(uri); + const redirectedUri = redirectDotvscodeRoot(uri, vscode.FileSystemError.FileNotFound(uri)); if (redirectedUri.path !== uri.path) { // When redirecting the /.vscode subtree we must fill in as-yet-unvisited folders to fix https://github.com/intersystems-community/vscode-objectscript/issues/1143 entryPromise = this._lookup(redirectedUri, true); @@ -290,8 +290,8 @@ export class FileSystemProvider implements vscode.FileSystemProvider { } public async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { - if (uri.path.includes(".vscode/")) { - throw vscode.FileSystemError.NoPermissions("Cannot read the /.vscode directory"); + if (uri.path.includes(".vscode/") || uri.path.endsWith(".vscode")) { + throw new vscode.FileSystemError("Cannot read the /.vscode directory"); } const parent = await this._lookupAsDirectory(uri); const api = new AtelierAPI(uri); @@ -406,7 +406,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { } public createDirectory(uri: vscode.Uri): void | Thenable { - uri = redirectDotvscodeRoot(uri); + uri = redirectDotvscodeRoot(uri, new vscode.FileSystemError("Server does not have a /_vscode web application")); const basename = path.posix.basename(uri.path); const dirname = uri.with({ path: path.posix.dirname(uri.path) }); return this._lookupAsDirectory(dirname).then((parent) => { @@ -435,9 +435,9 @@ export class FileSystemProvider implements vscode.FileSystemProvider { overwrite: boolean; } ): void | Thenable { - uri = redirectDotvscodeRoot(uri); + uri = redirectDotvscodeRoot(uri, new vscode.FileSystemError("Server does not have a /_vscode web application")); if (uri.path.startsWith("/.")) { - throw vscode.FileSystemError.NoPermissions("dot-folders not supported by server"); + throw new vscode.FileSystemError("dot-folders are not supported by server"); } const csp = isCSP(uri); const fileName = isfsDocumentName(uri, csp); @@ -462,10 +462,12 @@ export class FileSystemProvider implements vscode.FileSystemProvider { [, clsname] = match; } if (clsname == "") { - throw new Error("Cannot save a malformed class"); + throw new vscode.FileSystemError("Cannot save a malformed class"); } if (fileName.slice(0, -4) != clsname) { - throw new Error("Cannot save an isfs class where the class name and file name do not match"); + throw new vscode.FileSystemError( + "Cannot save an isfs class where the class name and file name do not match" + ); } if (openLowCodeEditors.has(uri.toString())) { // This class is open in a low-code editor, so any @@ -474,7 +476,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { } // Check if the class is deployed if (await isClassDeployed(fileName, api)) { - throw new Error("Cannot overwrite a deployed class"); + throw new vscode.FileSystemError("Cannot overwrite a deployed class"); } } const contentBuffer = Buffer.from(content); @@ -505,7 +507,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { .catch((error) => { // Throw all failures const errorStr = stringifyError(error); - throw errorStr ? errorStr : vscode.FileSystemError.Unavailable(uri); + throw errorStr ? new vscode.FileSystemError(errorStr) : vscode.FileSystemError.Unavailable(uri); }); }, (error) => { @@ -529,7 +531,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { .catch((error) => { // Throw all failures const errorStr = stringifyError(error); - throw errorStr ? errorStr : vscode.FileSystemError.Unavailable(uri); + throw errorStr ? new vscode.FileSystemError(errorStr) : vscode.FileSystemError.Unavailable(uri); }) .then((data) => { // New file has been written @@ -639,7 +641,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { } public async delete(uri: vscode.Uri, options: { recursive: boolean }): Promise { - uri = redirectDotvscodeRoot(uri); + uri = redirectDotvscodeRoot(uri, vscode.FileSystemError.FileNotFound(uri)); const { project } = isfsConfig(uri); const csp = isCSP(uri); const api = new AtelierAPI(uri); @@ -724,13 +726,13 @@ export class FileSystemProvider implements vscode.FileSystemProvider { public async rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean }): Promise { if (!oldUri.path.split("/").pop().includes(".")) { - throw vscode.FileSystemError.NoPermissions("Cannot rename a package/folder"); + throw new vscode.FileSystemError("Cannot rename a package/folder"); } if (oldUri.path.split(".").pop().toLowerCase() != newUri.path.split(".").pop().toLowerCase()) { - throw vscode.FileSystemError.NoPermissions("Cannot change a file's extension during rename"); + throw new vscode.FileSystemError("Cannot change a file's extension during rename"); } if (vscode.workspace.getWorkspaceFolder(oldUri) != vscode.workspace.getWorkspaceFolder(newUri)) { - throw vscode.FileSystemError.NoPermissions("Cannot rename a file across workspace folders"); + throw new vscode.FileSystemError("Cannot rename a file across workspace folders"); } // Check if the destination exists let newFileStat: vscode.FileStat; @@ -774,7 +776,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { .catch((error) => { // Throw all failures const errorStr = stringifyError(error); - throw errorStr ? errorStr : vscode.FileSystemError.Unavailable(newUri); + throw errorStr ? new vscode.FileSystemError(errorStr) : vscode.FileSystemError.Unavailable(newUri); }) .then(async (response) => { // New file has been written @@ -807,7 +809,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { */ public async compile(uri: vscode.Uri, file?: File, update?: boolean): Promise { if (!uri || uri.scheme != FILESYSTEM_SCHEMA) return; - uri = redirectDotvscodeRoot(uri); + uri = redirectDotvscodeRoot(uri, new vscode.FileSystemError("Server does not have a /_vscode web application")); const compileList: string[] = []; try { const entry = file || (await this._lookup(uri, true)); @@ -958,9 +960,9 @@ export class FileSystemProvider implements vscode.FileSystemProvider { // Fetch from server and cache it, optionally the passed cached copy if unchanged on server private async _lookupAsFile(uri: vscode.Uri, cachedFile?: File): Promise { - uri = redirectDotvscodeRoot(uri); + uri = redirectDotvscodeRoot(uri, vscode.FileSystemError.FileNotFound(uri)); if (uri.path.startsWith("/.")) { - throw vscode.FileSystemError.NoPermissions("dot-folders not supported by server"); + throw new vscode.FileSystemError("dot-folders are not supported by server"); } const csp = isCSP(uri); const name = path.basename(uri.path); @@ -992,13 +994,13 @@ export class FileSystemProvider implements vscode.FileSystemProvider { throw error?.statusCode == 404 ? vscode.FileSystemError.FileNotFound(uri) : errorStr - ? errorStr + ? new vscode.FileSystemError(errorStr) : vscode.FileSystemError.Unavailable(uri); }); } private async _lookupParentDirectory(uri: vscode.Uri): Promise { - uri = redirectDotvscodeRoot(uri); + uri = redirectDotvscodeRoot(uri, new vscode.FileSystemError("Server does not have a /_vscode web application")); return this._lookupAsDirectory(uri.with({ path: path.posix.dirname(uri.path) })); } diff --git a/src/utils/index.ts b/src/utils/index.ts index 7710fbf6..40935b85 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -140,14 +140,7 @@ export function cspAppsForUri(uri: vscode.Uri): string[] { /** Get a list of all CSP web apps in the server-namespace that `api` is connected to. */ export function cspAppsForApi(api: AtelierAPI): string[] { - return ( - cspApps.get( - (api.config.serverName && api.config.serverName != "" - ? `${api.config.serverName}:${api.config.ns}` - : `${api.config.host}:${api.config.port}${api.config.pathPrefix}:${api.config.ns}` - ).toLowerCase() - ) ?? [] - ); + return cspApps.get(`${api.serverId}:${api.config.ns}`.toLowerCase()) ?? []; } /** @@ -598,31 +591,50 @@ export async function shellWithDocker(): Promise { interface WSServerRootFolderData { redirectDotvscode: boolean; + canRedirectDotvscode: boolean; } const wsServerRootFolders = new Map(); -/** - * Add uri to the wsServerRootFolders map if eligible - */ -export async function addWsServerRootFolderData(uri: vscode.Uri): Promise { - if (!schemas.includes(uri.scheme)) { - return; - } - const value: WSServerRootFolderData = { - redirectDotvscode: true, - }; - if (isCSP(uri) && !["", "/"].includes(uri.path)) { - // A CSP-type root folder for a specific webapp that already has a .vscode/settings.json file must not redirect .vscode/* references - const api = new AtelierAPI(uri); - api - .headDoc(`${uri.path}${!uri.path.endsWith("/") ? "/" : ""}.vscode/settings.json`) - .then(() => { - value.redirectDotvscode = false; - }) - .catch(() => {}); - } - wsServerRootFolders.set(uri.toString(), value); +/** Cache information about redirection of `.vscode` folder contents for server-side folders */ +export async function addWsServerRootFolderData(wsFolders: readonly vscode.WorkspaceFolder[]): Promise { + if (!wsFolders?.length) return; + return Promise.allSettled( + wsFolders.map(async (wsFolder) => { + if (notIsfs(wsFolder.uri)) return; + const api = new AtelierAPI(wsFolder.uri); + if (!api.active) return; + const value: WSServerRootFolderData = { + redirectDotvscode: true, + canRedirectDotvscode: true, + }; + if (isCSP(wsFolder.uri) && !["", "/"].includes(wsFolder.uri.path)) { + // A CSP-type root folder for a specific webapp that already has a + // .vscode/settings.json file must not redirect .vscode/* references + await api + .headDoc(`${wsFolder.uri.path}${!wsFolder.uri.path.endsWith("/") ? "/" : ""}.vscode/settings.json`) + .then(() => { + value.redirectDotvscode = false; + }) + .catch(() => {}); + } + if (value.redirectDotvscode) { + // We must redirect .vscode Uris for this folder, so see + // if the web app to do so is configured on the server + const key = `${api.serverId}:%SYS`.toLowerCase(); + let webApps = cspApps.get(key); + if (!webApps) { + webApps = await api + .getCSPApps(false, "%SYS") + .then((data) => data.result.content ?? []) + .catch(() => []); + cspApps.set(key, webApps); + } + value.canRedirectDotvscode = webApps.includes("/_vscode"); + } + wsServerRootFolders.set(wsFolder.uri.toString(), value); + }) + ); } /** @@ -635,16 +647,23 @@ export async function addWsServerRootFolderData(uri: vscode.Uri): Promise * For both syntaxes the namespace folder name is uppercased * * @returns uri, altered if necessary. - * @throws if `ns` queryparam is missing but required. + * @throws if `ns` queryparam is missing but required, or if redirection + * is required but not supported by the server and `err` was passed. */ -export function redirectDotvscodeRoot(uri: vscode.Uri): vscode.Uri { - if (!schemas.includes(uri.scheme)) { - return uri; - } +export function redirectDotvscodeRoot(uri: vscode.Uri, err?: vscode.FileSystemError): vscode.Uri { + if (notIsfs(uri)) return; const dotMatch = uri.path.match(/^(.*)\/\.vscode(\/.*)?$/); if (dotMatch) { const dotvscodeRoot = uri.with({ path: dotMatch[1] || "/" }); - if (!wsServerRootFolders.get(dotvscodeRoot.toString())?.redirectDotvscode) { + const rootData = wsServerRootFolders.get(dotvscodeRoot.toString()); + if (!rootData?.redirectDotvscode) { + // Don't redirect .vscode Uris + return uri; + } + if (!rootData?.canRedirectDotvscode) { + // Need to redirect .vscode Uris, but the server doesn't support it. + // Throw if the caller gave us something to throw. + if (err) throw err; return uri; } let namespace: string;