From 0dee850c75ea3ed663013e5b5051cdba0063d4dd Mon Sep 17 00:00:00 2001 From: Keith Cressman Date: Tue, 31 Mar 2026 14:28:57 -0400 Subject: [PATCH 1/2] Infer doc URI based on doc name for the New File command --- src/commands/newFile.ts | 97 ++++++++++++++++++++------------------ src/utils/documentIndex.ts | 56 ++++++++++++++++++++++ 2 files changed, 108 insertions(+), 45 deletions(-) diff --git a/src/commands/newFile.ts b/src/commands/newFile.ts index 9436e4a6..ba7332fd 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 { @@ -1032,52 +1032,59 @@ 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(/^\/+/, "")}`, - }) - ); + // Try to infer the URI from the document index + const inferredUri = inferDocUri(`${cls}.cls`, wsFolder); + if (inferredUri) { + // Use the inferred URI directly without prompting + clsUri = inferredUri; + } else { + // Fall back to objectscript.export settings and prompt the user + 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(); - } - }); - inputBox.onDidHide(() => { - if (!showingSave) resolve(undefined); - inputBox.dispose(); - }); - inputBox.onDidChangeValue((value) => { - inputBox.validationMessage = value.endsWith(".cls") ? undefined : "File extension must be .cls"; + 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(); }); - inputBox.show(); - }); + } } if (clsUri && clsContent) { diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 82c0d840..ce2dc50a 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -478,3 +478,59 @@ 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(docName.lastIndexOf(".")); + 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); + 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; + for (let i = 0; i < Math.min(docPkgSegments.length, indexPkgSegments.length); i++) { + if (docPkgSegments[i] === indexPkgSegments[i]) { + matchLen++; + } else { + break; + } + } + if (matchLen > bestMatchLen) { + bestMatchLen = matchLen; + bestPathPrefix = indexPathPrefix; + } + }); + if (!bestPathPrefix) return; + // Convert the document name to a file path and prepend the prefix + const docNamePath = `${docNameNoExt.replaceAll(".", "/")}${docExt}`; + const inferredPath = `${bestPathPrefix}${docNamePath}`; + return wsFolder.uri.with({ path: inferredPath }); +} From 0ce6897e3a998479e6a59c5aeecab2dbe57e925b Mon Sep 17 00:00:00 2001 From: Keith Cressman Date: Tue, 31 Mar 2026 17:58:30 -0400 Subject: [PATCH 2/2] Refactoring --- src/commands/newFile.ts | 102 ++++++++++++++++++------------------- src/utils/documentIndex.ts | 19 +++---- 2 files changed, 58 insertions(+), 63 deletions(-) diff --git a/src/commands/newFile.ts b/src/commands/newFile.ts index ba7332fd..7e153daf 100644 --- a/src/commands/newFile.ts +++ b/src/commands/newFile.ts @@ -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. @@ -1033,58 +1082,7 @@ Class ${cls}${superclass ? ` Extends ${superclass}` : ""} clsUri = DocumentContentProvider.getUri(`${cls}.cls`, undefined, undefined, undefined, wsFolder.uri); } else { // Try to infer the URI from the document index - const inferredUri = inferDocUri(`${cls}.cls`, wsFolder); - if (inferredUri) { - // Use the inferred URI directly without prompting - clsUri = inferredUri; - } else { - // Fall back to objectscript.export settings and prompt the user - 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(); - }); - } + 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 ce2dc50a..fa20c0e8 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -492,7 +492,7 @@ export function inferDocName(uri: vscode.Uri): string | undefined { */ export function inferDocUri(docName: string, wsFolder: vscode.WorkspaceFolder): vscode.Uri | undefined { const exts = [".cls", ".mac", ".int", ".inc"]; - const docExt = docName.slice(docName.lastIndexOf(".")); + const docExt = docName.slice(-4).toLowerCase(); if (!exts.includes(docExt)) return; const index = wsFolderIndex.get(wsFolder.uri.toString()); if (!index || !index.uris.size) return; @@ -504,7 +504,7 @@ export function inferDocUri(docName: string, wsFolder: vscode.WorkspaceFolder): let bestPathPrefix = ""; let bestMatchLen = -1; index.uris.forEach((indexDocName, indexDocUriStr) => { - const indexDocExt = indexDocName.slice(-4); + 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; @@ -516,12 +516,11 @@ export function inferDocUri(docName: string, wsFolder: vscode.WorkspaceFolder): // 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; - for (let i = 0; i < Math.min(docPkgSegments.length, indexPkgSegments.length); i++) { - if (docPkgSegments[i] === indexPkgSegments[i]) { - matchLen++; - } else { - break; - } + while ( + matchLen < Math.min(docPkgSegments.length, indexPkgSegments.length) && + docPkgSegments[matchLen] === indexPkgSegments[matchLen] + ) { + matchLen++; } if (matchLen > bestMatchLen) { bestMatchLen = matchLen; @@ -530,7 +529,5 @@ export function inferDocUri(docName: string, wsFolder: vscode.WorkspaceFolder): }); if (!bestPathPrefix) return; // Convert the document name to a file path and prepend the prefix - const docNamePath = `${docNameNoExt.replaceAll(".", "/")}${docExt}`; - const inferredPath = `${bestPathPrefix}${docNamePath}`; - return wsFolder.uri.with({ path: inferredPath }); + return wsFolder.uri.with({ path: `${bestPathPrefix}${docNameNoExt.replaceAll(".", "/")}${docExt}` }); }