From 4f5f9825eb765bb444b8457a802fa9c00ea42884 Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Tue, 17 Feb 2026 08:36:00 -0500 Subject: [PATCH 1/7] Use document index to infer a name for a newly created client-side class or routine --- src/extension.ts | 6 ++-- src/utils/documentIndex.ts | 57 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index d1968ff9..bf34cc65 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -139,6 +139,7 @@ import { pickDocument } from "./utils/documentPicker"; import { disposeDocumentIndex, indexWorkspaceFolder, + inferDocName, removeIndexOfWorkspaceFolder, storeTouchedByVSCode, updateIndex, @@ -1411,12 +1412,13 @@ export async function activate(context: vscode.ExtensionContext): Promise { return; } // Generate the new content + const defaultName = inferDocName(uri)?.slice(0, -4); const fileExt = uri.path.split(".").pop().toLowerCase(); const newContent = fileExt == "cls" - ? ["Class $1 Extends %RegisteredObject", "{", "// $0", "}", ""] + ? [`Class \${1${defaultName ? `:${defaultName}` : ""}} Extends %RegisteredObject`, "{", "$0", "}", ""] : [ - `ROUTINE $1${fileExt == "int" ? " [Type=INT]" : fileExt == "inc" ? " [Type=INC]" : ""}`, + `ROUTINE \${1${defaultName ? `:${defaultName}` : ""}}${fileExt == "int" ? " [Type=INT]" : fileExt == "inc" ? " [Type=INC]" : ""}`, `${fileExt == "int" ? ";" : "#;"} $0`, "", ]; diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 19614fa3..a6cde485 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -22,9 +22,9 @@ import { sendClientSideSyncTelemetryEvent } from "../extension"; interface WSFolderIndex { /** The `FileSystemWatcher` for this workspace folder */ watcher: vscode.FileSystemWatcher; - /** Map of document names (i.e., server-side names) to VSCode URIs */ + /** Map of document names (i.e., server-side names) to VS Code URIs */ documents: Map; - /** Map of VSCode URIs to document names */ + /** Map of VS Code URIs to document names */ uris: Map; } @@ -426,3 +426,56 @@ export function allDocumentsInWorkspace(wsFolder: vscode.WorkspaceFolder): strin export function getDocumentForUri(uri: vscode.Uri): string { return wsFolderIndex.get(vscode.workspace.getWorkspaceFolder(uri)?.uri.toString())?.uris.get(uri.toString()); } + +/** + * Use the known mappings between files and document names to infer + * a name for a document contained in file `uri`. For example, + * `uri` with path `/wsFolder/src/User/Test.cls` may return + * `User.Test.cls`. Returns `undefined` if an inference couldn't + * be made. Only attempts inferencing for classes or routines. + * Does not attempt to read `uri`. This is useful for + * generating stub content for a file that was just created. + */ +export function inferDocName(uri: vscode.Uri): string | undefined { + const exts = [".cls", ".mac", ".int", ".inc"]; + const fileExt = uri.path.slice(-4).toLowerCase(); + if (!exts.includes(fileExt)) return; + const wsFolder = vscode.workspace.getWorkspaceFolder(uri); + if (!wsFolder) return; + const index = wsFolderIndex.get(wsFolder.uri.toString()); + if (!index) return; + // Convert the URI into an array of path segments + const uriParts = uri.path.split("/"); + uriParts.pop(); + // Stop looping once we reach the workspace folder root + const loopEnd = wsFolder.uri.path.split("/").length - (wsFolder.uri.path.endsWith("/") ? 1 : 0); + // Look for known documents in the same directory tree as the target URI. + // Once we find a match, look at the relationship between the URI and name + // and apply that same relationship to the target URI. Start at the containing + // directory of the target and then work up the tree until we have a match. + let result: string; + for (let i = uriParts.length; i >= loopEnd; i--) { + const uriDir = `${uriParts.slice(0, i).join("/")}/`; + for (const [docUriStr, docName] of index.uris) { + const docUri = vscode.Uri.parse(docUriStr); + if (exts.includes(docName.slice(-4)) && docUri.path.startsWith(uriDir)) { + // This class or routine is in the same directory tree as the target + // so attempt to determine how its name relates to its URI + const docNamePath = `/${docName.slice(0, -4).replaceAll(".", "/")}${docName.slice(-4)}`; + // Make sure the file extension is lowercased in the path before matching + const startOfDocName = (docUri.path.slice(0, -3) + docUri.path.slice(-3).toLowerCase()).lastIndexOf( + docNamePath + ); + if (startOfDocName > -1) { + // We've identified the leading path segments that don't contribute to the document name, + // so remove them from the target URI before generating the document name. Need the + 1 to + // remove the leading slash which was part of the match string. + result = `${uri.path.slice(startOfDocName + 1, -4).replaceAll("/", ".")}${fileExt}`; + break; + } + } + } + if (result) break; + } + return result; +} From 25ef372b55dfca35dc0d4ef94fa0443026b4571f Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Thu, 19 Feb 2026 07:00:26 -0500 Subject: [PATCH 2/7] Update documentIndex.ts --- src/utils/documentIndex.ts | 56 ++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index a6cde485..5efe9240 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -443,39 +443,35 @@ export function inferDocName(uri: vscode.Uri): string | undefined { const wsFolder = vscode.workspace.getWorkspaceFolder(uri); if (!wsFolder) return; const index = wsFolderIndex.get(wsFolder.uri.toString()); - if (!index) return; - // Convert the URI into an array of path segments - const uriParts = uri.path.split("/"); - uriParts.pop(); - // Stop looping once we reach the workspace folder root - const loopEnd = wsFolder.uri.path.split("/").length - (wsFolder.uri.path.endsWith("/") ? 1 : 0); - // Look for known documents in the same directory tree as the target URI. - // Once we find a match, look at the relationship between the URI and name - // and apply that same relationship to the target URI. Start at the containing - // directory of the target and then work up the tree until we have a match. - let result: string; - for (let i = uriParts.length; i >= loopEnd; i--) { - const uriDir = `${uriParts.slice(0, i).join("/")}/`; - for (const [docUriStr, docName] of index.uris) { + if (!index || !index.uris.size) return; + // Get a list of all unique paths containing classes or routines that + // do not contribute to the name of the documents contained within + const containingPaths: Set = new Set(); + index.uris.forEach((docName, docUriStr) => { + const docNameExt = docName.slice(-4); + if (exts.includes(docNameExt)) { const docUri = vscode.Uri.parse(docUriStr); - if (exts.includes(docName.slice(-4)) && docUri.path.startsWith(uriDir)) { - // This class or routine is in the same directory tree as the target - // so attempt to determine how its name relates to its URI - const docNamePath = `/${docName.slice(0, -4).replaceAll(".", "/")}${docName.slice(-4)}`; - // Make sure the file extension is lowercased in the path before matching - const startOfDocName = (docUri.path.slice(0, -3) + docUri.path.slice(-3).toLowerCase()).lastIndexOf( - docNamePath - ); - if (startOfDocName > -1) { - // We've identified the leading path segments that don't contribute to the document name, - // so remove them from the target URI before generating the document name. Need the + 1 to - // remove the leading slash which was part of the match string. - result = `${uri.path.slice(startOfDocName + 1, -4).replaceAll("/", ".")}${fileExt}`; - break; - } + // This entry is for a class or routine so see if its name and file system path match + const docNamePath = `/${docName.slice(0, -4).replaceAll(".", "/")}${docNameExt}`; + // Make sure the file extension is lowercased in the path before matching + const startOfDocName = (docUri.path.slice(0, -3) + docUri.path.slice(-3).toLowerCase()).lastIndexOf(docNamePath); + if (startOfDocName > -1) { + // The document name is the trailing substring of the file system path with different delimiters + containingPaths.add(docUri.path.slice(0, startOfDocName + 1)); } } - if (result) break; + }); + if (!containingPaths.size) return; // We couldn't learn anyhting from the documents in the index + // Sort the values in the Set by number of segments descending so we check the longest paths first + const containingPathsSorted = Array.from(containingPaths).sort((a, b) => b.split("/").length - a.split("/").length); + let result: string; + for (const prefix of containingPathsSorted) { + if (uri.path.startsWith(prefix)) { + // We've identified the leading path segments that don't contribute to the document + // name, so remove them from the target URI before generating the document name + result = `${uri.path.slice(prefix.length, -4).replaceAll("/", ".")}${fileExt}`; + break; + } } return result; } From 38f8e459463eb7e15349fc594fa12737eb7702e2 Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Thu, 19 Feb 2026 08:18:05 -0500 Subject: [PATCH 3/7] Update documentIndex.ts --- src/utils/documentIndex.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 5efe9240..0e63050c 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -448,11 +448,11 @@ export function inferDocName(uri: vscode.Uri): string | undefined { // do not contribute to the name of the documents contained within const containingPaths: Set = new Set(); index.uris.forEach((docName, docUriStr) => { - const docNameExt = docName.slice(-4); - if (exts.includes(docNameExt)) { + const docExt = docName.slice(-4); + if (exts.includes(docExt)) { const docUri = vscode.Uri.parse(docUriStr); // This entry is for a class or routine so see if its name and file system path match - const docNamePath = `/${docName.slice(0, -4).replaceAll(".", "/")}${docNameExt}`; + const docNamePath = `/${docName.slice(0, -4).replaceAll(".", "/")}${docExt}`; // Make sure the file extension is lowercased in the path before matching const startOfDocName = (docUri.path.slice(0, -3) + docUri.path.slice(-3).toLowerCase()).lastIndexOf(docNamePath); if (startOfDocName > -1) { @@ -461,8 +461,8 @@ export function inferDocName(uri: vscode.Uri): string | undefined { } } }); - if (!containingPaths.size) return; // We couldn't learn anyhting from the documents in the index - // Sort the values in the Set by number of segments descending so we check the longest paths first + if (!containingPaths.size) return; // We couldn't learn anything from the documents in the index + // Sort the values in the Set by number of segments descending so we check the deepest paths first const containingPathsSorted = Array.from(containingPaths).sort((a, b) => b.split("/").length - a.split("/").length); let result: string; for (const prefix of containingPathsSorted) { From d56ffb2981cebbd34fbc5ed435b33c1c1f775583 Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Thu, 19 Feb 2026 16:08:43 -0500 Subject: [PATCH 4/7] Update documentIndex.ts --- src/utils/documentIndex.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 0e63050c..12609811 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -450,14 +450,14 @@ export function inferDocName(uri: vscode.Uri): string | undefined { index.uris.forEach((docName, docUriStr) => { const docExt = docName.slice(-4); if (exts.includes(docExt)) { - const docUri = vscode.Uri.parse(docUriStr); // This entry is for a class or routine so see if its name and file system path match const docNamePath = `/${docName.slice(0, -4).replaceAll(".", "/")}${docExt}`; // Make sure the file extension is lowercased in the path before matching - const startOfDocName = (docUri.path.slice(0, -3) + docUri.path.slice(-3).toLowerCase()).lastIndexOf(docNamePath); - if (startOfDocName > -1) { + let fullPath = vscode.Uri.parse(docUriStr).path; + fullPath = fullPath.slice(0, -3) + fullPath.slice(-3).toLowerCase(); + if (fullPath.endsWith(docNamePath)) { // The document name is the trailing substring of the file system path with different delimiters - containingPaths.add(docUri.path.slice(0, startOfDocName + 1)); + containingPaths.add(fullPath.slice(0, -docNamePath.length + 1)); } } }); From a22417839962ccbcae667d240f032821d11fb8b6 Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Thu, 19 Feb 2026 16:15:18 -0500 Subject: [PATCH 5/7] Update documentIndex.ts --- src/utils/documentIndex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 12609811..c89c2c54 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -463,7 +463,7 @@ export function inferDocName(uri: vscode.Uri): string | undefined { }); if (!containingPaths.size) return; // We couldn't learn anything from the documents in the index // Sort the values in the Set by number of segments descending so we check the deepest paths first - const containingPathsSorted = Array.from(containingPaths).sort((a, b) => b.split("/").length - a.split("/").length); + const containingPathsSorted = Array.from(containingPaths).sort((a, b) => b.length - a.length); let result: string; for (const prefix of containingPathsSorted) { if (uri.path.startsWith(prefix)) { From f8612ba1f613bce937281b206a834c3b295beecf Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Thu, 19 Feb 2026 16:20:17 -0500 Subject: [PATCH 6/7] Update documentIndex.ts --- src/utils/documentIndex.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index c89c2c54..f0cac578 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -453,11 +453,11 @@ export function inferDocName(uri: vscode.Uri): string | undefined { // This entry is for a class or routine so see if its name and file system path match const docNamePath = `/${docName.slice(0, -4).replaceAll(".", "/")}${docExt}`; // Make sure the file extension is lowercased in the path before matching - let fullPath = vscode.Uri.parse(docUriStr).path; - fullPath = fullPath.slice(0, -3) + fullPath.slice(-3).toLowerCase(); - if (fullPath.endsWith(docNamePath)) { + let docFullPath = vscode.Uri.parse(docUriStr).path; + docFullPath = docFullPath.slice(0, -3) + docFullPath.slice(-3).toLowerCase(); + if (docFullPath.endsWith(docNamePath)) { // The document name is the trailing substring of the file system path with different delimiters - containingPaths.add(fullPath.slice(0, -docNamePath.length + 1)); + containingPaths.add(docFullPath.slice(0, -docNamePath.length + 1)); } } }); From 081c6726cdf5456c5247aeef3957c369e67ddcd9 Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Thu, 19 Feb 2026 16:32:18 -0500 Subject: [PATCH 7/7] Update documentIndex.ts --- src/utils/documentIndex.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index f0cac578..82c0d840 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -462,7 +462,10 @@ export function inferDocName(uri: vscode.Uri): string | undefined { } }); if (!containingPaths.size) return; // We couldn't learn anything from the documents in the index - // Sort the values in the Set by number of segments descending so we check the deepest paths first + // Sort the values in the Set by length descending so we check child directories before their parents. + // This ensures that we use the mapping of a document that is as close a neighbor as possible. This + // is necessary for the rare situaions for documents in /foo/bar/ have a different mapping than /foo/ + // and the target URI is in /foo/bar/ or a subfolder of it. const containingPathsSorted = Array.from(containingPaths).sort((a, b) => b.length - a.length); let result: string; for (const prefix of containingPathsSorted) {