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
158 changes: 121 additions & 37 deletions CHANGELOG.md

Large diffs are not rendered by default.

28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,29 +48,51 @@ Open VS Code. Go to Extensions view (<kbd>⌘</kbd>/<kbd>Ctrl</kbd>+<kbd>Shift</
This extension is able to to take advantage of some VS Code APIs that have not yet been finalized.

The additional features (and the APIs used) are:

- Server-side [searching across files](https://code.visualstudio.com/docs/editor/codebasics#_search-across-files) being accessed using isfs (_TextSearchProvider_)
- [Quick Open](https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_quick-open) of isfs files (_FileSearchProvider_).

To unlock these features (optional):

1. Download and install a beta version from GitHub. This is necessary because Marketplace does not allow publication of extensions that use proposed APIs.
- Go to https://github.com/intersystems-community/vscode-objectscript/releases
- 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.
- 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.

- Go to https://github.com/intersystems-community/vscode-objectscript/releases
- 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.
- 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.

2. From [Command Palette](https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_command-palette) choose `Preferences: Configure Runtime Arguments`.
3. In the argv.json file that opens, add this line (required for both Stable and Insiders versions of VS Code):

```json
"enable-proposed-api": ["intersystems-community.vscode-objectscript"]
```

4. Exit VS Code and relaunch it.
5. Verify that the ObjectScript channel of the Output panel reports this:

```
intersystems-community.vscode-objectscript version X.Y.Z-beta.1 activating with proposed APIs available.
```

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.

## Cross-workspace Go to Definition

> **Implementation developed and maintained by Consistem Sistemas**

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.

```json
{
"objectscript.export": {
"folder": "src",
"searchOtherWorkspaceFolders": ["shared"]
}
}
```

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.

## Notes

- Connection-related output appears in the 'Output' view while switched to the 'ObjectScript' channel using the drop-down menu on the view titlebar.
Expand Down
17 changes: 12 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
{
"name": "vscode-objectscript",
"displayName": "InterSystems ObjectScript",
"displayName": "Consistem InterSystems ObjectScript",
"description": "InterSystems ObjectScript language support for Visual Studio Code",
"version": "3.0.7-SNAPSHOT",
"icon": "images/logo.png",
"aiKey": "InstrumentationKey=9cd75d51-697c-406c-a929-2bcf46e97c64;IngestionEndpoint=https://eastus2-4.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus2.livediagnostics.monitor.azure.com/;ApplicationId=a431c56f-8ccc-4b99-b5e9-fce3763215b1",
"categories": [
"Programming Languages",
"Other",
Expand All @@ -19,11 +18,11 @@
],
"repository": {
"type": "git",
"url": "https://github.com/intersystems-community/vscode-objectscript"
"url": "https://github.com/consistem/vscode-objectscript"
},
"homepage": "https://docs.intersystems.com/components/csp/docbook/DocBook.UI.Page.cls?KEY=GVSCO",
"homepage": "https://github.com/consistem/vscode-objectscript#readme",
"license": "MIT",
"publisher": "intersystems-community",
"publisher": "consistem",
"capabilities": {
"untrustedWorkspaces": {
"supported": true
Expand Down Expand Up @@ -1396,6 +1395,14 @@
},
"additionalProperties": false
},
"searchOtherWorkspaceFolders": {
"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.",
"type": "array",
"items": {
"type": "string"
},
"default": []
},
"atelier": {
"description": "Export source code as Atelier did it, with packages as subfolders. This setting only affects classes, routines, include files and DFI files.",
"type": "boolean"
Expand Down
5 changes: 1 addition & 4 deletions src/commands/unitTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -917,10 +917,7 @@ async function runHandler(
/** The `configureHandler` function for the `TestRunProfile`s. */
function configureHandler(): void {
// Open the settings UI and focus on the "objectscript.unitTest" settings
vscode.commands.executeCommand(
"workbench.action.openSettings",
"@ext:intersystems-community.vscode-objectscript unitTest"
);
vscode.commands.executeCommand("workbench.action.openSettings", "@ext:consistem.vscode-objectscript unitTest");
}

/** Set up the `TestController` and all of its dependencies. */
Expand Down
10 changes: 6 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const extensionId = "intersystems-community.vscode-objectscript";
export const extensionId = "consistem.vscode-objectscript";
export const lsExtensionId = "intersystems.language-server";
export const smExtensionId = "intersystems-community.servermanager";

Expand Down Expand Up @@ -161,7 +161,7 @@ import { isfsConfig } from "./utils/FileProviderUtil";

const packageJson = vscode.extensions.getExtension(extensionId).packageJSON;
const extensionVersion = packageJson.version;
const aiKey = packageJson.aiKey;
const aiKey: string | undefined = packageJson.aiKey;
const PANEL_LABEL = "ObjectScript";
const lowCodeEditorViewType = packageJson.contributes.customEditors[0].viewType;

Expand Down Expand Up @@ -655,7 +655,7 @@ function proposedApiPrompt(active: boolean, added?: readonly vscode.WorkspaceFol
switch (action) {
case "Yes":
vscode.env.openExternal(
vscode.Uri.parse("https://github.com/intersystems-community/vscode-objectscript#enable-proposed-apis")
vscode.Uri.parse("https://github.com/consistem/vscode-objectscript#enable-proposed-apis")
);
break;
case "Never":
Expand Down Expand Up @@ -859,14 +859,16 @@ let incLangConf: vscode.Disposable;
let intLangConf: vscode.Disposable;

export async function activate(context: vscode.ExtensionContext): Promise<any> {
if (!packageJson.version.includes("-") || packageJson.version.includes("-beta.")) {
if (aiKey && (!packageJson.version.includes("-") || packageJson.version.includes("-beta."))) {
// Don't send telemetry for development builds
try {
reporter = new TelemetryReporter(aiKey);
} catch {
// Run without telemetry
reporter = undefined;
}
} else {
reporter = undefined;
}

// workaround for Theia, issue https://github.com/eclipse-theia/theia/issues/8435
Expand Down
85 changes: 64 additions & 21 deletions src/providers/DocumentContentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,30 +165,73 @@ export class DocumentContentProvider implements vscode.TextDocumentContentProvid
});
}
} else {
const conn = config("conn", workspaceFolder);
const conn = config("conn", workspaceFolder) ?? {};
const exportConfig =
workspaceFolder && workspaceFolder !== ""
? (config("export", workspaceFolder) as { searchOtherWorkspaceFolders?: string[] })
: undefined;
const searchOtherWorkspaceFolders = Array.isArray(exportConfig?.searchOtherWorkspaceFolders)
? exportConfig.searchOtherWorkspaceFolders
.map((value) => (typeof value === "string" ? value.trim() : ""))
.filter((value) => value.length > 0)
: [];
const includeAllFolders = searchOtherWorkspaceFolders.includes("*");
const explicitAdditionalFolders = new Set(
searchOtherWorkspaceFolders.filter((value) => value !== "*").map((value) => value.toLowerCase())
);
if (!forceServerCopy) {
// Look for the document in the local file system
const localFile = this.findLocalUri(name, workspaceFolder);
if (localFile && (!namespace || namespace === conn.ns)) {
// Exists as a local file and we aren't viewing a different namespace on the same server,
// so return a uri that will open the local file.
const tryLocalUri = (folderName: string, allowNamespaceMismatch: boolean): vscode.Uri => {
const localFile = this.findLocalUri(name, folderName);
if (!localFile) return;
if (!allowNamespaceMismatch && namespace) {
const folderConn = config("conn", folderName) ?? {};
if (folderConn.ns && namespace !== folderConn.ns) {
return;
}
}
return localFile;
} else {
// The local file doesn't exist in this folder, so check any other
// local folders in this workspace if it's a multi-root workspace
const wFolders = vscode.workspace.workspaceFolders;
if (wFolders && wFolders.length > 1) {
// This is a multi-root workspace
};

// Look for the document in the local file system
const primaryLocal = tryLocalUri(workspaceFolder, false);
if (primaryLocal) {
return primaryLocal;
}

// Check any other eligible local folders in this workspace if it's a multi-root workspace
const wFolders = vscode.workspace.workspaceFolders;
if (wFolders && wFolders.length > 1 && workspaceFolder) {
const candidates: { folder: vscode.WorkspaceFolder; allowNamespaceMismatch: boolean }[] = [];
const seen = new Set<string>();
const addCandidate = (folder: vscode.WorkspaceFolder, allowNamespaceMismatch: boolean): void => {
if (!notIsfs(folder.uri)) return;
if (folder.name === workspaceFolder) return;
if (seen.has(folder.name)) return;
candidates.push({ folder, allowNamespaceMismatch });
seen.add(folder.name);
};

for (const wFolder of wFolders) {
if (wFolder.name === workspaceFolder) continue;
const wFolderConn = config("conn", wFolder.name) ?? {};
if (compareConns(conn, wFolderConn) && (!namespace || namespace === wFolderConn.ns)) {
addCandidate(wFolder, false);
}
}

if (includeAllFolders || explicitAdditionalFolders.size > 0) {
for (const wFolder of wFolders) {
if (notIsfs(wFolder.uri) && wFolder.name != workspaceFolder) {
// This isn't the folder that we checked originally
const wFolderConn = config("conn", wFolder.name);
if (compareConns(conn, wFolderConn) && (!namespace || namespace === wFolderConn.ns)) {
// This folder is connected to the same server:ns combination as the original folder
const wFolderFile = this.findLocalUri(name, wFolder.name);
if (wFolderFile) return wFolderFile;
}
}
if (wFolder.name === workspaceFolder) continue;
const shouldInclude = includeAllFolders || explicitAdditionalFolders.has(wFolder.name.toLowerCase());
if (!shouldInclude) continue;
addCandidate(wFolder, true);
}
}

for (const candidate of candidates) {
const candidateLocal = tryLocalUri(candidate.folder.name, candidate.allowNamespaceMismatch);
if (candidateLocal) {
return candidateLocal;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/test/runTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async function main() {
installExtension("intersystems-community.servermanager");
installExtension("intersystems.language-server");

const launchArgs = ["-n", workspace, "--enable-proposed-api", "intersystems-community.vscode-objectscript"];
const launchArgs = ["-n", workspace, "--enable-proposed-api", "consistem.vscode-objectscript"];

// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs });
Expand Down
84 changes: 79 additions & 5 deletions src/test/suite/extension.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,99 @@
import * as assert from "assert";
import { before } from "mocha";
import * as path from "path";

// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import { window, extensions } from "vscode";
import { extensionId, smExtensionId } from "../../extension";
import * as vscode from "vscode";
import { extensionId, smExtensionId, OBJECTSCRIPT_FILE_SCHEMA } from "../../extension";
import { getUrisForDocument } from "../../utils/documentIndex";

async function waitForIndexedDocument(documentName: string, workspaceFolderName: string): Promise<void> {
const workspaceFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === workspaceFolderName);
assert.ok(workspaceFolder, `Workspace folder '${workspaceFolderName}' was not found.`);
const start = Date.now();
while (Date.now() - start < 10000) {
if (getUrisForDocument(documentName, workspaceFolder).length > 0) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
assert.fail(`Timed out waiting for '${documentName}' to be indexed in workspace folder '${workspaceFolderName}'.`);
}

function getDefinitionTargets(definitions: (vscode.Location | vscode.DefinitionLink)[]): vscode.Uri[] {
return definitions
.map((definition) => ("targetUri" in definition ? definition.targetUri : definition.uri))
.filter((uri): uri is vscode.Uri => !!uri);
}

suite("Extension Test Suite", () => {
suiteSetup(async function () {
// make sure extension is activated
const serverManager = extensions.getExtension(smExtensionId);
const serverManager = vscode.extensions.getExtension(smExtensionId);
await serverManager?.activate();
const ext = extensions.getExtension(extensionId);
const ext = vscode.extensions.getExtension(extensionId);
await ext?.activate();
});

before(() => {
window.showInformationMessage("Start all tests.");
vscode.window.showInformationMessage("Start all tests.");
});

test("Sample test", () => {
assert.ok("All good");
});

test("Go to Definition resolves to sibling workspace folder", async function () {
this.timeout(10000);
await waitForIndexedDocument("MultiRoot.Shared.cls", "shared");
const clientFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === "client");
assert.ok(clientFolder, "Client workspace folder not available.");
const callerUri = vscode.Uri.joinPath(clientFolder.uri, "src", "MultiRoot", "Caller.cls");
const document = await vscode.workspace.openTextDocument(callerUri);
await vscode.window.showTextDocument(document);

const target = "MultiRoot.Shared";
const sharedOffset = document.getText().indexOf(target);
assert.notStrictEqual(sharedOffset, -1, "Shared class reference not found in Caller.cls");
const position = document.positionAt(sharedOffset + target.indexOf("Shared") + 1);
const definitions = (await vscode.commands.executeCommand(
"vscode.executeDefinitionProvider",
callerUri,
position
)) as (vscode.Location | vscode.DefinitionLink)[];
assert.ok(definitions?.length, "Expected at least one definition result");
const targetUris = getDefinitionTargets(definitions);
const sharedTargetSuffix = path.join("shared", "src", "MultiRoot", "Shared.cls");
assert.ok(
targetUris.some((uri) => uri.scheme === "file" && uri.fsPath.endsWith(sharedTargetSuffix)),
"Expected Go to Definition to resolve to the shared workspace folder"
);
});

test("Go to Definition falls back to server URI when local copy missing", async function () {
this.timeout(10000);
await waitForIndexedDocument("MultiRoot.Shared.cls", "shared");
const clientFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === "client");
assert.ok(clientFolder, "Client workspace folder not available.");
const callerUri = vscode.Uri.joinPath(clientFolder.uri, "src", "MultiRoot", "Caller.cls");
const document = await vscode.workspace.openTextDocument(callerUri);
await vscode.window.showTextDocument(document);

const target = "MultiRoot.ServerOnly";
const offset = document.getText().indexOf(target);
assert.notStrictEqual(offset, -1, "Server-only class reference not found in Caller.cls");
const position = document.positionAt(offset + target.indexOf("ServerOnly") + 1);
const definitions = (await vscode.commands.executeCommand(
"vscode.executeDefinitionProvider",
callerUri,
position
)) as (vscode.Location | vscode.DefinitionLink)[];
assert.ok(definitions?.length, "Expected definition result when resolving missing class");
const targetUris = getDefinitionTargets(definitions);
assert.ok(
targetUris.some((uri) => uri.scheme === OBJECTSCRIPT_FILE_SCHEMA),
"Expected Go to Definition to return a server URI when no local copy exists"
);
});
});
12 changes: 12 additions & 0 deletions test-fixtures/multi-root/client/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"objectscript.conn": {
"active": true,
"ns": "USER"
},
"objectscript.export": {
"folder": "src",
"searchOtherWorkspaceFolders": [
"shared"
]
}
}
10 changes: 10 additions & 0 deletions test-fixtures/multi-root/client/src/MultiRoot/Caller.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Class MultiRoot.Caller Extends %RegisteredObject
{

ClassMethod Test()
{
Do ##class(MultiRoot.Shared).Ping()
Do ##class(MultiRoot.ServerOnly).Ping()
}

}
9 changes: 9 additions & 0 deletions test-fixtures/multi-root/shared/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"objectscript.conn": {
"active": false,
"ns": "SAMPLES"
},
"objectscript.export": {
"folder": "src"
}
}
Loading