diff --git a/src/commands/newFile.ts b/src/commands/newFile.ts index 9436e4a6..7e153daf 100644 --- a/src/commands/newFile.ts +++ b/src/commands/newFile.ts @@ -5,7 +5,7 @@ import { FILESYSTEM_SCHEMA } from "../extension"; import { DocumentContentProvider } from "../providers/DocumentContentProvider"; import { replaceFile, getWsFolder, handleError, displayableUri } from "../utils"; import { getFileName } from "./export"; -import { getUrisForDocument } from "../utils/documentIndex"; +import { getUrisForDocument, inferDocUri } from "../utils/documentIndex"; import { pickDocument } from "../utils/documentPicker"; interface InputStepItem extends vscode.QuickPickItem { @@ -236,6 +236,55 @@ function getLocalUri(cls: string, wsFolder: vscode.WorkspaceFolder): vscode.Uri return clsUri; } +/** Prompt the user for a file path using the export settings as a default */ +function promptForDocUri(cls: string, wsFolder: vscode.WorkspaceFolder): Promise { + const localUri = getLocalUri(cls, wsFolder); + return new Promise((resolve) => { + 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 (${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]; + let showingSave = false; + inputBox.onDidTriggerButton(() => { + // User wants to use the save dialog + showingSave = true; + inputBox.hide(); + vscode.window + .showSaveDialog({ + defaultUri: localUri, + filters: { + Classes: ["cls"], + }, + }) + .then( + (u) => resolve(u), + () => resolve(undefined) + ); + }); + inputBox.onDidAccept(() => { + if (typeof inputBox.validationMessage != "string") { + resolve( + wsFolder.uri.with({ + path: `${wsFolder.uri.path}${!wsFolder.uri.path.endsWith("/") ? "/" : ""}${inputBox.value.replace(/^\/+/, "")}`, + }) + ); + inputBox.hide(); + } + }); + inputBox.onDidHide(() => { + if (!showingSave) resolve(undefined); + inputBox.dispose(); + }); + inputBox.onDidChangeValue((value) => { + inputBox.validationMessage = value.endsWith(".cls") ? undefined : "File extension must be .cls"; + }); + inputBox.show(); + }); +} + /** * Check if `cls` is a valid class name. * Returns `undefined` if yes, and the reason if no. @@ -1032,52 +1081,8 @@ Class ${cls}${superclass ? ` Extends ${superclass}` : ""} // Generate the URI clsUri = DocumentContentProvider.getUri(`${cls}.cls`, undefined, undefined, undefined, wsFolder.uri); } else { - // Ask the user for the URI - const localUri = getLocalUri(cls, wsFolder); - clsUri = await new Promise((resolve) => { - 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 (${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]; - let showingSave = false; - inputBox.onDidTriggerButton(() => { - // User wants to use the save dialog - showingSave = true; - inputBox.hide(); - vscode.window - .showSaveDialog({ - defaultUri: localUri, - filters: { - Classes: ["cls"], - }, - }) - .then( - (u) => resolve(u), - () => resolve(undefined) - ); - }); - inputBox.onDidAccept(() => { - if (typeof inputBox.validationMessage != "string") { - resolve( - wsFolder.uri.with({ - path: `${wsFolder.uri.path}${!wsFolder.uri.path.endsWith("/") ? "/" : ""}${inputBox.value.replace(/^\/+/, "")}`, - }) - ); - inputBox.hide(); - } - }); - inputBox.onDidHide(() => { - if (!showingSave) resolve(undefined); - inputBox.dispose(); - }); - inputBox.onDidChangeValue((value) => { - inputBox.validationMessage = value.endsWith(".cls") ? undefined : "File extension must be .cls"; - }); - inputBox.show(); - }); + // Try to infer the URI from the document index + clsUri = inferDocUri(`${cls}.cls`, wsFolder) ?? (await promptForDocUri(cls, wsFolder)); } if (clsUri && clsContent) { diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 82c0d840..fa20c0e8 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -478,3 +478,56 @@ export function inferDocName(uri: vscode.Uri): string | undefined { } return result; } + +/** + * Use the known mappings between files and document names to infer + * a URI for a new document with name `docName` in `wsFolder`. + * For example, if the index shows that `User.Existing.cls` maps to + * `/wsFolder/src/User/Existing.cls`, then calling this with + * `User.NewClass.cls` will return a URI for `/wsFolder/src/User/NewClass.cls`. + * Returns `undefined` if an inference couldn't be made. + * + * Finds the indexed document that shares the longest package prefix + * with `docName` and uses its containing path. + */ +export function inferDocUri(docName: string, wsFolder: vscode.WorkspaceFolder): vscode.Uri | undefined { + const exts = [".cls", ".mac", ".int", ".inc"]; + const docExt = docName.slice(-4).toLowerCase(); + if (!exts.includes(docExt)) return; + const index = wsFolderIndex.get(wsFolder.uri.toString()); + if (!index || !index.uris.size) return; + const docNameNoExt = docName.slice(0, -4); // remove extension + const docPkgSegments = docNameNoExt.split(".").slice(0, -1); // remove class/routine name + // For each indexed document, compute its containing path and measure how + // closely its package matches the target document's package. Pick the one + // with the most shared leading package segments + let bestPathPrefix = ""; + let bestMatchLen = -1; + index.uris.forEach((indexDocName, indexDocUriStr) => { + const indexDocExt = indexDocName.slice(-4).toLowerCase(); + if (!exts.includes(indexDocExt)) return; + const indexDocNamePath = `/${indexDocName.slice(0, -4).replaceAll(".", "/")}${indexDocExt}`; + let indexDocFullPath = vscode.Uri.parse(indexDocUriStr).path; + indexDocFullPath = indexDocFullPath.slice(0, -3) + indexDocFullPath.slice(-3).toLowerCase(); + + if (!indexDocFullPath.endsWith(indexDocNamePath)) return; + const indexPathPrefix = indexDocFullPath.slice(0, -indexDocNamePath.length + 1); + + // Count how many leading package segments the indexed doc shares with the target + const indexPkgSegments = indexDocName.slice(0, -4).split(".").slice(0, -1); + let matchLen = 0; + while ( + matchLen < Math.min(docPkgSegments.length, indexPkgSegments.length) && + docPkgSegments[matchLen] === indexPkgSegments[matchLen] + ) { + matchLen++; + } + if (matchLen > bestMatchLen) { + bestMatchLen = matchLen; + bestPathPrefix = indexPathPrefix; + } + }); + if (!bestPathPrefix) return; + // Convert the document name to a file path and prepend the prefix + return wsFolder.uri.with({ path: `${bestPathPrefix}${docNameNoExt.replaceAll(".", "/")}${docExt}` }); +}