Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
97 changes: 52 additions & 45 deletions src/commands/newFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<vscode.Uri>((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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest extracting the prompt logic into a helper function to make the control flow clearer and easier to follow.

clsUri = inferDocUri(`${cls}.cls`, wsFolder) ?? promptDocUri(...);

const localUri = getLocalUri(cls, wsFolder);
clsUri = await new Promise<vscode.Uri>((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) {
Expand Down
56 changes: 56 additions & 0 deletions src/utils/documentIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("."));
Copy link
Copy Markdown
Contributor

@isc-bsaviano isc-bsaviano Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can just do slice(-4)

if (!exts.includes(docExt)) return;
Comment thread
isc-klu marked this conversation as resolved.
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;
Comment thread
isc-klu marked this conversation as resolved.
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);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of lines 507–514 appears to be computing indexPathPrefix—the directory prefix for a .cls file. This logic seems broadly reusable, and it might be beneficial to extract it into a helper function.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to leave it as is given that it's only a few lines, is kind of an abstract operation, has cases where the closure needs to exit, and I think currently would be only be reusable in the inferDocName method


// 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++) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need both matchLen and i. I would remove matchLen and increment i instead.

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 });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These last three lines can be condensed into one by removing the intermediate const declarations. It's a purely stylistic change you can reject though.

}
Loading