Skip to content

Commit f0e8c0b

Browse files
authored
Infer doc URI based on doc name for the New File command (#1741)
* Infer doc URI based on doc name for the New File command * Refactoring
1 parent 7bf7120 commit f0e8c0b

File tree

2 files changed

+105
-47
lines changed

2 files changed

+105
-47
lines changed

src/commands/newFile.ts

Lines changed: 52 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { FILESYSTEM_SCHEMA } from "../extension";
55
import { DocumentContentProvider } from "../providers/DocumentContentProvider";
66
import { replaceFile, getWsFolder, handleError, displayableUri } from "../utils";
77
import { getFileName } from "./export";
8-
import { getUrisForDocument } from "../utils/documentIndex";
8+
import { getUrisForDocument, inferDocUri } from "../utils/documentIndex";
99
import { pickDocument } from "../utils/documentPicker";
1010

1111
interface InputStepItem extends vscode.QuickPickItem {
@@ -236,6 +236,55 @@ function getLocalUri(cls: string, wsFolder: vscode.WorkspaceFolder): vscode.Uri
236236
return clsUri;
237237
}
238238

239+
/** Prompt the user for a file path using the export settings as a default */
240+
function promptForDocUri(cls: string, wsFolder: vscode.WorkspaceFolder): Promise<vscode.Uri | undefined> {
241+
const localUri = getLocalUri(cls, wsFolder);
242+
return new Promise<vscode.Uri | undefined>((resolve) => {
243+
const inputBox = vscode.window.createInputBox();
244+
inputBox.ignoreFocusOut = true;
245+
inputBox.buttons = [{ iconPath: new vscode.ThemeIcon("save-as"), tooltip: "Show 'Save As' dialog" }];
246+
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.`;
247+
inputBox.title = "Enter a file path for the new class";
248+
inputBox.value = localUri.path.slice(wsFolder.uri.path.length);
249+
inputBox.valueSelection = [inputBox.value.length, inputBox.value.length];
250+
let showingSave = false;
251+
inputBox.onDidTriggerButton(() => {
252+
// User wants to use the save dialog
253+
showingSave = true;
254+
inputBox.hide();
255+
vscode.window
256+
.showSaveDialog({
257+
defaultUri: localUri,
258+
filters: {
259+
Classes: ["cls"],
260+
},
261+
})
262+
.then(
263+
(u) => resolve(u),
264+
() => resolve(undefined)
265+
);
266+
});
267+
inputBox.onDidAccept(() => {
268+
if (typeof inputBox.validationMessage != "string") {
269+
resolve(
270+
wsFolder.uri.with({
271+
path: `${wsFolder.uri.path}${!wsFolder.uri.path.endsWith("/") ? "/" : ""}${inputBox.value.replace(/^\/+/, "")}`,
272+
})
273+
);
274+
inputBox.hide();
275+
}
276+
});
277+
inputBox.onDidHide(() => {
278+
if (!showingSave) resolve(undefined);
279+
inputBox.dispose();
280+
});
281+
inputBox.onDidChangeValue((value) => {
282+
inputBox.validationMessage = value.endsWith(".cls") ? undefined : "File extension must be .cls";
283+
});
284+
inputBox.show();
285+
});
286+
}
287+
239288
/**
240289
* Check if `cls` is a valid class name.
241290
* Returns `undefined` if yes, and the reason if no.
@@ -1032,52 +1081,8 @@ Class ${cls}${superclass ? ` Extends ${superclass}` : ""}
10321081
// Generate the URI
10331082
clsUri = DocumentContentProvider.getUri(`${cls}.cls`, undefined, undefined, undefined, wsFolder.uri);
10341083
} else {
1035-
// Ask the user for the URI
1036-
const localUri = getLocalUri(cls, wsFolder);
1037-
clsUri = await new Promise<vscode.Uri>((resolve) => {
1038-
const inputBox = vscode.window.createInputBox();
1039-
inputBox.ignoreFocusOut = true;
1040-
inputBox.buttons = [{ iconPath: new vscode.ThemeIcon("save-as"), tooltip: "Show 'Save As' dialog" }];
1041-
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.`;
1042-
inputBox.title = "Enter a file path for the new class";
1043-
inputBox.value = localUri.path.slice(wsFolder.uri.path.length);
1044-
inputBox.valueSelection = [inputBox.value.length, inputBox.value.length];
1045-
let showingSave = false;
1046-
inputBox.onDidTriggerButton(() => {
1047-
// User wants to use the save dialog
1048-
showingSave = true;
1049-
inputBox.hide();
1050-
vscode.window
1051-
.showSaveDialog({
1052-
defaultUri: localUri,
1053-
filters: {
1054-
Classes: ["cls"],
1055-
},
1056-
})
1057-
.then(
1058-
(u) => resolve(u),
1059-
() => resolve(undefined)
1060-
);
1061-
});
1062-
inputBox.onDidAccept(() => {
1063-
if (typeof inputBox.validationMessage != "string") {
1064-
resolve(
1065-
wsFolder.uri.with({
1066-
path: `${wsFolder.uri.path}${!wsFolder.uri.path.endsWith("/") ? "/" : ""}${inputBox.value.replace(/^\/+/, "")}`,
1067-
})
1068-
);
1069-
inputBox.hide();
1070-
}
1071-
});
1072-
inputBox.onDidHide(() => {
1073-
if (!showingSave) resolve(undefined);
1074-
inputBox.dispose();
1075-
});
1076-
inputBox.onDidChangeValue((value) => {
1077-
inputBox.validationMessage = value.endsWith(".cls") ? undefined : "File extension must be .cls";
1078-
});
1079-
inputBox.show();
1080-
});
1084+
// Try to infer the URI from the document index
1085+
clsUri = inferDocUri(`${cls}.cls`, wsFolder) ?? (await promptForDocUri(cls, wsFolder));
10811086
}
10821087

10831088
if (clsUri && clsContent) {

src/utils/documentIndex.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,3 +478,56 @@ export function inferDocName(uri: vscode.Uri): string | undefined {
478478
}
479479
return result;
480480
}
481+
482+
/**
483+
* Use the known mappings between files and document names to infer
484+
* a URI for a new document with name `docName` in `wsFolder`.
485+
* For example, if the index shows that `User.Existing.cls` maps to
486+
* `/wsFolder/src/User/Existing.cls`, then calling this with
487+
* `User.NewClass.cls` will return a URI for `/wsFolder/src/User/NewClass.cls`.
488+
* Returns `undefined` if an inference couldn't be made.
489+
*
490+
* Finds the indexed document that shares the longest package prefix
491+
* with `docName` and uses its containing path.
492+
*/
493+
export function inferDocUri(docName: string, wsFolder: vscode.WorkspaceFolder): vscode.Uri | undefined {
494+
const exts = [".cls", ".mac", ".int", ".inc"];
495+
const docExt = docName.slice(-4).toLowerCase();
496+
if (!exts.includes(docExt)) return;
497+
const index = wsFolderIndex.get(wsFolder.uri.toString());
498+
if (!index || !index.uris.size) return;
499+
const docNameNoExt = docName.slice(0, -4); // remove extension
500+
const docPkgSegments = docNameNoExt.split(".").slice(0, -1); // remove class/routine name
501+
// For each indexed document, compute its containing path and measure how
502+
// closely its package matches the target document's package. Pick the one
503+
// with the most shared leading package segments
504+
let bestPathPrefix = "";
505+
let bestMatchLen = -1;
506+
index.uris.forEach((indexDocName, indexDocUriStr) => {
507+
const indexDocExt = indexDocName.slice(-4).toLowerCase();
508+
if (!exts.includes(indexDocExt)) return;
509+
const indexDocNamePath = `/${indexDocName.slice(0, -4).replaceAll(".", "/")}${indexDocExt}`;
510+
let indexDocFullPath = vscode.Uri.parse(indexDocUriStr).path;
511+
indexDocFullPath = indexDocFullPath.slice(0, -3) + indexDocFullPath.slice(-3).toLowerCase();
512+
513+
if (!indexDocFullPath.endsWith(indexDocNamePath)) return;
514+
const indexPathPrefix = indexDocFullPath.slice(0, -indexDocNamePath.length + 1);
515+
516+
// Count how many leading package segments the indexed doc shares with the target
517+
const indexPkgSegments = indexDocName.slice(0, -4).split(".").slice(0, -1);
518+
let matchLen = 0;
519+
while (
520+
matchLen < Math.min(docPkgSegments.length, indexPkgSegments.length) &&
521+
docPkgSegments[matchLen] === indexPkgSegments[matchLen]
522+
) {
523+
matchLen++;
524+
}
525+
if (matchLen > bestMatchLen) {
526+
bestMatchLen = matchLen;
527+
bestPathPrefix = indexPathPrefix;
528+
}
529+
});
530+
if (!bestPathPrefix) return;
531+
// Convert the document name to a file path and prepend the prefix
532+
return wsFolder.uri.with({ path: `${bestPathPrefix}${docNameNoExt.replaceAll(".", "/")}${docExt}` });
533+
}

0 commit comments

Comments
 (0)