diff --git a/src/commands/compile.ts b/src/commands/compile.ts index a1d17c9b..b1893127 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -181,13 +181,9 @@ What do you want to do?`, // Overwrite return importFile(file, true, true); case "Pull Server Changes": - outputChannel.appendLine(`${file.name}: Loading changes from server`); - outputChannel.show(true); loadChanges([file]); return Promise.reject(); case "Cancel": - outputChannel.appendLine(`${file.name}: Import and Compile canceled by user`); - outputChannel.show(true); return Promise.reject(); } return Promise.reject(); diff --git a/src/commands/connectFolderToServerNamespace.ts b/src/commands/connectFolderToServerNamespace.ts index c0c241ee..1cd26e16 100644 --- a/src/commands/connectFolderToServerNamespace.ts +++ b/src/commands/connectFolderToServerNamespace.ts @@ -7,7 +7,7 @@ import { serverManagerApi, resolveUsernameAndPassword, } from "../extension"; -import { handleError, isUnauthenticated, notIsfs } from "../utils"; +import { handleError, isUnauthenticated, notIsfs, displayableUri } from "../utils"; interface ConnSettings { server: string; @@ -131,8 +131,8 @@ export async function connectFolderToServerNamespace(): Promise { // the server may be configured at the workspace folder level. const answer = await vscode.window.showQuickPick( [ - { label: `Workspace Folder ${folder.name}`, detail: folder.uri.toString(true) }, - { label: "Workspace File", detail: vscode.workspace.workspaceFile.toString(true) }, + { label: `Workspace Folder ${folder.name}`, detail: displayableUri(folder.uri) }, + { label: "Workspace File", detail: displayableUri(vscode.workspace.workspaceFile) }, ], { title: "Store the server connection at the workspace or folder level?" } ); diff --git a/src/commands/export.ts b/src/commands/export.ts index c632e255..2c898dea 100644 --- a/src/commands/export.ts +++ b/src/commands/export.ts @@ -11,6 +11,7 @@ import { lastUsedLocalUri, notNull, outputChannel, + displayableUri, RateLimiter, replaceFile, stringifyError, @@ -96,7 +97,7 @@ async function exportFile(wsFolderUri: vscode.Uri, namespace: string, name: stri let fileUri = vscode.Uri.file(fileName); if (wsFolderUri.scheme != "file") fileUri = wsFolderUri.with({ path: fileUri.path }); const log = (status: string) => - outputChannel.appendLine(`Export '${name}' to '${fileUri.toString(true)}' - ${status}`); + outputChannel.appendLine(`Export '${name}' to '${displayableUri(fileUri)}' - ${status}`); try { const data = await api.getDoc(name, wsFolderUri); @@ -380,7 +381,7 @@ export async function exportDocumentsToXMLFile(): Promise { const xmlContent = await api.actionXMLExport(documents).then((data) => data.result.content); // Save the file await replaceFile(uri, xmlContent); - outputChannel.appendLine(`Exported to ${uri.scheme == "file" ? uri.fsPath : uri.toString(true)}`); + outputChannel.appendLine(`Exported to ${displayableUri(uri)}`); } } catch (error) { handleError(error, "Error executing 'Export Documents to XML File...' command."); diff --git a/src/commands/newFile.ts b/src/commands/newFile.ts index 5d299a33..3aa2093e 100644 --- a/src/commands/newFile.ts +++ b/src/commands/newFile.ts @@ -3,7 +3,7 @@ import path = require("path"); import { AtelierAPI } from "../api"; import { FILESYSTEM_SCHEMA } from "../extension"; import { DocumentContentProvider } from "../providers/DocumentContentProvider"; -import { replaceFile, getWsFolder, handleError } from "../utils"; +import { replaceFile, getWsFolder, handleError, displayableUri } from "../utils"; import { getFileName } from "./export"; import { getUrisForDocument } from "../utils/documentIndex"; @@ -949,7 +949,7 @@ Parameter ENSPURGE As BOOLEAN = 1; const inputBox = vscode.window.createInputBox(); inputBox.ignoreFocusOut = true; inputBox.buttons = [{ iconPath: new vscode.ThemeIcon("save-as"), tooltip: "Show 'Save As' dialog" }]; - inputBox.prompt = `The path is relative to the workspace folder root (${wsFolder.uri.toString(true)}). Intermediate folders that do not exist will be created. Click the 'Save As' icon to open the standard save dialog instead.`; + inputBox.prompt = `The path is relative to the workspace folder root (${displayableUri(wsFolder.uri)}). Intermediate folders that do not exist will be created. Click the 'Save As' icon to open the standard save dialog instead.`; inputBox.title = "Enter a file path for the new class"; inputBox.value = localUri.path.slice(wsFolder.uri.path.length); inputBox.valueSelection = [inputBox.value.length, inputBox.value.length]; diff --git a/src/commands/unitTest.ts b/src/commands/unitTest.ts index 421d4660..15cea315 100644 --- a/src/commands/unitTest.ts +++ b/src/commands/unitTest.ts @@ -6,6 +6,7 @@ import { handleError, methodOffsetToLine, notIsfs, + displayableUri, stripClassMemberNameQuotes, uriIsParentOf, } from "../utils"; @@ -396,7 +397,7 @@ async function runHandler( roots.map((i) => { return { label: i.label, - detail: i.uri.toString(true), + detail: displayableUri(i.uri), item: i, }; }), diff --git a/src/commands/xmlToUdl.ts b/src/commands/xmlToUdl.ts index fdb21bb7..74980af3 100644 --- a/src/commands/xmlToUdl.ts +++ b/src/commands/xmlToUdl.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode"; import path = require("path"); import { config, OBJECTSCRIPTXML_FILE_SCHEMA, xmlContentProvider } from "../extension"; import { AtelierAPI } from "../api"; -import { replaceFile, fileExists, getWsFolder, handleError, notIsfs, outputChannel } from "../utils"; +import { replaceFile, fileExists, getWsFolder, handleError, notIsfs, outputChannel, displayableUri } from "../utils"; import { getFileName } from "./export"; const exportHeader = /^\s* { const uri = textEditor.document.uri; const content = textEditor.document.getText(); + const uriString = displayableUri(uri); if (notIsfs(uri) && uri.path.toLowerCase().endsWith("xml") && textEditor.document.lineCount > 2) { if (exportHeader.test(textEditor.document.lineAt(1).text)) { const api = new AtelierAPI(uri); @@ -23,10 +24,7 @@ export async function previewXMLAsUDL(textEditor: vscode.TextEditor, auto = fals .cvtXmlUdl(content) .then((data) => data.result.content); if (udlDocs.length == 0) { - vscode.window.showErrorMessage( - `File '${uri.toString(true)}' contains no documents that can be previewed.`, - "Dismiss" - ); + vscode.window.showErrorMessage(`File '${uriString}' contains no documents that can be previewed.`, "Dismiss"); return; } // Prompt the user for documents to preview @@ -77,7 +75,7 @@ export async function previewXMLAsUDL(textEditor: vscode.TextEditor, auto = fals handleError(error, "Error executing 'Preview XML as UDL' command."); } } else if (!auto) { - vscode.window.showErrorMessage(`XML file '${uri.toString(true)}' is not an InterSystems export.`, "Dismiss"); + vscode.window.showErrorMessage(`XML file '${uriString}' is not an InterSystems export.`, "Dismiss"); } } } @@ -137,8 +135,9 @@ export async function extractXMLFileContents(xmlUri?: vscode.Uri): Promise } // Read the XML file const xmlContent = new TextDecoder().decode(await vscode.workspace.fs.readFile(xmlUri)).split(/\r?\n/); + const xmlUriString = displayableUri(xmlUri); if (xmlContent.length < 3 || !exportHeader.test(xmlContent[1])) { - vscode.window.showErrorMessage(`XML file '${xmlUri.toString(true)}' is not an InterSystems export.`, "Dismiss"); + vscode.window.showErrorMessage(`XML file '${xmlUriString}' is not an InterSystems export.`, "Dismiss"); return; } // Convert the file @@ -146,10 +145,7 @@ export async function extractXMLFileContents(xmlUri?: vscode.Uri): Promise .cvtXmlUdl(xmlContent.join("\n")) .then((data) => data.result.content); if (udlDocs.length == 0) { - vscode.window.showErrorMessage( - `File '${xmlUri.toString(true)}' contains no documents that can be extracted.`, - "Dismiss" - ); + vscode.window.showErrorMessage(`File '${xmlUriString}' contains no documents that can be extracted.`, "Dismiss"); return; } // Prompt the user for documents to extract @@ -177,7 +173,7 @@ export async function extractXMLFileContents(xmlUri?: vscode.Uri): Promise if (!docWhitelist.includes(udlDoc.name)) continue; // This file wasn't selected const fileUri = wsFolder.uri.with({ path: getFileName(rootFolder, udlDoc.name, atelier, addCategory, map, "/") }); if (await fileExists(fileUri)) { - outputChannel.appendLine(`File '${fileUri.toString(true)}' already exists.`); + outputChannel.appendLine(`File '${displayableUri(fileUri)}' already exists.`); errs++; continue; } diff --git a/src/extension.ts b/src/extension.ts index 8e2d279a..863d5a73 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -108,6 +108,7 @@ import { addWsServerRootFolderData, getWsFolder, exportedUris, + displayableUri, } from "./utils"; import { ObjectScriptDiagnosticProvider } from "./providers/ObjectScriptDiagnosticProvider"; import { DocumentLinkProvider } from "./providers/DocumentLinkProvider"; @@ -1163,7 +1164,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { } catch (error) { handleError( error, - `Failed to overwrite contents of file '${file.uri.toString(true)}' with server copy of '${file.fileName}'.` + `Failed to overwrite contents of file '${displayableUri(file.uri)}' with server copy of '${file.fileName}'.` ); } }), diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 8f6aaace..770a7d4b 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -11,6 +11,7 @@ import { notIsfs, openLowCodeEditors, outputChannel, + displayableUri, } from "."; import { isText } from "istextorbinary"; import { AtelierAPI } from "../api"; @@ -67,11 +68,11 @@ async function getCurrentFile( // or a TypeError from decode(). Don't log TypeError // since the file may be a non-text file that has // an extension that we interpret as text (like cls or mac). - // Also don't log "FileNotFound" errors, which are probably - // caused by concurrency issues. We should ignore such files - // rather than alerting the user. - if (error instanceof vscode.FileSystemError && error.code != "FileNotFound") { - outputChannel.appendLine(`Failed to read contents of '${uri.toString(true)}': ${error.toString()}`); + // Don't log "FileNotFound" errors, which are probably + // caused by concurrency issues, or "FileIsADirectory" + // issues, since we don't care about directories. + if (error instanceof vscode.FileSystemError && !["FileNotFound", "FileIsADirectory"].includes(error.code)) { + outputChannel.appendLine(`Failed to read contents of '${displayableUri(uri)}': ${error.toString()}`); } } } @@ -114,17 +115,9 @@ function generateDeleteFn(wsFolderUri: vscode.Uri): (doc: string) => void { docs.length = 0; api.deleteDocs(docsCopy).then((data) => { let failed = 0; + const ts = tsString(); for (const doc of data.result) { - if (doc.status != "" && !doc.status.includes("#16005:")) { - // The document was not deleted, so log the error. - // Error 16005 means we tried to delete a document - // that didn't exist. Since the purpose of this - // call was to delete the document, and at the - // end the document isn't there, we should ignore - // this error so the user doesn't get confused. - failed++; - outputChannel.appendLine(`${failed == 1 ? "\n" : ""}${doc.status}`); - } + failed += outputDelete(doc.name, doc.status, ts); } if (failed > 0) { outputChannel.show(true); @@ -151,6 +144,37 @@ export function storeTouchedByVSCode(uri: vscode.Uri): void { } } +/** Create a timestamp string for use in a log entry */ +function tsString(): string { + const date = new Date(); + return `${date.toISOString().split("T").shift()} ${date.toLocaleTimeString(undefined, { hour12: false })}`; +} + +/** Output a log entry */ +function output(docName: string, msg: string, ts?: string): void { + outputChannel.appendLine(`${ts ?? tsString()} [${docName}] ${msg}`); +} + +/** Output a log entry for a successful import */ +function outputImport(docName: string, uri: vscode.Uri): void { + output(docName, `Imported from '${displayableUri(uri)}'`); +} + +/** + * Output a log entry for a successful or failed delete. + * Does not output a log entry if the file did not exist on the server. + * Returns `1` if the deleton failed, else `0`. + */ +function outputDelete(docName: string, status: string, ts: string): number { + if (status == "") { + output(docName, "Deleted", ts); + } else if (!status.includes("#16005:")) { + output(docName, `Deletion failed: ${status}`, ts); + return 1; + } + return 0; +} + /** Create index of `wsFolder` and set up a `FileSystemWatcher` to keep the index up to date */ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Promise { if (!notIsfs(wsFolder.uri)) return; @@ -186,6 +210,10 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr // safely ignore the event. return; } + if (!uri.path.split("/").pop().includes(".")) { + // Ignore creation and change events for folders + return; + } const uriString = uri.toString(); if (!created) { const stat = await vscode.workspace.fs.stat(uri).then(undefined, () => {}); @@ -237,6 +265,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr // Create or update the document on the server importFile(change.addedOrChanged) .then(() => { + outputImport(change.addedOrChanged.name, uri); if (conf.get("compileOnSave")) { // Compile right away if this document is in the active text editor. // This is needed to avoid noticeable latency when a user is editing diff --git a/src/utils/index.ts b/src/utils/index.ts index 16acb9a4..4bec901b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -966,7 +966,7 @@ export async function getWsFolder( return vscode.window .showQuickPick( folders.map((f) => { - return { label: f.name, detail: f.uri.toString(true), f }; + return { label: f.name, detail: displayableUri(f.uri), f }; }), { canPickMany: false, @@ -1006,7 +1006,7 @@ export async function replaceFile(uri: vscode.Uri, content: string | string[] | : new TextEncoder().encode(Array.isArray(content) ? content.join("\n") : content), }); const success = await vscode.workspace.applyEdit(wsEdit); - if (!success) throw `Failed to create or replace contents of file '${uri.toString(true)}'`; + if (!success) throw `Failed to create or replace contents of file '${displayableUri(uri)}'`; } /** Show the compilation failure error message if required. */ @@ -1025,6 +1025,11 @@ export function compileErrorMsg(conf: vscode.WorkspaceConfiguration): void { }); } +/** Return a string containing the displayable form of `uri` */ +export function displayableUri(uri: vscode.Uri): string { + return uri.scheme == "file" ? uri.fsPath : uri.toString(true); +} + class Semaphore { /** Queue of tasks waiting to acquire the semaphore */ private _tasks: (() => void)[] = [];