Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
99 changes: 52 additions & 47 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 @@ -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<vscode.Uri | undefined> {
const localUri = getLocalUri(cls, wsFolder);
return new Promise<vscode.Uri | undefined>((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.
Expand Down Expand Up @@ -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<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(/^\/+/, "")}`,
})
);
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) {
Expand Down
53 changes: 53 additions & 0 deletions src/utils/documentIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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).toLowerCase();
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;
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}` });
}
Loading