Skip to content

Commit ad4ed0e

Browse files
authored
Merge pull request #4 from consistem/update-multi-root-search-in-documentcontentprovider
feat: enable cross-workspace definition lookup
2 parents ffc47fe + 45b7b82 commit ad4ed0e

File tree

10 files changed

+227
-32
lines changed

10 files changed

+227
-32
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change Log
22

3+
## [Unreleased]
4+
- Enhancements
5+
- Allow configuring cross-workspace Go to Definition lookups via `objectscript.export.searchOtherWorkspaceFolders` so local sources in sibling workspace folders are resolved before falling back to the server.
6+
37
## [3.0.6] 09-Sep-2025
48
- Enhancements
59
- Add `objectscript.unitTest.enabled` setting (#1627)

README.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,29 +48,51 @@ Open VS Code. Go to Extensions view (<kbd>⌘</kbd>/<kbd>Ctrl</kbd>+<kbd>Shift</
4848
This extension is able to to take advantage of some VS Code APIs that have not yet been finalized.
4949

5050
The additional features (and the APIs used) are:
51+
5152
- Server-side [searching across files](https://code.visualstudio.com/docs/editor/codebasics#_search-across-files) being accessed using isfs (_TextSearchProvider_)
5253
- [Quick Open](https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_quick-open) of isfs files (_FileSearchProvider_).
5354

5455
To unlock these features (optional):
5556

5657
1. Download and install a beta version from GitHub. This is necessary because Marketplace does not allow publication of extensions that use proposed APIs.
57-
- Go to https://github.com/intersystems-community/vscode-objectscript/releases
58-
- Locate the beta immediately above the release you installed from Marketplace. For instance, if you installed `3.0.6`, look for `3.0.7-beta.1`. This will be functionally identical to the Marketplace version apart from being able to use proposed APIs.
59-
- Download the VSIX file (for example `vscode-objectscript-3.0.7-beta.1.vsix`) and install it. One way to install a VSIX is to drag it from your download folder and drop it onto the list of extensions in the Extensions view of VS Code.
58+
59+
- Go to https://github.com/intersystems-community/vscode-objectscript/releases
60+
- Locate the beta immediately above the release you installed from Marketplace. For instance, if you installed `3.0.6`, look for `3.0.7-beta.1`. This will be functionally identical to the Marketplace version apart from being able to use proposed APIs.
61+
- Download the VSIX file (for example `vscode-objectscript-3.0.7-beta.1.vsix`) and install it. One way to install a VSIX is to drag it from your download folder and drop it onto the list of extensions in the Extensions view of VS Code.
6062

6163
2. From [Command Palette](https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_command-palette) choose `Preferences: Configure Runtime Arguments`.
6264
3. In the argv.json file that opens, add this line (required for both Stable and Insiders versions of VS Code):
65+
6366
```json
6467
"enable-proposed-api": ["intersystems-community.vscode-objectscript"]
6568
```
69+
6670
4. Exit VS Code and relaunch it.
6771
5. Verify that the ObjectScript channel of the Output panel reports this:
72+
6873
```
6974
intersystems-community.vscode-objectscript version X.Y.Z-beta.1 activating with proposed APIs available.
7075
```
7176

7277
After a subsequent update of the extension from Marketplace you will only have to download and install the new `vscode-objectscript-X.Y.Z-beta.1` VSIX. None of the other steps above are needed again.
7378

79+
## Cross-workspace Go to Definition
80+
81+
> **Implementation developed and maintained by Consistem Sistemas**
82+
83+
When working in a multi-root workspace, the extension normally searches the current workspace folder (and any sibling folders connected to the same namespace) for local copies of ObjectScript code before requesting the server version. If you keep shared source code in other workspace folders with different connection settings, set the `objectscript.export.searchOtherWorkspaceFolders` array in the consuming folder's settings so those folders are considered first. Use workspace-folder names, or specify `"*"` to search every non-`isfs` folder.
84+
85+
```json
86+
{
87+
"objectscript.export": {
88+
"folder": "src",
89+
"searchOtherWorkspaceFolders": ["shared"]
90+
}
91+
}
92+
```
93+
94+
With this setting enabled, features such as Go to Definition resolve to the first matching local file across the configured workspace folders before falling back to the server copy.
95+
7496
## Notes
7597

7698
- Connection-related output appears in the 'Output' view while switched to the 'ObjectScript' channel using the drop-down menu on the view titlebar.

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,6 +1396,14 @@
13961396
},
13971397
"additionalProperties": false
13981398
},
1399+
"searchOtherWorkspaceFolders": {
1400+
"markdownDescription": "Additional workspace folders to search for client-side sources when resolving ObjectScript documents. Specify `\"*\"` to search all non-isfs workspace folders in the current multi-root workspace before falling back to the server.",
1401+
"type": "array",
1402+
"items": {
1403+
"type": "string"
1404+
},
1405+
"default": []
1406+
},
13991407
"atelier": {
14001408
"description": "Export source code as Atelier did it, with packages as subfolders. This setting only affects classes, routines, include files and DFI files.",
14011409
"type": "boolean"

src/providers/DocumentContentProvider.ts

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -165,30 +165,73 @@ export class DocumentContentProvider implements vscode.TextDocumentContentProvid
165165
});
166166
}
167167
} else {
168-
const conn = config("conn", workspaceFolder);
168+
const conn = config("conn", workspaceFolder) ?? {};
169+
const exportConfig =
170+
workspaceFolder && workspaceFolder !== ""
171+
? (config("export", workspaceFolder) as { searchOtherWorkspaceFolders?: string[] })
172+
: undefined;
173+
const searchOtherWorkspaceFolders = Array.isArray(exportConfig?.searchOtherWorkspaceFolders)
174+
? exportConfig.searchOtherWorkspaceFolders
175+
.map((value) => (typeof value === "string" ? value.trim() : ""))
176+
.filter((value) => value.length > 0)
177+
: [];
178+
const includeAllFolders = searchOtherWorkspaceFolders.includes("*");
179+
const explicitAdditionalFolders = new Set(
180+
searchOtherWorkspaceFolders.filter((value) => value !== "*").map((value) => value.toLowerCase())
181+
);
169182
if (!forceServerCopy) {
170-
// Look for the document in the local file system
171-
const localFile = this.findLocalUri(name, workspaceFolder);
172-
if (localFile && (!namespace || namespace === conn.ns)) {
173-
// Exists as a local file and we aren't viewing a different namespace on the same server,
174-
// so return a uri that will open the local file.
183+
const tryLocalUri = (folderName: string, allowNamespaceMismatch: boolean): vscode.Uri => {
184+
const localFile = this.findLocalUri(name, folderName);
185+
if (!localFile) return;
186+
if (!allowNamespaceMismatch && namespace) {
187+
const folderConn = config("conn", folderName) ?? {};
188+
if (folderConn.ns && namespace !== folderConn.ns) {
189+
return;
190+
}
191+
}
175192
return localFile;
176-
} else {
177-
// The local file doesn't exist in this folder, so check any other
178-
// local folders in this workspace if it's a multi-root workspace
179-
const wFolders = vscode.workspace.workspaceFolders;
180-
if (wFolders && wFolders.length > 1) {
181-
// This is a multi-root workspace
193+
};
194+
195+
// Look for the document in the local file system
196+
const primaryLocal = tryLocalUri(workspaceFolder, false);
197+
if (primaryLocal) {
198+
return primaryLocal;
199+
}
200+
201+
// Check any other eligible local folders in this workspace if it's a multi-root workspace
202+
const wFolders = vscode.workspace.workspaceFolders;
203+
if (wFolders && wFolders.length > 1 && workspaceFolder) {
204+
const candidates: { folder: vscode.WorkspaceFolder; allowNamespaceMismatch: boolean }[] = [];
205+
const seen = new Set<string>();
206+
const addCandidate = (folder: vscode.WorkspaceFolder, allowNamespaceMismatch: boolean): void => {
207+
if (!notIsfs(folder.uri)) return;
208+
if (folder.name === workspaceFolder) return;
209+
if (seen.has(folder.name)) return;
210+
candidates.push({ folder, allowNamespaceMismatch });
211+
seen.add(folder.name);
212+
};
213+
214+
for (const wFolder of wFolders) {
215+
if (wFolder.name === workspaceFolder) continue;
216+
const wFolderConn = config("conn", wFolder.name) ?? {};
217+
if (compareConns(conn, wFolderConn) && (!namespace || namespace === wFolderConn.ns)) {
218+
addCandidate(wFolder, false);
219+
}
220+
}
221+
222+
if (includeAllFolders || explicitAdditionalFolders.size > 0) {
182223
for (const wFolder of wFolders) {
183-
if (notIsfs(wFolder.uri) && wFolder.name != workspaceFolder) {
184-
// This isn't the folder that we checked originally
185-
const wFolderConn = config("conn", wFolder.name);
186-
if (compareConns(conn, wFolderConn) && (!namespace || namespace === wFolderConn.ns)) {
187-
// This folder is connected to the same server:ns combination as the original folder
188-
const wFolderFile = this.findLocalUri(name, wFolder.name);
189-
if (wFolderFile) return wFolderFile;
190-
}
191-
}
224+
if (wFolder.name === workspaceFolder) continue;
225+
const shouldInclude = includeAllFolders || explicitAdditionalFolders.has(wFolder.name.toLowerCase());
226+
if (!shouldInclude) continue;
227+
addCandidate(wFolder, true);
228+
}
229+
}
230+
231+
for (const candidate of candidates) {
232+
const candidateLocal = tryLocalUri(candidate.folder.name, candidate.allowNamespaceMismatch);
233+
if (candidateLocal) {
234+
return candidateLocal;
192235
}
193236
}
194237
}

src/test/suite/extension.test.ts

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,99 @@
11
import * as assert from "assert";
22
import { before } from "mocha";
3+
import * as path from "path";
34

45
// You can import and use all API from the 'vscode' module
56
// as well as import your extension to test it
6-
import { window, extensions } from "vscode";
7-
import { extensionId, smExtensionId } from "../../extension";
7+
import * as vscode from "vscode";
8+
import { extensionId, smExtensionId, OBJECTSCRIPT_FILE_SCHEMA } from "../../extension";
9+
import { getUrisForDocument } from "../../utils/documentIndex";
10+
11+
async function waitForIndexedDocument(documentName: string, workspaceFolderName: string): Promise<void> {
12+
const workspaceFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === workspaceFolderName);
13+
assert.ok(workspaceFolder, `Workspace folder '${workspaceFolderName}' was not found.`);
14+
const start = Date.now();
15+
while (Date.now() - start < 10000) {
16+
if (getUrisForDocument(documentName, workspaceFolder).length > 0) {
17+
return;
18+
}
19+
await new Promise((resolve) => setTimeout(resolve, 100));
20+
}
21+
assert.fail(`Timed out waiting for '${documentName}' to be indexed in workspace folder '${workspaceFolderName}'.`);
22+
}
23+
24+
function getDefinitionTargets(definitions: (vscode.Location | vscode.DefinitionLink)[]): vscode.Uri[] {
25+
return definitions
26+
.map((definition) => ("targetUri" in definition ? definition.targetUri : definition.uri))
27+
.filter((uri): uri is vscode.Uri => !!uri);
28+
}
829

930
suite("Extension Test Suite", () => {
1031
suiteSetup(async function () {
1132
// make sure extension is activated
12-
const serverManager = extensions.getExtension(smExtensionId);
33+
const serverManager = vscode.extensions.getExtension(smExtensionId);
1334
await serverManager?.activate();
14-
const ext = extensions.getExtension(extensionId);
35+
const ext = vscode.extensions.getExtension(extensionId);
1536
await ext?.activate();
1637
});
1738

1839
before(() => {
19-
window.showInformationMessage("Start all tests.");
40+
vscode.window.showInformationMessage("Start all tests.");
2041
});
2142

2243
test("Sample test", () => {
2344
assert.ok("All good");
2445
});
46+
47+
test("Go to Definition resolves to sibling workspace folder", async function () {
48+
this.timeout(10000);
49+
await waitForIndexedDocument("MultiRoot.Shared.cls", "shared");
50+
const clientFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === "client");
51+
assert.ok(clientFolder, "Client workspace folder not available.");
52+
const callerUri = vscode.Uri.joinPath(clientFolder.uri, "src", "MultiRoot", "Caller.cls");
53+
const document = await vscode.workspace.openTextDocument(callerUri);
54+
await vscode.window.showTextDocument(document);
55+
56+
const target = "MultiRoot.Shared";
57+
const sharedOffset = document.getText().indexOf(target);
58+
assert.notStrictEqual(sharedOffset, -1, "Shared class reference not found in Caller.cls");
59+
const position = document.positionAt(sharedOffset + target.indexOf("Shared") + 1);
60+
const definitions = (await vscode.commands.executeCommand(
61+
"vscode.executeDefinitionProvider",
62+
callerUri,
63+
position
64+
)) as (vscode.Location | vscode.DefinitionLink)[];
65+
assert.ok(definitions?.length, "Expected at least one definition result");
66+
const targetUris = getDefinitionTargets(definitions);
67+
const sharedTargetSuffix = path.join("shared", "src", "MultiRoot", "Shared.cls");
68+
assert.ok(
69+
targetUris.some((uri) => uri.scheme === "file" && uri.fsPath.endsWith(sharedTargetSuffix)),
70+
"Expected Go to Definition to resolve to the shared workspace folder"
71+
);
72+
});
73+
74+
test("Go to Definition falls back to server URI when local copy missing", async function () {
75+
this.timeout(10000);
76+
await waitForIndexedDocument("MultiRoot.Shared.cls", "shared");
77+
const clientFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === "client");
78+
assert.ok(clientFolder, "Client workspace folder not available.");
79+
const callerUri = vscode.Uri.joinPath(clientFolder.uri, "src", "MultiRoot", "Caller.cls");
80+
const document = await vscode.workspace.openTextDocument(callerUri);
81+
await vscode.window.showTextDocument(document);
82+
83+
const target = "MultiRoot.ServerOnly";
84+
const offset = document.getText().indexOf(target);
85+
assert.notStrictEqual(offset, -1, "Server-only class reference not found in Caller.cls");
86+
const position = document.positionAt(offset + target.indexOf("ServerOnly") + 1);
87+
const definitions = (await vscode.commands.executeCommand(
88+
"vscode.executeDefinitionProvider",
89+
callerUri,
90+
position
91+
)) as (vscode.Location | vscode.DefinitionLink)[];
92+
assert.ok(definitions?.length, "Expected definition result when resolving missing class");
93+
const targetUris = getDefinitionTargets(definitions);
94+
assert.ok(
95+
targetUris.some((uri) => uri.scheme === OBJECTSCRIPT_FILE_SCHEMA),
96+
"Expected Go to Definition to return a server URI when no local copy exists"
97+
);
98+
});
2599
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"objectscript.conn": {
3+
"active": true,
4+
"ns": "USER"
5+
},
6+
"objectscript.export": {
7+
"folder": "src",
8+
"searchOtherWorkspaceFolders": [
9+
"shared"
10+
]
11+
}
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Class MultiRoot.Caller Extends %RegisteredObject
2+
{
3+
4+
ClassMethod Test()
5+
{
6+
Do ##class(MultiRoot.Shared).Ping()
7+
Do ##class(MultiRoot.ServerOnly).Ping()
8+
}
9+
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"objectscript.conn": {
3+
"active": false,
4+
"ns": "SAMPLES"
5+
},
6+
"objectscript.export": {
7+
"folder": "src"
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Class MultiRoot.Shared Extends %RegisteredObject
2+
{
3+
4+
ClassMethod Ping()
5+
{
6+
Quit
7+
}
8+
9+
}

test-fixtures/test.code-workspace

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
{
22
"folders": [
33
{
4-
"path": "."
4+
"name": "client",
5+
"path": "multi-root/client"
56
},
7+
{
8+
"name": "shared",
9+
"path": "multi-root/shared"
10+
}
611
],
712
"settings": {
813
"objectscript.conn": {
914
"active": false
1015
},
1116
"objectscript.ignoreInstallServerManager": true,
12-
"intersystems.servers": {
13-
}
17+
"intersystems.servers": {}
1418
}
1519
}

0 commit comments

Comments
 (0)