diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
index cc87a216..998f32ce 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -99,6 +99,7 @@ jobs:
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ steps.set-version.outputs.version }}
+ target_commitish: ${{ github.sha }}
prerelease: ${{ github.event_name != 'release' }}
files: ${{ steps.set-version.outputs.name }}.vsix
token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e5d1f19..34243df1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,9 @@
# Change Log
## [3.2.0] 06-Oct-2025
+
Minimum VS Code version is now 1.104.0.
+
- Enhancements
- Better error handling when expanding InterSystems and Projects Explorers (#1652)
- Allow opening low-code editors from read-only file systems (#1655)
@@ -17,6 +19,7 @@ Minimum VS Code version is now 1.104.0.
- Upgrade dependencies (#1650, #1653)
## [3.0.6] 09-Sep-2025
+
- Enhancements
- Add `objectscript.unitTest.enabled` setting (#1627)
- Add a `New File...` command for creating an Interoperability Message (#1629)
@@ -33,6 +36,7 @@ Minimum VS Code version is now 1.104.0.
- Don't append extra trailing newlines when saving server-side web app files (#1648)
## [3.0.5] 21-Jul-2025
+
- Enhancements
- Better telemetry (#1608)
- Output the path to the export file when exporting to an XML file (#1616)
@@ -47,6 +51,7 @@ Minimum VS Code version is now 1.104.0.
- Fix resolving of server connection for files opened from InterSystems Explorer or XML preview (#1622)
## [3.0.4] 30-Jun-2025
+
- Enhancements
- Shorten server-side error messages (#1587)
- Make it more clear when a server connection failed due to a timeout (#1592)
@@ -56,6 +61,7 @@ Minimum VS Code version is now 1.104.0.
- Fix comparing of local and server versions of web app files (#1599)
## [3.0.3] 16-Jun-2025
+
- Enhancements
- Cache the contents of files fetched during a debug session (#1579)
- Fixes
@@ -78,6 +84,7 @@ Minimum VS Code version is now 1.104.0.
- Fix saving and re-opening of server-side documents on VS Code 1.101.0 (#1584)
## [3.0.2] 20-May-2025
+
- Enhancements
- Allow `objectscript.multilineMethodArgs` to be set per workspace folder (#1534)
- Log out of web sessions when VS Code exits (#1540)
@@ -97,11 +104,13 @@ Minimum VS Code version is now 1.104.0.
- Don't report error when deleting a local file that doesn't exist on the server (#1555)
## [3.0.1] 04-Apr-2025
+
- Fixes
- Fix issue where `Undo` after a save deletes the file being edited (#1524)
- Fix endless save loop when one local workspace folder is a subfolder of another (#1525)
## [3.0.0] 02-Apr-2025
+
- Enhancements
- Client-side editing overhaul (#1401, #1470, #1515, #1520):
- Support the use of client-side editing in any non-isfs workspace folder, not just folders in your local file system. For example, with [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview).
@@ -142,10 +151,12 @@ Minimum VS Code version is now 1.104.0.
- Upgrade dependencies
## [2.12.10] 13-Nov-2024
+
- Fixes
- Prevent overprompting for permission and account (#1456)
## [2.12.9] 29-Oct-2024
+
- Enhancements
- Add `Launch Lite Terminal` action to Explorer (#1438)
- Add timeout to initial connection request (#1440)
@@ -157,10 +168,12 @@ Minimum VS Code version is now 1.104.0.
- Support for line wrapping in Lite Terminal (#1452)
## [2.12.8] 23-Sep-2024
+
- Fixes
- Solve 1.93 performance issue (#1428)
## [2.12.7] 05-Aug-2024
+
- Enhancements
- Fire source control hooks for opened and closed documents (#1414)
- Always stop the debug target process when attaching (#1415)
@@ -175,7 +188,9 @@ Minimum VS Code version is now 1.104.0.
- Don't append CSPCHD for web applications that don't support it by default (#1420)
## [2.12.6] 23-Jul-2024
+
Minimum VS Code version is now 1.91.0.
+
- Enhancements
- Support command stepping in debugger (requires InterSystems IRIS 2023.1.5, 2024.1.1+, or 2024.2+) (#1385)
- Add `Compile` command to server-side file explorer (#1389)
@@ -195,6 +210,7 @@ Minimum VS Code version is now 1.91.0.
- Provide Project and User parameters to Studio add-ins (#1402)
## [2.12.5] 29-May-2024
+
- Enhancements
- [Open symbol by name](https://code.visualstudio.com/docs/editor/editingevolved#_open-symbol-by-name) (`Ctrl/Cmd-T`) improvements (#1366):
- Show classes as well as class members
@@ -205,6 +221,7 @@ Minimum VS Code version is now 1.91.0.
- Show prompt for username if unauthenticated access fails when no username is specified in the server definition (#1372)
## [2.12.4] 14-May-2024
+
- Enhancements
- Remove `objectscript.ignoreInstallServerManager` setting (#1339)
- Make ObjectScript comment tokens configurable (#1353)
@@ -216,6 +233,7 @@ Minimum VS Code version is now 1.91.0.
- Support trailing slash in `isfs` directory URIs (#1357)
## [2.12.3] 26-Mar-2024
+
- Enhancements
- Improve `Jump to Tag + Offset` UI (#1325)
- Add `Modify Project Metadata...` command (#1326)
@@ -229,6 +247,7 @@ Minimum VS Code version is now 1.91.0.
- Fix manual refresh of Test Explorer (#1336) (suggested by @ollitanska)
## [2.12.2] 28-Feb-2024
+
- Enhancements
- Add auto-closing of C-style block comments (#1311)
- Server-side source control improvements (#1314):
@@ -246,14 +265,17 @@ Minimum VS Code version is now 1.91.0.
- Refresh ObjectScript Explorer files when they are re-opened (#1321)
## [2.12.1] 05-Feb-2024
+
- Fixes
- Don't create unit test items in workspace folders that don't support running tests (#1307)
- Update `objectscript.unitTest.relativeTestRoots` validation regex (#1308)
- Fix `undefined` errors when building array of unit tests to load (#1308)
## [2.12.0] 29-Jan-2024
+
Minimum VS Code version is now 1.83.0.
This extension now depends on the [InterSystems Server Manager](https://marketplace.visualstudio.com/items?itemName=intersystems-community.servermanager) extension.
+
- Enhancements
- Add support for running and debugging unit tests (#1269)
- Use Server Manager's View Container (#1270)
@@ -263,7 +285,7 @@ This extension now depends on the [InterSystems Server Manager](https://marketpl
- Add testing link to new KPIs (#1302)
- Add CodeLenses for BPLs, DTLs, KPIs and Rules (#1303)
- Fixes
- - Harden `TextSearchProvider` (#1276, #1294)
+ - Harden `TextSearchProvider` (#1276, #1294)
- Fix WebSocket Terminal del key (#1285)
- Make server-side search respect Context Lines feature of Search Editor (#1290)
- Better message when WebSocket Terminal can't be started (#1293)
@@ -271,6 +293,7 @@ This extension now depends on the [InterSystems Server Manager](https://marketpl
- Remove `glob` as a dependency (#1300)
## [2.10.5] 02-Nov-2023
+
- Enhancements
- Use new Modern themes when loading Studio syntax colors (#1264)
- Fixes
@@ -278,6 +301,7 @@ This extension now depends on the [InterSystems Server Manager](https://marketpl
- Keep file contents when copying class definition if "Class" line not found (#1267)
## [2.10.4] 01-Nov-2023
+
- Fixes
- Fix sorting of items in Projects Explorer (#1246)
- Don't show REST APIs in Explorer CSP Files list (#1248)
@@ -290,6 +314,7 @@ This extension now depends on the [InterSystems Server Manager](https://marketpl
- Allow users to select no resource in new KPI command (#1261)
## [2.10.3] 25-Sep-2023
+
- Enhancements
- Put link to editor in class comment when creating new BPL/DTL (#1231)
- Make it easier to add namespace from same server to workspace (#1232)
@@ -299,6 +324,7 @@ This extension now depends on the [InterSystems Server Manager](https://marketpl
- Remove unneeded snippets (#1235)
## [2.10.2] 07-Sep-2023
+
- Enhancements
- Improve message to help resolve scenario where isfs user lacks `%DB_IRISSYS:READ` (#1211)
- Improve MAC and INT stubs created for new server-side routine (#1218)
@@ -309,6 +335,7 @@ This extension now depends on the [InterSystems Server Manager](https://marketpl
- Display routine members of server-side project correctly (#1226)
## [2.10.1] 10-Aug-2023
+
- Enhancements
- Only add WebSocket Terminal button to Server Manager 3.4.2+ tree if server is compatible (#1204)
- Add `Copy Invocation` CodeLens above query definition in class (#1198)
@@ -318,11 +345,13 @@ This extension now depends on the [InterSystems Server Manager](https://marketpl
- Properly report search matches for super classes (#1200)
## [2.10.0] 20-Jul-2023
+
Minimum VS Code version is now 1.75.0
+
- Enhancements
- Add WebSocket Terminal support (#1150)
- Support a ${project} variable in Server Actions Menu custom entries (#1157)
- - Support importing/exporting XML files (#1171)
+ - Support importing/exporting XML files (#1171)
- Support a ${username} variable in Server Actions Menu custom entries (#1173)
- Migrate to [official ISC documentation](https://docs.intersystems.com/components/csp/docbook/DocBook.UI.Page.cls?KEY=GVSCO) (#1185)
- Fixes
@@ -344,6 +373,7 @@ Minimum VS Code version is now 1.75.0
- Upgrade vulnerable dependencies.
## [2.8.1] 15-May-2023
+
- Enhancements
- Prompt user to enable proposed APIs when server-side folder is opened (#1140)
- Show config names of interoperability jobs in `Attach to Process` debug menu (#1089) (contributed by @ollitanska)
@@ -353,6 +383,7 @@ Minimum VS Code version is now 1.75.0
- Make folder-specific settings for ISFS folder work again (#1144)
## [2.8.0] 04-Apr-2023
+
- Enhancements
- Integrate Angular Rule Editor (#1014)
- Add command to refresh local file contents (#1066) (contributed by @ollitanska)
@@ -365,6 +396,7 @@ Minimum VS Code version is now 1.75.0
- Upgrade vulnerable dependencies.
## [2.6.0] 27-Feb-2023
+
- Enhancements
- Implement async server-side search (#1045) (requires [proposed API enabled](https://github.com/intersystems-community/vscode-objectscript#enable-proposed-apis) and InterSystems IRIS 2023.1+)
- Add `Switch Namespace` option to Server Actions menu for local workspace folders (#1065) (contributed by @ollitanska)
@@ -384,11 +416,13 @@ Minimum VS Code version is now 1.75.0
- Upgrade vulnerable dependencies.
## [2.4.3] 02-Feb-2023
+
- Fixes
- Fix deployed check (#1071)
- Fix opening of `isfs` files from ObjectScript Explorer (#1072)
## [2.4.2] 01-Feb-2023
+
- Enhancements
- Use query instead of index for class Deployed checks (#1054)
- Use `docker compose` command if present (#1057)
@@ -401,10 +435,12 @@ Minimum VS Code version is now 1.75.0
- Upgrade vulnerable dependencies.
## [2.4.1] 12-Jan-2023
+
- Fixes
- Fix 'No file system provider found' errors when debugging local file (#1047)
## [2.4.0] 10-Jan-2023
+
- Enhancements
- Show server name in Status Bar (#1017)
- Server-side search: use include/exclude specs (#1021)
@@ -417,10 +453,11 @@ Minimum VS Code version is now 1.75.0
- Hide `-injection` languages from selector (#1011)
- Properly report matches in Storage definitions (#1025)
- Fix debug breakpoint mapping when Language Server is absent (#1031)
- - Don't call `openTextDocument` in debugger (#1042)
+ - Don't call `openTextDocument` in debugger (#1042)
- Upgrade vulnerable dependencies.
## [2.2.0] 31-Oct-2022
+
- Enhancements
- Add features to ease migration from Studio (see [Migrating from Studio documentation page](https://docs.intersystems.com/components/csp/docbook/DocBook.UI.Page.cls?KEY=GVSCO_fromstudio) for details) (#1003)
- Improve CodeLenses (#1007)
@@ -432,12 +469,14 @@ Minimum VS Code version is now 1.75.0
- Upgrade vulnerable dependencies.
## [2.0.0] 04-Oct-2022
+
- Enhancements
- Use Server Manager version 3's enhanced security for stored passwords. Explicit permission must be given by the user before Server Manager will provide a connection's stored password to this extension. This feature previewed in the 1.x pre-releases, which 2.0.0 supersedes.
- Add `Copy Invocation` CodeLens alongside `Debug this Method`. Hideable using the `objectscript.debug.copyToClipboard` setting (#974)
- Add `objectscript.importOnSave` setting to control whether saving a client-side file updates code on the connected server. Default is `true` (#985)
## [1.8.2] 08-Aug-2022
+
- Enhancements
- Support `objectscript` and `objectscript-class` as the info string for [fenced code blocks](https://spec.commonmark.org/0.30/#fenced-code-blocks) when editing Markdown. However coloring does not render in preview (#964)
- Fixes
@@ -445,6 +484,7 @@ Minimum VS Code version is now 1.75.0
- Dispose of all event handlers when deactivating (#967)
## [1.8.1] 25-Jul-2022
+
- Fixes
- New class should ignore `objectscript.export.folder` setting (#938)
- Get correct host port number for connection to docker-compose with multiple services (#941)
@@ -456,13 +496,16 @@ Minimum VS Code version is now 1.75.0
- Upgrade vulnerable dependencies.
## [1.8.0] 20-Apr-2022
+
- Enhancements
- Add support for server-side projects (#851)
- Implement isfs folder rename and deletion (#923, #922)
- - Support "mapped" flag for isfs and export filters, to exclude packages mapped from other databases (#931)
+ - Support "mapped" flag for isfs and export filters, to exclude packages mapped from other databases (#931)
## [1.6.0] 06-Apr-2022
+
Minimum VS Code version is now 1.66.0
+
- Enhancements
- Colorize text in Output channel (API has finalized) (#910)
- Add `objectscript.export.exactFilter` setting (#913)
@@ -476,6 +519,7 @@ Minimum VS Code version is now 1.66.0
- Upgrade vulnerable dependencies.
## [1.4.4] 21-Mar-2022
+
- Enhancements
- Compile asynchronously to avoid timing out (#890)
- Add `objectscript.explorer.alwaysShowServerCopy` setting to make ObjectScript Explorer always open server-side code, even when local copy exists (#494)
@@ -489,12 +533,14 @@ Minimum VS Code version is now 1.66.0
- Upgrade vulnerable dependencies.
## [1.4.3] 28-Feb-2022
+
- Enhancements
- Add `objectscript.openClassContracted` setting (#876)
- Fixes
- Fix 1.4.2 regression that broke server-side editing from ObjectScript Explorer and reloading of open documents when reopening isfs workspaces (#879)
## [1.4.2] 23-Feb-2022
+
- Enhancements
- Generate content when a new local class or routine is created (#867)
- Add file icons (#822)
@@ -510,10 +556,12 @@ Minimum VS Code version is now 1.66.0
- Upgrade vulnerable dependencies.
## [1.4.1] 14-Jan-2022
+
- Fixes
- Version 1.4.0 is failing to activate (#827)
## [1.4.0] 14-Jan-2022
+
- Enhancements
- Make `Ctrl / Cmd+T` lookup (Open Symbol by Name) check all servers connected to a multi-root workspace (#815)
- Improve exporting (#818)
@@ -528,10 +576,12 @@ Minimum VS Code version is now 1.66.0
- Remove `vscode-objectscript-output` language from selector (#805)
## [1.2.2] 07-Dec-2021
+
- Fixes
- Exporting not working with new version 1.2.1 (#799)
## [1.2.0] 02-Dec-2021
+
- Enhancements
- Overhaul `WorkspaceSymbolProvider` (#772)
- Add `Open Shell in Docker` option to Server Actions menu (#778)
@@ -545,28 +595,31 @@ Minimum VS Code version is now 1.66.0
- Upgrade vulnerable dependencies (#787)
## [1.1.1] 09-Nov-2021
+
- Fixes
- - Debugger: Breakpoint with no hitCondition cannot be set (#766)
+ - Debugger: Breakpoint with no hitCondition cannot be set (#766)
## [1.1.0] 08-Nov-2021
+
- Enhancements
- - Add 'Show Class Documentation Preview' button and command.
- - Improve how line comment markers carry over when newline is entered (#541)
- - Allow server-side source control class UserAction method call with Action=3 to launch an http/s or ftp/s URL in the external browser (contributed by @a-boertien).
- - Add support for conditional breakpoints.
- - Improve documentation.
+ - Add 'Show Class Documentation Preview' button and command.
+ - Improve how line comment markers carry over when newline is entered (#541)
+ - Allow server-side source control class UserAction method call with Action=3 to launch an http/s or ftp/s URL in the external browser (contributed by @a-boertien).
+ - Add support for conditional breakpoints.
+ - Improve documentation.
- Fixes
- - Prevent save of isfs class if filename doesn't match the class it defines (#410)
- - Refresh ObjectScript Explorer after export (#502)
- - Improve message when `/api/atelier` web application is disabled or missing (#752)
- - Correctly handle dots in routine names, preventing two copies of the same routine from being opened concurrently (#748)
- - Handle multiple selections when performing compile or delete from ObjectScript Explorer (#746)
- - Revert document instead of attempting an undo when server-side source control signals this is necessary.
- - Resolve issue causing unusable authentication page after CSP timeout.
- - Fix XML to UDL conversion.
- - Upgrade vulnerable dependencies.
+ - Prevent save of isfs class if filename doesn't match the class it defines (#410)
+ - Refresh ObjectScript Explorer after export (#502)
+ - Improve message when `/api/atelier` web application is disabled or missing (#752)
+ - Correctly handle dots in routine names, preventing two copies of the same routine from being opened concurrently (#748)
+ - Handle multiple selections when performing compile or delete from ObjectScript Explorer (#746)
+ - Revert document instead of attempting an undo when server-side source control signals this is necessary.
+ - Resolve issue causing unusable authentication page after CSP timeout.
+ - Fix XML to UDL conversion.
+ - Upgrade vulnerable dependencies.
## [1.0.14] 20-Sep-2021
+
- Require confirmation before compiling all code in namespace (#677)
- Respect `maxResults` parameter when running server-side search (#713)
- Handle multiple spaces between `Class` keyword and classname (#717)
@@ -578,6 +631,7 @@ Minimum VS Code version is now 1.66.0
- Upgrade vulnerable dependency.
## [1.0.13] 09-Jul-2021
+
- Add Watchpoint support to debugging (#697)
- Make QuickOpen respect any `filter=xxx` query parameter on the isfs folder definition (#593)
- Fix unexpected alerts about server-side copy being newer when working with isfs (#683)
@@ -588,6 +642,7 @@ Minimum VS Code version is now 1.66.0
- Allow opting out of 'Other Studio Action' server-side source control calls (#691)
## [1.0.12] 10-Jun-2021
+
- Allow extension to work in untrusted workspaces.
- Don't switch to File Explorer view when opening a file from ObjectScript Explorer (#651)
- Scroll to correct line after an Output panel link is clicked (#657)
@@ -596,6 +651,7 @@ Minimum VS Code version is now 1.66.0
- Upgrade vulnerable dependencies.
## [1.0.11] 12-May-2021
+
- Support client-side web app (CSP) workflow as long as web app path is in the `/csp/*` space (#147, #449)
- Add compile-only commands 'Compile Current File' and 'Compile Current File with Specified Flags...' (#595)
- Add 'Edit Other' command plus menu option below 'View Other' (#309)
@@ -616,6 +672,7 @@ Minimum VS Code version is now 1.66.0
- Upgrade vulnerable dependencies.
## [1.0.10] 26-Apr-2021
+
- Avoid prompting for already-saved password (#61)
- Constrain QuickOpen list contents when `isfs` folder path targets a specific package (#581)
- Show `isfs` folder label in breadcrumb even without proposed APIs enabled (#599)
@@ -628,26 +685,29 @@ Minimum VS Code version is now 1.66.0
- Upgrade vulnerable dependencies.
## [1.0.9] 22-Mar-2021
+
- Allow system files (% classes) to be searched from non-%SYS namespace.
- Handle `objectscript.conn.server` referencing non-existent `intersystems.servers` entry (#586)
- Improve README.
- Upgrade vulnerable dependencies.
## [1.0.8] 15-Jan-2021
+
- Implement `isfs://server:namespace/` syntax as an alternative to the `ns=NAMESPACE` query parameter (#450)
- Use new isfs notation in entries created by 'Add Server Namespace to Workspace' (#554)
- Load server-side (isfs) folder-specific snippets (#552)
- Improve snippets:
- - Add a ///-comment tabstop at the start of all snippets used in class definitions.
- - Add descriptive default text to more tabstops.
- - Add third superclass to multi-superclass snippet.
- - Uniformly use Capitalized command names and UPPERCASE function names in ObjectScript.
- - Standardize body layout in definitions to reflect layout of result.
- - Tidy how duplicate tabstops are used.
+ - Add a ///-comment tabstop at the start of all snippets used in class definitions.
+ - Add descriptive default text to more tabstops.
+ - Add third superclass to multi-superclass snippet.
+ - Uniformly use Capitalized command names and UPPERCASE function names in ObjectScript.
+ - Standardize body layout in definitions to reflect layout of result.
+ - Tidy how duplicate tabstops are used.
- Support searching all Studio document types when using symbol search (Cmd/Ctrl + T).
- Upgrade vulnerable dependency.
## [1.0.7] 4-Jan-2021
+
- Fix issue affecting use with Docker on Windows (#516)
- Resolve problem debugging in a multi-root workspace using isfs (#387)
- Allow 'View Other' from custom Studio documents.
@@ -656,6 +716,7 @@ Minimum VS Code version is now 1.66.0
- Upgrade vulnerable dependency.
## [1.0.6] 13-Nov-2020
+
- Target current class when opening documentation from Server Actions quickpick, launched by click on ObjectScript panel in status bar (#490)
- Improve code snippets (#493)
- Update README to state need for manual download and install of beta VSIX in order to use proposed APIs (#478)
@@ -665,26 +726,28 @@ Minimum VS Code version is now 1.66.0
- Exclude Studio project documents (.prj) from isfs tree (#501)
- Fix variable tree cascade that occurred when value was edited during debugging (#505)
- Show clickable url launching graphical editor for BPL and DTL documents opened from isfs folder (#508)
- - To show .bpl and .dtl files, add `filter=*` to isfs folder's `uri` property in your `XXX.code-workspace` file.
- - Alternatively, use `View Other` from context menu of the corresponding class.
+ - To show .bpl and .dtl files, add `filter=*` to isfs folder's `uri` property in your `XXX.code-workspace` file.
+ - Alternatively, use `View Other` from context menu of the corresponding class.
- Display supported image files correctly when opened from isfs web application folder (#394)
- Prevent import from overwriting class that is in [deployed mode](https://docs.intersystems.com/iris20181/csp/docbook/Doc.View.cls?KEY=GOBJ_classes#GOBJ_deploy_classes) (#382)
- Respect `pathPrefix` property of an `intersystems.servers` connection definition in more places:
- - debugger connections
- - urls on Server Actions menu
+ - debugger connections
+ - urls on Server Actions menu
## [1.0.5] 5-Nov-2020
+
- Defer to Language Server 1.0.5+ for folding range information (#473)
- Add `objectscript.overwriteServerChanges` setting to permit unconditional import from local file (#464)
- Fix authentication problem introduced in 1.0.2 (#458)
- Handle Unicode characters in identifiers (#337)
- Avoid inappropriate transfer of user-level `objectscript.conn` settings into workspace-level settings (#460)
- Enhancements available only when proposed APIs are enabled:
- - Improve format of results from Quick Open server-side file name search (#467)
- - Add root folder label text to label of isfs file (#455)
- - Add '(read-only)' suffix to label of non-editable file opened from ObjectScript Explorer (#471)
+ - Improve format of results from Quick Open server-side file name search (#467)
+ - Add root folder label text to label of isfs file (#455)
+ - Add '(read-only)' suffix to label of non-editable file opened from ObjectScript Explorer (#471)
## [1.0.4] 30-Oct-2020
+
- Wait for connection checks to complete during activation.
- Display debugging values correctly when they contain characters above ASCII 127.
- Fix broken server-side .vscode storage mechanism when isfs query string includes other parameters after `ns`.
@@ -694,9 +757,11 @@ Minimum VS Code version is now 1.66.0
- Differentiate "Edit" and "View" options better on isfs dialog.
## [1.0.3] 24-Oct-2020
+
- Fix problem that prevented 1.0.2 from publishing to Marketplace.
## [1.0.2] 23-Oct-2020
+
- Fix problem with excessive license use.
- Install language server extension in the background.
- Use less status bar space.
@@ -704,18 +769,22 @@ Minimum VS Code version is now 1.66.0
- Add `objectscript.export.map` setting.
## [1.0.1] 20-Oct-2020
+
- First production release.
## [0.9.5]
+
- Fix regression in 0.9.4 that broke `Add Server Namespace to Workspace...`.
## [0.9.4]
+
- Support folder-level settings, snippets and debug configurations for server-side (isfs) workspace folders. This feature requires a `/_vscode` webapp using the %SYS namespace.
- Support webapp-type roots referencing a path that is an ancestor of one or more webapps that use the target namespace. For example `isfs://server/?ns=%SYS&csp` gives access to all %SYS webapps from a single root folder.
- Enhance `Add Server Namespace to Workspace...` command and quickstart button to add webapp-type roots.
- Remove requirement for namespaces to be uppercase in settings.
## [0.9.3]
+
- Add quickstart button to ObjectScript Explorer view when local folder is open but no `objectscript.conn` settings are available to it.
- Add `Jump to Tag + Offset` command for MACs and INTs, and make it available via click on statusbar field.
- Support server-side editing of other filetypes such as HL7, LUT.
@@ -729,10 +798,12 @@ Minimum VS Code version is now 1.66.0
- Prepare to coexist with upcoming language server extension.
## [0.9.2]
+
- Implement `Add Server Namespace to Workspace...` command and surface it on folder context menus in VS Code Explorer.
- Add `Choose Server and Namespace` button to VS Code Explorer view when no folder or workspace is open. This provides a quick way to get started with server-centric development, particularly when combined with the 'just-in-time' connection definition enhancement that arrived in version 0.0.7 of the Server Manager extension.
## [0.9.1]
+
- Fix problem that caused isfs-type saves to report incorrectly that server version was newer.
- Prevent silent overwrite on retry after an import was initially canceled because of server-side difference.
- Serialize and deduplicate initial credential prompting when a multi-server workspace is opened.
@@ -744,6 +815,7 @@ Minimum VS Code version is now 1.66.0
- Add missing 0.9.0 CHANGELOG.
## [0.9.0]
+
- Change publisher id to be 'intersystems-community'.
- Refresh correctly from server after isfs-type save and compile.
- Swap the two sides displayed by a compare invoked after local file import conflict. Server copy is now on the left, to match convention elsewhere in VS Code.
@@ -754,25 +826,27 @@ Minimum VS Code version is now 1.66.0
- Upgrade vulnerable dependencies.
## [0.8.9]
+
- Fix saving of isfs-type server-side editing, broken in 0.8.8.
- Implement double-click opening from ObjectScript Explorer.
- Make ObjectScript Explorer handle non-isfs multi-server multi-root workspace correctly.
- Reload VS Code Explorer tree after successful connection.
- Fix some issues with `export.addCategory` setting:
- - Resolve error when non-string was used as folder value.
- - If setting contains multiple patterns, check all of them, in given order.
+ - Resolve error when non-string was used as folder value.
+ - If setting contains multiple patterns, check all of them, in given order.
- Fix server-side searching of isfs-type root that uses `intersystems.servers` for its connection.
- - Server-side searching uses a VS Code API that is still (1.48) at "proposed" stage. See [here](https://github.com/intersystems-community/vscode-objectscript/issues/126#issuecomment-674089387) for instructions on how to use this pre-release feature.
+ - Server-side searching uses a VS Code API that is still (1.48) at "proposed" stage. See [here](https://github.com/intersystems-community/vscode-objectscript/issues/126#issuecomment-674089387) for instructions on how to use this pre-release feature.
- No longer use progress indicator when server-side source control displays a page.
- Do not call server-side AfterUserAction if not necessary.
- Upgrade vulnerable dependencies.
## [0.8.8]
+
- Fix retrieval of password when `objectscript.conn.server` defers to Server Manager.
- Fix command completions, broken in 0.8.7.
- Improve ObjectScript Explorer:
- - Files that will be loaded from local workspace now show their filetype icon and a full path tooltip.
- - Fix rare case where code would load from wrong place.
+ - Files that will be loaded from local workspace now show their filetype icon and a full path tooltip.
+ - Fix rare case where code would load from wrong place.
- Skip compilation of local CSP files for now.
- Improve handling of server modification date comparisons.
- Fix incorrect `Studio Action "Changed Namespace" not supported` message in output channel.
@@ -781,6 +855,7 @@ Minimum VS Code version is now 1.66.0
- Improve README information about username and password in settings.
## [0.8.7]
+
- Use `intersystems.servers` object for more flexible connection definitions.
- Recommend [intersystems-community.servermanager](https://marketplace.visualstudio.com/items?itemName=intersystems-community.servermanager) extension for management of `intersystems.servers` definitions.
- Support server-side source control and other server-side commands.
@@ -802,23 +877,28 @@ Minimum VS Code version is now 1.66.0
- Webpack extension to reduce size.
## [0.8.6] - 2020-04-23
+
- Support $ETRAP system variable.
- Fix opening Docker terminal.
## [0.8.5] - 2020-04-20
+
- Fix errors in embedded JS code.
- Fix diagnostic error for values in quotes.
## [0.8.3] - 2020-03-23
+
- Support for custom address in isfs.
- Multi select in explorer view for mass export.
## [0.8.2] - 2020-03-04
+
- Show current place (label+pos^routine) in status bar for INT code.
- Fix syntax highlighting.
- Support for ${namespace} in links.
## [0.8.1] - 2020-02-06
+
- Some small fixes in filtering for isfs.
- Fixed connection info in Explorer.
- Extra links for server.
@@ -830,6 +910,7 @@ Minimum VS Code version is now 1.66.0
- Password prompt, live connection status.
## [0.8.0]
+
- "Debug this ClassMethod" feature added, to quickly debug any classmethod in a class
- Change variable value while debugging
- When virtual filesystem `isfs://` used, now possible to execute some actions from Studio Source class menu
diff --git a/README.md b/README.md
index e9cbb239..a6c7427e 100644
--- a/README.md
+++ b/README.md
@@ -48,23 +48,28 @@ Open VS Code. Go to Extensions view (⌘/Ctrl+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.2.0`, look for `3.2.1-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.2.1-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.2.0`, look for `3.2.1-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.2.1-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.
```
diff --git a/package.json b/package.json
index 0efb9761..2c2f2b77 100644
--- a/package.json
+++ b/package.json
@@ -127,6 +127,10 @@
"command": "vscode-objectscript.viewOthers",
"when": "vscode-objectscript.connectActive"
},
+ {
+ "command": "vscode-objectscript.getGlobalDocumentation",
+ "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive"
+ },
{
"command": "vscode-objectscript.subclass",
"when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive"
@@ -478,6 +482,15 @@
}
],
"editor/context": [
+ {
+ "command": "vscode-objectscript.ccs.goToDefinition",
+ "group": "navigation@0",
+ "when": "editorTextFocus && editorLangId =~ /^objectscript/"
+ },
+ {
+ "command": "-editor.action.revealDefinition",
+ "when": "editorLangId =~ /^objectscript/"
+ },
{
"command": "vscode-objectscript.viewOthers",
"when": "vscode-objectscript.connectActive",
@@ -525,6 +538,16 @@
}
],
"editor/title": [
+ {
+ "command": "vscode-objectscript.ccs.goToDefinition",
+ "group": "navigation@0",
+ "when": "editorTextFocus && editorLangId =~ /^objectscript/"
+ },
+ {
+ "command": "-editor.action.revealDefinition",
+ "group": "navigation@0",
+ "when": "editorLangId =~ /^objectscript/"
+ },
{
"command": "vscode-objectscript.serverCommands.sourceControl",
"group": "navigation@1",
@@ -839,6 +862,27 @@
"command": "vscode-objectscript.export",
"title": "Export Code from Server"
},
+ {
+ "category": "ObjectScript",
+ "command": "vscode-objectscript.resolveContextExpression",
+ "title": "Resolve Context Expression"
+ },
+ {
+ "category": "ObjectScript",
+ "command": "vscode-objectscript.getGlobalDocumentation",
+ "title": "Show Global Documentation",
+ "enablement": "vscode-objectscript.connectActive"
+ },
+ {
+ "category": "ObjectScript",
+ "command": "vscode-objectscript.ccs.goToDefinition",
+ "title": "Go to Definition"
+ },
+ {
+ "category": "ObjectScript",
+ "command": "vscode-objectscript.ccs.followDefinitionLink",
+ "title": "Follow Definition Link"
+ },
{
"category": "ObjectScript",
"command": "vscode-objectscript.compile",
@@ -938,6 +982,12 @@
"enablement": "vscode-objectscript.connectActive",
"title": "View Other"
},
+ {
+ "category": "ObjectScript",
+ "command": "vscode-objectscript.getGlobalDocumentation",
+ "enablement": "vscode-objectscript.connectActive",
+ "title": "Show Global Documentation"
+ },
{
"category": "ObjectScript",
"command": "vscode-objectscript.subclass",
@@ -1207,6 +1257,11 @@
}
],
"keybindings": [
+ {
+ "command": "vscode-objectscript.ccs.goToDefinition",
+ "key": "F12",
+ "when": "editorTextFocus && editorLangId =~ /^objectscript/"
+ },
{
"command": "vscode-objectscript.compile",
"key": "Ctrl+F7",
@@ -1219,6 +1274,18 @@
"mac": "Cmd+Shift+F7",
"when": "editorLangId =~ /^objectscript/"
},
+ {
+ "command": "vscode-objectscript.resolveContextExpression",
+ "key": "Ctrl+Alt+Space",
+ "mac": "Cmd+Alt+Space",
+ "when": "editorTextFocus && editorLangId =~ /^objectscript/"
+ },
+ {
+ "command": "vscode-objectscript.getGlobalDocumentation",
+ "key": "Ctrl+Q",
+ "mac": "Cmd+Q",
+ "when": "editorTextFocus && editorLangId =~ /^objectscript/"
+ },
{
"command": "vscode-objectscript.viewOthers",
"key": "Ctrl+Shift+V",
diff --git a/src/api/index.ts b/src/api/index.ts
index 4bdc2073..b24cc809 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -719,6 +719,10 @@ export class AtelierAPI {
});
}
+ public getGlobalDocumentation(body: { selectedText: string }): Promise {
+ return this.request(1, "POST", `${this.ns}/action/getglobaldocumentation`, body);
+ }
+
// v1+
public actionCompile(docs: string[], flags?: string, source = false): Promise {
docs = docs.map((doc) => this.transformNameIfCsp(doc));
diff --git a/src/ccs/CHANGELOG.md b/src/ccs/CHANGELOG.md
new file mode 100644
index 00000000..69299d34
--- /dev/null
+++ b/src/ccs/CHANGELOG.md
@@ -0,0 +1,20 @@
+# CCS Change Log
+
+## [3.2.0] 06-Oct-2025
+
+> Based on [InterSystems VSCode ObjectScript v3.2.0 changelog](https://github.com/intersystems-community/vscode-objectscript/blob/v3.2.0/CHANGELOG.md)
+
+- Enhancements
+ - Add ObjectScript enter rules for semicolon (`;`) continuation on line break (#5)
+ - Auto-indent dot syntax on Enter for `objectscript`/`objectscript-int` (replicates leading dots) (#6)
+ - Added `resolveContextExpression` command: posts current line/routine to API, inserts returned code on success, shows error otherwise (#7)
+ - Reorganize CCS module structure into `src/ccs/` with separated folders for config, core, sourcecontrol, and commands (#12)
+ - Add `core/` and centralized `config/` scaffolds for internal module structuring (#14)
+ - Introduce `ContextExpressionClient` and centralized route handling for CCS API calls (#15)
+ - Reorganize SourceControl API into dedicated `clients/` folder (#16)
+ - Add Ctrl+Q to fetch global documentation from selection and print to Output (#17)
+ - Unify Go to Definition (F12) and Ctrl+Click through CCS API resolution (#20)
+- Fixes
+ - Prevent unwanted semicolon insertion on ObjectScript line breaks (#13)
+ - Fix prettier `Insert enter` error in ObjectScript editor rules (#10)
+ - Ensure consistent indentation and formatting for `.mac` and `.int` routines (#11)
diff --git a/src/ccs/commands/contextHelp.ts b/src/ccs/commands/contextHelp.ts
new file mode 100644
index 00000000..40861e76
--- /dev/null
+++ b/src/ccs/commands/contextHelp.ts
@@ -0,0 +1,48 @@
+import * as path from "path";
+import * as vscode from "vscode";
+
+import { ContextExpressionClient } from "../sourcecontrol/clients/contextExpressionClient";
+import { handleError } from "../../utils";
+
+const sharedClient = new ContextExpressionClient();
+
+export async function resolveContextExpression(): Promise {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ return;
+ }
+
+ const { document, selection } = editor;
+ const contextExpression = selection.isEmpty
+ ? document.lineAt(selection.active.line).text.trim()
+ : document.getText(selection).trim();
+
+ if (!contextExpression) {
+ void vscode.window.showErrorMessage("Context expression is empty.");
+ return;
+ }
+
+ const routine = path.basename(document.fileName);
+
+ try {
+ const response = await sharedClient.resolve(document, { routine, contextExpression });
+ const data = response ?? {};
+
+ if (typeof data.status === "string" && data.status.toLowerCase() === "success" && data.textExpression) {
+ const eol = document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n";
+ const textExpression = data.textExpression.replace(/\r?\n/g, eol);
+ const formattedTextExpression = textExpression.replace(/^/, "\t");
+ const rangeToReplace = selection.isEmpty
+ ? document.lineAt(selection.active.line).range
+ : new vscode.Range(selection.start, selection.end);
+ await editor.edit((editBuilder) => {
+ editBuilder.replace(rangeToReplace, formattedTextExpression);
+ });
+ } else {
+ const errorMessage = data.message || "Failed to resolve context expression.";
+ void vscode.window.showErrorMessage(errorMessage);
+ }
+ } catch (error) {
+ handleError(error, "Failed to resolve context expression.");
+ }
+}
diff --git a/src/ccs/commands/globalDocumentation.ts b/src/ccs/commands/globalDocumentation.ts
new file mode 100644
index 00000000..938481f7
--- /dev/null
+++ b/src/ccs/commands/globalDocumentation.ts
@@ -0,0 +1,48 @@
+import * as vscode from "vscode";
+
+import { GlobalDocumentationClient } from "../sourcecontrol/clients/globalDocumentationClient";
+import { handleError, outputChannel } from "../../utils";
+
+const sharedClient = new GlobalDocumentationClient();
+
+function getSelectedOrCurrentLineText(editor: vscode.TextEditor): string {
+ const { selection, document } = editor;
+
+ if (!selection || selection.isEmpty) {
+ return document.lineAt(selection.active.line).text.trim();
+ }
+
+ return document.getText(selection).trim();
+}
+
+export async function showGlobalDocumentation(): Promise {
+ const editor = vscode.window.activeTextEditor;
+
+ if (!editor) {
+ return;
+ }
+
+ const selectedText = getSelectedOrCurrentLineText(editor);
+
+ if (!selectedText) {
+ void vscode.window.showErrorMessage("Selection is empty. Select text or place the cursor on a line with content.");
+ return;
+ }
+
+ try {
+ const content = await sharedClient.fetch(editor.document, { selectedText });
+
+ if (!content || !content.trim()) {
+ void vscode.window.showInformationMessage("Global documentation did not return any content.");
+ return;
+ }
+
+ outputChannel.appendLine("==================== Global Documentation ====================");
+ for (const line of content.split(/\r?\n/)) {
+ outputChannel.appendLine(line);
+ }
+ outputChannel.show(true);
+ } catch (error) {
+ handleError(error, "Failed to retrieve global documentation.");
+ }
+}
diff --git a/src/ccs/commands/goToDefinitionLocalFirst.ts b/src/ccs/commands/goToDefinitionLocalFirst.ts
new file mode 100644
index 00000000..31d48b38
--- /dev/null
+++ b/src/ccs/commands/goToDefinitionLocalFirst.ts
@@ -0,0 +1,26 @@
+import * as vscode from "vscode";
+
+import { lookupCcsDefinition } from "../features/definitionLookup/lookup";
+
+export async function goToDefinitionLocalFirst(): Promise {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ return;
+ }
+
+ const { document, selection } = editor;
+ const position = selection.active;
+ const tokenSource = new vscode.CancellationTokenSource();
+
+ try {
+ const location = await lookupCcsDefinition(document, position, tokenSource.token);
+ if (location) {
+ await vscode.window.showTextDocument(location.uri, { selection: location.range });
+ return;
+ }
+ } finally {
+ tokenSource.dispose();
+ }
+
+ await vscode.commands.executeCommand("editor.action.revealDefinition");
+}
diff --git a/src/ccs/config/schema.md b/src/ccs/config/schema.md
new file mode 100644
index 00000000..34d7f689
--- /dev/null
+++ b/src/ccs/config/schema.md
@@ -0,0 +1,13 @@
+# Configuração do módulo CCS
+
+As opções abaixo ficam no escopo `objectscript.ccs` e controlam as integrações específicas
+para o fork da Consistem.
+
+| Chave | Tipo | Padrão | Descrição |
+| ---------------- | ------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------- |
+| `endpoint` | `string` | `undefined` | URL base alternativa para a API. Se não definida, a URL é derivada da conexão ativa do Atelier. |
+| `requestTimeout` | `number` | `500` | Tempo limite (ms) aplicado às chamadas HTTP do módulo. Valores menores ou inválidos são normalizados para zero. |
+| `debugLogging` | `boolean` | `false` | Quando verdadeiro, registra mensagens detalhadas no `ObjectScript` Output Channel. |
+| `flags` | `Record` | `{}` | Feature flags opcionais que podem ser lidas pelas features do módulo. |
+
+Essas configurações não exigem reload da janela; toda leitura é feita sob demanda.
diff --git a/src/ccs/config/settings.ts b/src/ccs/config/settings.ts
new file mode 100644
index 00000000..e641142e
--- /dev/null
+++ b/src/ccs/config/settings.ts
@@ -0,0 +1,51 @@
+import * as vscode from "vscode";
+
+export interface CcsSettings {
+ endpoint?: string;
+ requestTimeout: number;
+ debugLogging: boolean;
+ flags: Record;
+}
+
+const CCS_CONFIGURATION_SECTION = "objectscript.ccs";
+const DEFAULT_TIMEOUT = 500;
+
+export function getCcsSettings(): CcsSettings {
+ const configuration = vscode.workspace.getConfiguration(CCS_CONFIGURATION_SECTION);
+ const endpoint = sanitizeEndpoint(configuration.get("endpoint"));
+ const requestTimeout = coerceTimeout(configuration.get("requestTimeout"));
+ const debugLogging = Boolean(configuration.get("debugLogging"));
+ const flags = configuration.get>("flags") ?? {};
+
+ return {
+ endpoint,
+ requestTimeout,
+ debugLogging,
+ flags,
+ };
+}
+
+export function isFlagEnabled(flag: string, settings: CcsSettings = getCcsSettings()): boolean {
+ return Boolean(settings.flags?.[flag]);
+}
+
+function sanitizeEndpoint(endpoint?: string): string | undefined {
+ if (!endpoint) {
+ return undefined;
+ }
+
+ const trimmed = endpoint.trim();
+ if (!trimmed) {
+ return undefined;
+ }
+
+ return trimmed.replace(/\/+$/, "");
+}
+
+function coerceTimeout(timeout: number | undefined): number {
+ if (typeof timeout !== "number" || Number.isNaN(timeout)) {
+ return DEFAULT_TIMEOUT;
+ }
+
+ return Math.max(0, Math.floor(timeout));
+}
diff --git a/src/ccs/core/http.ts b/src/ccs/core/http.ts
new file mode 100644
index 00000000..50254aea
--- /dev/null
+++ b/src/ccs/core/http.ts
@@ -0,0 +1,81 @@
+import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from "axios";
+import * as https from "https";
+import * as vscode from "vscode";
+
+import { logDebug, logError } from "./logging";
+import { getCcsSettings } from "../config/settings";
+
+interface CreateClientOptions {
+ baseURL: string;
+ auth?: AxiosRequestConfig["auth"];
+ defaultTimeout?: number;
+}
+
+export function createHttpClient(options: CreateClientOptions): AxiosInstance {
+ const { baseURL, auth, defaultTimeout } = options;
+ const strictSSL = vscode.workspace.getConfiguration("http").get("proxyStrictSSL");
+ const httpsAgent = new https.Agent({ rejectUnauthorized: strictSSL });
+ const timeout = typeof defaultTimeout === "number" ? defaultTimeout : getCcsSettings().requestTimeout;
+
+ const client = axios.create({
+ baseURL,
+ auth,
+ timeout,
+ headers: { "Content-Type": "application/json" },
+ httpsAgent,
+ });
+
+ attachLogging(client);
+
+ return client;
+}
+
+function attachLogging(client: AxiosInstance): void {
+ client.interceptors.request.use((config) => {
+ logDebug(`HTTP ${config.method?.toUpperCase()} ${resolveFullUrl(client, config)}`);
+ return config;
+ });
+
+ client.interceptors.response.use(
+ (response) => {
+ logDebug(`HTTP ${response.status} ${resolveFullUrl(client, response.config)}`);
+ return response;
+ },
+ (error: AxiosError) => {
+ if (axios.isCancel(error)) {
+ logDebug("HTTP request cancelled");
+ return Promise.reject(error);
+ }
+
+ const status = error.response?.status;
+ const url = resolveFullUrl(client, error.config ?? {});
+ const message = typeof status === "number" ? `HTTP ${status} ${url}` : `HTTP request failed ${url}`;
+ logError(message, error);
+ return Promise.reject(error);
+ }
+ );
+}
+
+function resolveFullUrl(client: AxiosInstance, config: AxiosRequestConfig | InternalAxiosRequestConfig): string {
+ const base = config.baseURL ?? client.defaults.baseURL ?? "";
+ const url = config.url ?? "";
+ if (!base) {
+ return url;
+ }
+
+ if (/^https?:/i.test(url)) {
+ return url;
+ }
+
+ return `${base}${url}`;
+}
+
+export function createAbortSignal(token: vscode.CancellationToken): { signal: AbortSignal; dispose: () => void } {
+ const controller = new AbortController();
+ const subscription = token.onCancellationRequested(() => controller.abort());
+
+ return {
+ signal: controller.signal,
+ dispose: () => subscription.dispose(),
+ };
+}
diff --git a/src/ccs/core/logging.ts b/src/ccs/core/logging.ts
new file mode 100644
index 00000000..4c23968b
--- /dev/null
+++ b/src/ccs/core/logging.ts
@@ -0,0 +1,52 @@
+import { inspect } from "util";
+
+import { outputChannel } from "../../utils";
+import { getCcsSettings } from "../config/settings";
+
+type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR";
+
+const PREFIX = "[CCS]";
+
+export function logDebug(message: string, ...details: unknown[]): void {
+ if (!getCcsSettings().debugLogging) {
+ return;
+ }
+ writeLog("DEBUG", message, details);
+}
+
+export function logInfo(message: string, ...details: unknown[]): void {
+ writeLog("INFO", message, details);
+}
+
+export function logWarn(message: string, ...details: unknown[]): void {
+ writeLog("WARN", message, details);
+}
+
+export function logError(message: string, error?: unknown): void {
+ const details = error ? [formatError(error)] : [];
+ writeLog("ERROR", message, details);
+}
+
+function writeLog(level: LogLevel, message: string, details: unknown[]): void {
+ const timestamp = new Date().toISOString();
+ outputChannel.appendLine(`${PREFIX} ${timestamp} ${level}: ${message}`);
+ if (details.length > 0) {
+ for (const detail of details) {
+ outputChannel.appendLine(`${PREFIX} ${stringify(detail)}`);
+ }
+ }
+}
+
+function stringify(value: unknown): string {
+ if (typeof value === "string") {
+ return value;
+ }
+ return inspect(value, { depth: 4, breakLength: Infinity });
+}
+
+function formatError(error: unknown): string {
+ if (error instanceof Error) {
+ return `${error.name}: ${error.message}${error.stack ? `\n${error.stack}` : ""}`;
+ }
+ return stringify(error);
+}
diff --git a/src/ccs/core/types.ts b/src/ccs/core/types.ts
new file mode 100644
index 00000000..707eb8ba
--- /dev/null
+++ b/src/ccs/core/types.ts
@@ -0,0 +1,21 @@
+export interface LocationJSON {
+ uri?: string;
+ line?: number;
+}
+
+export type ResolveDefinitionResponse = LocationJSON;
+
+export interface ResolveContextExpressionResponse {
+ status?: string;
+ textExpression?: string;
+ message?: string;
+}
+
+export interface SourceControlError {
+ message: string;
+ cause?: unknown;
+}
+export interface GlobalDocumentationResponse {
+ content?: string | string[] | Record | null;
+ message?: string;
+}
diff --git a/src/ccs/features/definitionLookup/extractQuery.ts b/src/ccs/features/definitionLookup/extractQuery.ts
new file mode 100644
index 00000000..4d214c09
--- /dev/null
+++ b/src/ccs/features/definitionLookup/extractQuery.ts
@@ -0,0 +1,311 @@
+import * as vscode from "vscode";
+
+export type QueryKind = "labelRoutine" | "routine" | "macro" | "class";
+
+export interface QueryMatch {
+ query: string;
+ normalizedQuery: string;
+ kind: QueryKind;
+ symbolName?: string;
+ range: vscode.Range;
+}
+
+type DefinitionToken = QueryMatch & { activationRange: vscode.Range };
+
+const LABEL_ROUTINE_REGEX = /\$\$([%A-Za-z][\w]*)\^([%A-Za-z][\w]*(?:\.[%A-Za-z][\w]*)*)/g;
+const ROUTINE_INVOCATION_KEYWORDS = ["do", "job"];
+const ROUTINE_INVOCATION_PATTERN = ROUTINE_INVOCATION_KEYWORDS.join("|");
+const COMMAND_LABEL_ROUTINE_REGEX = new RegExp(
+ `\\b(?:${ROUTINE_INVOCATION_PATTERN})\\b\\s+([%A-Za-z][\\w]*)\\^([%A-Za-z][\\w]*(?:\\.[%A-Za-z][\\w]*)*)`,
+ "gi"
+);
+const COMMAND_ROUTINE_REGEX = new RegExp(
+ `\\b(?:${ROUTINE_INVOCATION_PATTERN})\\b\\s+\\^([%A-Za-z][\\w]*(?:\\.[%A-Za-z][\\w]*)*)`,
+ "gi"
+);
+const MACRO_REGEX = /\${3}([%A-Za-z][%A-Za-z0-9_]*)/g;
+const CLASS_REFERENCE_REGEX = new RegExp(
+ "##class\\s*\\(\\s*([%A-Za-z][\\w]*(?:\\.[%A-Za-z][\\w]*)*)\\s*\\)(?:\\s*\\.\\s*([%A-Za-z][\\w]*))?",
+ "gi"
+);
+
+export function extractDefinitionQuery(
+ document: vscode.TextDocument,
+ position: vscode.Position
+): QueryMatch | undefined {
+ const line = position.line;
+ const lineText = document.lineAt(line).text;
+ const tokens = collectDefinitionTokens(lineText, line);
+
+ const directMatch = tokens.find((token) => containsPosition(token.range, position));
+ if (directMatch) {
+ return withoutActivationRange(directMatch);
+ }
+
+ const activationMatch = tokens.find((token) => containsPosition(token.activationRange, position));
+ if (activationMatch) {
+ return withoutActivationRange(activationMatch);
+ }
+
+ return undefined;
+}
+
+export function extractDefinitionQueries(document: vscode.TextDocument): QueryMatch[] {
+ const matches: QueryMatch[] = [];
+ for (let line = 0; line < document.lineCount; line++) {
+ const lineText = document.lineAt(line).text;
+ const tokens = collectDefinitionTokens(lineText, line);
+ for (const token of tokens) {
+ matches.push(withoutActivationRange(token));
+ }
+ }
+ return matches;
+}
+
+interface MatchContext {
+ line: number;
+ start: number;
+ text: string;
+ match: RegExpExecArray;
+}
+
+interface DefinitionMatcher {
+ regex: RegExp;
+ buildTokens(context: MatchContext): DefinitionToken[];
+}
+
+const MATCHERS: DefinitionMatcher[] = [
+ {
+ regex: LABEL_ROUTINE_REGEX,
+ buildTokens: ({ line, start, text, match }) => {
+ const [, labelName, routineName] = match;
+ const normalized = `${labelName}^${routineName}`;
+ const labelStart = start + 2;
+ const labelEnd = labelStart + labelName.length;
+ const caretIndex = text.indexOf("^");
+ if (caretIndex < 0) {
+ return [];
+ }
+ const caretColumn = start + caretIndex;
+ const routineStart = caretColumn + 1;
+ const routineEnd = routineStart + routineName.length;
+
+ return [
+ createToken({
+ line,
+ start: labelStart,
+ end: labelEnd,
+ query: text,
+ normalizedQuery: normalized,
+ kind: "labelRoutine",
+ symbolName: routineName,
+ }),
+ createToken({
+ line,
+ start: routineStart,
+ end: routineEnd,
+ activationStart: caretColumn,
+ query: `^${routineName}`,
+ normalizedQuery: `^${routineName}`,
+ kind: "routine",
+ symbolName: routineName,
+ }),
+ ];
+ },
+ },
+ {
+ regex: COMMAND_LABEL_ROUTINE_REGEX,
+ buildTokens: ({ line, start, text, match }) => {
+ const [, labelName, routineName] = match;
+ const normalized = `${labelName}^${routineName}`;
+ const labelOffset = text.indexOf(labelName);
+ if (labelOffset < 0) {
+ return [];
+ }
+ const labelStart = start + labelOffset;
+ const labelEnd = labelStart + labelName.length;
+ const caretIndex = text.indexOf("^");
+ if (caretIndex < 0) {
+ return [];
+ }
+ const caretColumn = start + caretIndex;
+ const routineOffset = text.lastIndexOf(routineName);
+ if (routineOffset < 0) {
+ return [];
+ }
+ const routineStart = start + routineOffset;
+ const routineEnd = routineStart + routineName.length;
+
+ return [
+ createToken({
+ line,
+ start: labelStart,
+ end: labelEnd,
+ query: normalized,
+ normalizedQuery: normalized,
+ kind: "labelRoutine",
+ symbolName: routineName,
+ }),
+ createToken({
+ line,
+ start: routineStart,
+ end: routineEnd,
+ activationStart: caretColumn,
+ query: `^${routineName}`,
+ normalizedQuery: `^${routineName}`,
+ kind: "routine",
+ symbolName: routineName,
+ }),
+ ];
+ },
+ },
+ {
+ regex: COMMAND_ROUTINE_REGEX,
+ buildTokens: ({ line, start, text, match }) => {
+ const [, routineName] = match;
+ const caretIndex = text.indexOf("^");
+ if (caretIndex < 0) {
+ return [];
+ }
+ const caretColumn = start + caretIndex;
+ const routineOffset = text.lastIndexOf(routineName);
+ if (routineOffset < 0) {
+ return [];
+ }
+ const routineStart = start + routineOffset;
+ const routineEnd = routineStart + routineName.length;
+
+ return [
+ createToken({
+ line,
+ start: routineStart,
+ end: routineEnd,
+ activationStart: caretColumn,
+ query: `^${routineName}`,
+ normalizedQuery: `^${routineName}`,
+ kind: "routine",
+ symbolName: routineName,
+ }),
+ ];
+ },
+ },
+ {
+ regex: MACRO_REGEX,
+ buildTokens: ({ line, start, text, match }) => {
+ const [, macroName] = match;
+ const macroStart = start + (text.length - macroName.length);
+ if (macroStart < start) {
+ return [];
+ }
+ const macroEnd = macroStart + macroName.length;
+
+ return [
+ createToken({
+ line,
+ start: macroStart,
+ end: macroEnd,
+ activationStart: start,
+ query: text,
+ normalizedQuery: text,
+ kind: "macro",
+ symbolName: macroName,
+ }),
+ ];
+ },
+ },
+ {
+ regex: CLASS_REFERENCE_REGEX,
+ buildTokens: ({ line, start, text, match }) => {
+ const [, className, methodName] = match;
+ const classOffset = text.indexOf(className);
+ if (classOffset < 0) {
+ return [];
+ }
+ const classStart = start + classOffset;
+ const classEnd = classStart + className.length;
+ const tokens: DefinitionToken[] = [
+ createToken({
+ line,
+ start: classStart,
+ end: classEnd,
+ query: `##class(${className})`,
+ normalizedQuery: `##class(${className})`,
+ kind: "class",
+ symbolName: className,
+ }),
+ ];
+
+ if (methodName) {
+ const methodOffset = text.lastIndexOf(methodName);
+ if (methodOffset < 0) {
+ return tokens;
+ }
+ const methodStart = start + methodOffset;
+ const methodEnd = methodStart + methodName.length;
+ tokens.push(
+ createToken({
+ line,
+ start: methodStart,
+ end: methodEnd,
+ query: `##class(${className}).${methodName}`,
+ normalizedQuery: `##class(${className}).${methodName}`,
+ kind: "class",
+ symbolName: className,
+ })
+ );
+ }
+
+ return tokens;
+ },
+ },
+];
+
+function collectDefinitionTokens(lineText: string, line: number): DefinitionToken[] {
+ const tokens: DefinitionToken[] = [];
+ for (const matcher of MATCHERS) {
+ const regex = cloneRegex(matcher.regex);
+ let match: RegExpExecArray | null;
+ while ((match = regex.exec(lineText)) !== null) {
+ tokens.push(...matcher.buildTokens({ line, start: match.index, text: match[0], match }));
+ if (!regex.global) {
+ break;
+ }
+ }
+ }
+ return tokens;
+}
+
+function createToken(options: {
+ line: number;
+ start: number;
+ end: number;
+ activationStart?: number;
+ query: string;
+ normalizedQuery: string;
+ kind: QueryKind;
+ symbolName?: string;
+}): DefinitionToken {
+ const { line, start, end, activationStart = start } = options;
+ const activationEnd = Math.max(end, activationStart + 1);
+ return {
+ query: options.query,
+ normalizedQuery: options.normalizedQuery,
+ kind: options.kind,
+ symbolName: options.symbolName,
+ range: new vscode.Range(line, start, line, end),
+ activationRange: new vscode.Range(line, activationStart, line, activationEnd),
+ };
+}
+
+function withoutActivationRange(token: DefinitionToken): QueryMatch {
+ const { activationRange: _activationRange, ...rest } = token;
+ return rest;
+}
+
+function containsPosition(range: vscode.Range, position: vscode.Position): boolean {
+ return position.isAfterOrEqual(range.start) && position.isBefore(range.end);
+}
+
+function cloneRegex(regex: RegExp): RegExp {
+ return new RegExp(regex.source, regex.flags);
+}
diff --git a/src/ccs/features/definitionLookup/lookup.ts b/src/ccs/features/definitionLookup/lookup.ts
new file mode 100644
index 00000000..7513bf58
--- /dev/null
+++ b/src/ccs/features/definitionLookup/lookup.ts
@@ -0,0 +1,82 @@
+import * as vscode from "vscode";
+
+import { ResolveDefinitionClient } from "../../sourcecontrol/clients/resolveDefinitionClient";
+import { currentFile, CurrentTextFile } from "../../../utils";
+import { extractDefinitionQuery, QueryMatch } from "./extractQuery";
+
+const sharedClient = new ResolveDefinitionClient();
+
+export interface LookupOptions {
+ client?: ResolveDefinitionClient;
+ onNoResult?: (details: { query: string; originalQuery?: string }) => void;
+}
+
+export async function lookupCcsDefinition(
+ document: vscode.TextDocument,
+ position: vscode.Position,
+ token: vscode.CancellationToken,
+ options: LookupOptions = {}
+): Promise {
+ const match = extractDefinitionQuery(document, position);
+ if (!match) {
+ return undefined;
+ }
+
+ if (!shouldUseExternalResolver(document, match)) {
+ return undefined;
+ }
+
+ const client = options.client ?? sharedClient;
+ const location = await client.resolve(document, match.normalizedQuery, token);
+ if (!location) {
+ options.onNoResult?.({ query: match.normalizedQuery, originalQuery: match.query });
+ }
+ return location;
+}
+
+function shouldUseExternalResolver(document: vscode.TextDocument, match: QueryMatch): boolean {
+ const current = currentFile(document);
+ if (!current) {
+ return true;
+ }
+
+ switch (match.kind) {
+ case "macro":
+ return !hasLocalMacroDefinition(document, match.symbolName);
+ case "class":
+ return !isCurrentClass(current, match.symbolName);
+ case "labelRoutine":
+ case "routine":
+ return !isCurrentRoutine(current, match.symbolName);
+ default:
+ return true;
+ }
+}
+
+function hasLocalMacroDefinition(document: vscode.TextDocument, macroName?: string): boolean {
+ if (!macroName) {
+ return false;
+ }
+ const regex = new RegExp(`^[\t ]*#def(?:ine|1arg)\\s+${macroName}\\b`, "mi");
+ return regex.test(document.getText());
+}
+
+function isCurrentClass(current: CurrentTextFile, target?: string): boolean {
+ if (!target || !current.name.toLowerCase().endsWith(".cls")) {
+ return false;
+ }
+ const currentClassName = current.name.slice(0, -4);
+ return currentClassName.toLowerCase() === target.toLowerCase();
+}
+
+function isCurrentRoutine(current: CurrentTextFile, target?: string): boolean {
+ if (!target) {
+ return false;
+ }
+ const routineMatch = current.name.match(/^(.*)\.(mac|int|inc)$/i);
+ if (!routineMatch) {
+ return false;
+ }
+ const [, routineName] = routineMatch;
+ return routineName.toLowerCase() === target.toLowerCase();
+}
diff --git a/src/ccs/index.ts b/src/ccs/index.ts
new file mode 100644
index 00000000..88212849
--- /dev/null
+++ b/src/ccs/index.ts
@@ -0,0 +1,21 @@
+export { getCcsSettings, isFlagEnabled, type CcsSettings } from "./config/settings";
+export { logDebug, logError, logInfo, logWarn } from "./core/logging";
+export { SourceControlApi } from "./sourcecontrol/client";
+export { resolveContextExpression } from "./commands/contextHelp";
+export { showGlobalDocumentation } from "./commands/globalDocumentation";
+export { ContextExpressionClient } from "./sourcecontrol/clients/contextExpressionClient";
+export { GlobalDocumentationClient } from "./sourcecontrol/clients/globalDocumentationClient";
+export { ResolveDefinitionClient } from "./sourcecontrol/clients/resolveDefinitionClient";
+export { lookupCcsDefinition, type LookupOptions } from "./features/definitionLookup/lookup";
+export {
+ extractDefinitionQuery,
+ extractDefinitionQueries,
+ type QueryMatch,
+ type QueryKind,
+} from "./features/definitionLookup/extractQuery";
+export { goToDefinitionLocalFirst } from "./commands/goToDefinitionLocalFirst";
+export { PrioritizedDefinitionProvider } from "./providers/PrioritizedDefinitionProvider";
+export {
+ DefinitionDocumentLinkProvider,
+ followDefinitionLinkCommand,
+} from "./providers/DefinitionDocumentLinkProvider";
diff --git a/src/ccs/providers/DefinitionDocumentLinkProvider.ts b/src/ccs/providers/DefinitionDocumentLinkProvider.ts
new file mode 100644
index 00000000..7fea9574
--- /dev/null
+++ b/src/ccs/providers/DefinitionDocumentLinkProvider.ts
@@ -0,0 +1,189 @@
+import * as vscode from "vscode";
+
+import { extractDefinitionQueries } from "../features/definitionLookup/extractQuery";
+
+export const followDefinitionLinkCommand = "vscode-objectscript.ccs.followDefinitionLink";
+
+type TimeoutHandle = ReturnType;
+
+export class DefinitionDocumentLinkProvider implements vscode.DocumentLinkProvider, vscode.Disposable {
+ private readonly decorationType = vscode.window.createTextEditorDecorationType({
+ textDecoration: "none",
+ });
+
+ private readonly _onDidChange = new vscode.EventEmitter();
+
+ public readonly onDidChange: vscode.Event = this._onDidChange.event;
+
+ private readonly supportedLanguages?: Set;
+
+ private readonly subscriptions: vscode.Disposable[] = [];
+
+ private readonly linkRanges = new Map();
+
+ private readonly refreshTimeouts = new Map();
+
+ constructor(supportedLanguages?: readonly string[]) {
+ this.supportedLanguages = supportedLanguages?.length ? new Set(supportedLanguages) : undefined;
+
+ this.subscriptions.push(
+ vscode.window.onDidChangeVisibleTextEditors(() => this.handleVisibleEditorsChange()),
+ vscode.window.onDidChangeActiveTextEditor(() => this.handleVisibleEditorsChange()),
+ vscode.workspace.onDidChangeTextDocument((event) => {
+ if (this.shouldHandleDocument(event.document)) {
+ this.scheduleRefresh(event.document);
+ }
+ }),
+ vscode.workspace.onDidCloseTextDocument((document) => this.clearDocument(document))
+ );
+
+ this.handleVisibleEditorsChange();
+ }
+
+ public provideDocumentLinks(document: vscode.TextDocument): vscode.DocumentLink[] {
+ const queries = extractDefinitionQueries(document);
+ this.updateDocumentRanges(
+ document,
+ queries.map((match) => match.range)
+ );
+
+ return queries.map((match) => {
+ const args = [document.uri.toString(), match.range.start.line, match.range.start.character];
+ const commandUri = vscode.Uri.parse(
+ `command:${followDefinitionLinkCommand}?${encodeURIComponent(JSON.stringify(args))}`
+ );
+ const link = new vscode.DocumentLink(match.range, commandUri);
+ link.tooltip = vscode.l10n.t("Go to Definition");
+ return link;
+ });
+ }
+
+ public dispose(): void {
+ for (const timeout of this.refreshTimeouts.values()) {
+ clearTimeout(timeout);
+ }
+ this.refreshTimeouts.clear();
+
+ for (const disposable of this.subscriptions) {
+ disposable.dispose();
+ }
+
+ for (const editor of vscode.window.visibleTextEditors) {
+ editor.setDecorations(this.decorationType, []);
+ }
+
+ this.linkRanges.clear();
+ this.decorationType.dispose();
+ this._onDidChange.dispose();
+ }
+
+ private handleVisibleEditorsChange(): void {
+ const visibleDocuments = new Set();
+
+ for (const editor of vscode.window.visibleTextEditors) {
+ if (!this.shouldHandleDocument(editor.document)) {
+ editor.setDecorations(this.decorationType, []);
+ continue;
+ }
+
+ const key = editor.document.uri.toString();
+ visibleDocuments.add(key);
+
+ const ranges = this.linkRanges.get(key);
+ if (ranges) {
+ editor.setDecorations(this.decorationType, ranges);
+ } else {
+ editor.setDecorations(this.decorationType, []);
+ this.scheduleRefresh(editor.document);
+ }
+ }
+
+ for (const key of [...this.linkRanges.keys()]) {
+ if (!visibleDocuments.has(key)) {
+ this.linkRanges.delete(key);
+ }
+ }
+ }
+
+ private scheduleRefresh(document: vscode.TextDocument): void {
+ if (document.isClosed || !this.shouldHandleDocument(document)) {
+ return;
+ }
+
+ const key = document.uri.toString();
+ const existing = this.refreshTimeouts.get(key);
+ if (existing) {
+ clearTimeout(existing);
+ }
+
+ const timeout = setTimeout(() => {
+ this.refreshTimeouts.delete(key);
+ if (document.isClosed) {
+ this.clearDocumentByKey(key);
+ return;
+ }
+ const queries = extractDefinitionQueries(document);
+ this.updateDocumentRanges(
+ document,
+ queries.map((match) => match.range)
+ );
+
+ this._onDidChange.fire();
+ }, 50);
+
+ this.refreshTimeouts.set(key, timeout);
+ }
+
+ private updateDocumentRanges(document: vscode.TextDocument, ranges: vscode.Range[]): void {
+ const key = document.uri.toString();
+
+ const existing = this.refreshTimeouts.get(key);
+ if (existing) {
+ clearTimeout(existing);
+ this.refreshTimeouts.delete(key);
+ }
+
+ if (ranges.length > 0) {
+ this.linkRanges.set(key, ranges);
+ } else {
+ this.linkRanges.delete(key);
+ }
+
+ this.applyDecorationsForKey(key, ranges);
+ }
+
+ private clearDocument(document: vscode.TextDocument): void {
+ this.clearDocumentByKey(document.uri.toString());
+ }
+
+ private clearDocumentByKey(key: string): void {
+ const timeout = this.refreshTimeouts.get(key);
+ if (timeout) {
+ clearTimeout(timeout);
+ this.refreshTimeouts.delete(key);
+ }
+
+ this.linkRanges.delete(key);
+ this.applyDecorationsForKey(key, []);
+ }
+
+ private applyDecorationsForKey(key: string, ranges: vscode.Range[]): void {
+ for (const editor of vscode.window.visibleTextEditors) {
+ if (editor.document.uri.toString() === key) {
+ editor.setDecorations(this.decorationType, ranges);
+ }
+ }
+ }
+
+ private shouldHandleDocument(document: vscode.TextDocument): boolean {
+ if (document.isClosed) {
+ return false;
+ }
+
+ if (this.supportedLanguages && !this.supportedLanguages.has(document.languageId)) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/ccs/providers/PrioritizedDefinitionProvider.ts b/src/ccs/providers/PrioritizedDefinitionProvider.ts
new file mode 100644
index 00000000..fdffc809
--- /dev/null
+++ b/src/ccs/providers/PrioritizedDefinitionProvider.ts
@@ -0,0 +1,34 @@
+import * as vscode from "vscode";
+
+import { ObjectScriptDefinitionProvider } from "../../providers/ObjectScriptDefinitionProvider";
+import { lookupCcsDefinition } from "../features/definitionLookup/lookup";
+
+export class PrioritizedDefinitionProvider implements vscode.DefinitionProvider {
+ private readonly delegate: ObjectScriptDefinitionProvider;
+ private readonly lookup: typeof lookupCcsDefinition;
+
+ public constructor(
+ delegate: ObjectScriptDefinitionProvider,
+ lookupFn: typeof lookupCcsDefinition = lookupCcsDefinition
+ ) {
+ this.delegate = delegate;
+ this.lookup = lookupFn;
+ }
+
+ public async provideDefinition(
+ document: vscode.TextDocument,
+ position: vscode.Position,
+ token: vscode.CancellationToken
+ ): Promise {
+ const location = await this.lookup(document, position, token, {
+ onNoResult: () => {
+ // No result from CCS resolver, fallback will be triggered
+ },
+ });
+ if (location) {
+ return location;
+ }
+
+ return this.delegate.provideDefinition(document, position, token);
+ }
+}
diff --git a/src/ccs/sourcecontrol/client.ts b/src/ccs/sourcecontrol/client.ts
new file mode 100644
index 00000000..ba4c05af
--- /dev/null
+++ b/src/ccs/sourcecontrol/client.ts
@@ -0,0 +1,57 @@
+import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
+
+import { AtelierAPI } from "../../api";
+import { getCcsSettings } from "../config/settings";
+import { createHttpClient } from "../core/http";
+import { logDebug } from "../core/logging";
+import { BASE_PATH } from "./routes";
+
+export class SourceControlApi {
+ private readonly client: AxiosInstance;
+
+ private constructor(client: AxiosInstance) {
+ this.client = client;
+ }
+
+ public static fromAtelierApi(api: AtelierAPI): SourceControlApi {
+ const { host, port, username, password, https: useHttps, pathPrefix } = api.config;
+
+ if (!host || !port) {
+ throw new Error("No active InterSystems server connection for this file.");
+ }
+
+ const normalizedPrefix = pathPrefix ? (pathPrefix.startsWith("/") ? pathPrefix : `/${pathPrefix}`) : "";
+ const trimmedPrefix = normalizedPrefix.endsWith("/") ? normalizedPrefix.slice(0, -1) : normalizedPrefix;
+ const encodedPrefix = encodeURI(trimmedPrefix);
+ const protocol = useHttps ? "https" : "http";
+ const defaultBaseUrl = `${protocol}://${host}:${port}${encodedPrefix}${BASE_PATH}`;
+
+ const { endpoint, requestTimeout } = getCcsSettings();
+ const baseURL = endpoint ?? defaultBaseUrl;
+ const auth =
+ typeof username === "string" && typeof password === "string"
+ ? {
+ username,
+ password,
+ }
+ : undefined;
+
+ logDebug("Creating SourceControl API client", { baseURL, hasAuth: Boolean(auth) });
+
+ const client = createHttpClient({
+ baseURL,
+ auth,
+ defaultTimeout: requestTimeout,
+ });
+
+ return new SourceControlApi(client);
+ }
+
+ public post>(
+ route: string,
+ data?: unknown,
+ config?: AxiosRequestConfig
+ ): Promise {
+ return this.client.post(route, data, config);
+ }
+}
diff --git a/src/ccs/sourcecontrol/clients/contextExpressionClient.ts b/src/ccs/sourcecontrol/clients/contextExpressionClient.ts
new file mode 100644
index 00000000..50461039
--- /dev/null
+++ b/src/ccs/sourcecontrol/clients/contextExpressionClient.ts
@@ -0,0 +1,54 @@
+import * as vscode from "vscode";
+
+import { AtelierAPI } from "../../../api";
+import { getCcsSettings } from "../../config/settings";
+import { logDebug } from "../../core/logging";
+import { ResolveContextExpressionResponse } from "../../core/types";
+import { SourceControlApi } from "../client";
+import { ROUTES } from "../routes";
+
+interface ResolveContextExpressionPayload {
+ routine: string;
+ contextExpression: string;
+}
+
+export class ContextExpressionClient {
+ private readonly apiFactory: (api: AtelierAPI) => SourceControlApi;
+
+ public constructor(apiFactory: (api: AtelierAPI) => SourceControlApi = SourceControlApi.fromAtelierApi) {
+ this.apiFactory = apiFactory;
+ }
+
+ public async resolve(
+ document: vscode.TextDocument,
+ payload: ResolveContextExpressionPayload
+ ): Promise {
+ const api = new AtelierAPI(document.uri);
+
+ let sourceControlApi: SourceControlApi;
+ try {
+ sourceControlApi = this.apiFactory(api);
+ } catch (error) {
+ logDebug("Failed to create SourceControl API client for context expression", error);
+ throw error;
+ }
+
+ const { requestTimeout } = getCcsSettings();
+
+ try {
+ const response = await sourceControlApi.post(
+ ROUTES.resolveContextExpression(),
+ payload,
+ {
+ timeout: requestTimeout,
+ validateStatus: (status) => status >= 200 && status < 300,
+ }
+ );
+
+ return response.data ?? {};
+ } catch (error) {
+ logDebug("Context expression resolution failed", error);
+ throw error;
+ }
+ }
+}
diff --git a/src/ccs/sourcecontrol/clients/globalDocumentationClient.ts b/src/ccs/sourcecontrol/clients/globalDocumentationClient.ts
new file mode 100644
index 00000000..6c779661
--- /dev/null
+++ b/src/ccs/sourcecontrol/clients/globalDocumentationClient.ts
@@ -0,0 +1,47 @@
+import * as vscode from "vscode";
+
+import { AtelierAPI } from "../../../api";
+import { getCcsSettings } from "../../config/settings";
+import { logDebug } from "../../core/logging";
+import { SourceControlApi } from "../client";
+import { ROUTES } from "../routes";
+
+interface GlobalDocumentationPayload {
+ selectedText: string;
+}
+
+export class GlobalDocumentationClient {
+ private readonly apiFactory: (api: AtelierAPI) => SourceControlApi;
+
+ public constructor(apiFactory: (api: AtelierAPI) => SourceControlApi = SourceControlApi.fromAtelierApi) {
+ this.apiFactory = apiFactory;
+ }
+
+ public async fetch(document: vscode.TextDocument, payload: GlobalDocumentationPayload): Promise {
+ const api = new AtelierAPI(document.uri);
+
+ let sourceControlApi: SourceControlApi;
+ try {
+ sourceControlApi = this.apiFactory(api);
+ } catch (error) {
+ logDebug("Failed to create SourceControl API client for global documentation", error);
+ throw error;
+ }
+
+ const { requestTimeout } = getCcsSettings();
+
+ try {
+ const response = await sourceControlApi.post(ROUTES.getGlobalDocumentation(), payload, {
+ timeout: requestTimeout,
+ responseType: "text",
+ transformResponse: [(data) => data],
+ validateStatus: (status) => status >= 200 && status < 300,
+ });
+
+ return typeof response.data === "string" ? response.data : "";
+ } catch (error) {
+ logDebug("Global documentation request failed", error);
+ throw error;
+ }
+ }
+}
diff --git a/src/ccs/sourcecontrol/clients/resolveDefinitionClient.ts b/src/ccs/sourcecontrol/clients/resolveDefinitionClient.ts
new file mode 100644
index 00000000..b2875f63
--- /dev/null
+++ b/src/ccs/sourcecontrol/clients/resolveDefinitionClient.ts
@@ -0,0 +1,130 @@
+import axios from "axios";
+import * as vscode from "vscode";
+
+import { AtelierAPI } from "../../../api";
+import { getCcsSettings } from "../../config/settings";
+import { createAbortSignal } from "../../core/http";
+import { logDebug } from "../../core/logging";
+import { ResolveDefinitionResponse } from "../../core/types";
+import { SourceControlApi } from "../client";
+import { ROUTES } from "../routes";
+import { toVscodeLocation } from "../paths";
+
+export class ResolveDefinitionClient {
+ private readonly apiFactory: (api: AtelierAPI) => SourceControlApi;
+
+ public constructor(apiFactory: (api: AtelierAPI) => SourceControlApi = SourceControlApi.fromAtelierApi) {
+ this.apiFactory = apiFactory;
+ }
+
+ private getAdditionalNamespaces(currentApi: AtelierAPI): string[] {
+ const workspaceFolders = vscode.workspace.workspaceFolders;
+ if (!workspaceFolders?.length) {
+ return [];
+ }
+
+ const { host, port } = currentApi.config;
+ const currentPathPrefix = currentApi.config.pathPrefix ?? "";
+ const currentNamespace = currentApi.ns;
+
+ if (!host || !port) {
+ return [];
+ }
+
+ const namespaces = new Set();
+
+ for (const folder of workspaceFolders) {
+ const folderApi = new AtelierAPI(folder.uri);
+ if (!folderApi.active) {
+ continue;
+ }
+
+ const { host: folderHost, port: folderPort } = folderApi.config;
+ const folderPathPrefix = folderApi.config.pathPrefix ?? "";
+
+ if (folderHost !== host || folderPort !== port || folderPathPrefix !== currentPathPrefix) {
+ continue;
+ }
+
+ const folderNamespace = folderApi.ns;
+ if (!folderNamespace || folderNamespace === currentNamespace) {
+ continue;
+ }
+
+ namespaces.add(folderNamespace.toUpperCase());
+ }
+
+ return Array.from(namespaces);
+ }
+
+ public async resolve(
+ document: vscode.TextDocument,
+ query: string,
+ token: vscode.CancellationToken
+ ): Promise {
+ const api = new AtelierAPI(document.uri);
+ const { host, port, username, password } = api.config;
+ const namespace = api.ns;
+
+ if (!api.active || !namespace || !host || !port || !username || !password) {
+ logDebug("CCS definition lookup skipped due to missing connection metadata", {
+ active: api.active,
+ namespace,
+ host,
+ port,
+ username: Boolean(username),
+ password: Boolean(password),
+ });
+ return undefined;
+ }
+
+ let sourceControlApi: SourceControlApi;
+ try {
+ sourceControlApi = this.apiFactory(api);
+ } catch (error) {
+ logDebug("Failed to create SourceControl API client", error);
+ return undefined;
+ }
+
+ const { requestTimeout } = getCcsSettings();
+ const { signal, dispose } = createAbortSignal(token);
+
+ const otherNamespaces = this.getAdditionalNamespaces(api);
+ const otherNamespacesStr = otherNamespaces.join(";");
+ const body = otherNamespaces.length ? { query, otherNamespaces: otherNamespacesStr } : { query };
+
+ logDebug("CCS definition lookup request", {
+ namespace,
+ endpoint: ROUTES.resolveDefinition(namespace),
+ body,
+ });
+
+ try {
+ const response = await sourceControlApi.post(
+ ROUTES.resolveDefinition(namespace),
+ body,
+ {
+ timeout: requestTimeout,
+ signal,
+ validateStatus: (status) => status >= 200 && status < 300,
+ }
+ );
+
+ const location = toVscodeLocation(response.data ?? {});
+ if (!location) {
+ logDebug("CCS definition lookup returned empty payload", response.data);
+ }
+ return location ?? undefined;
+ } catch (error) {
+ if (axios.isCancel(error)) {
+ logDebug("CCS definition lookup cancelled");
+ return undefined;
+ }
+
+ logDebug("CCS definition lookup failed", error);
+ return undefined;
+ } finally {
+ dispose();
+ }
+ }
+}
diff --git a/src/ccs/sourcecontrol/paths.ts b/src/ccs/sourcecontrol/paths.ts
new file mode 100644
index 00000000..c51ddcc8
--- /dev/null
+++ b/src/ccs/sourcecontrol/paths.ts
@@ -0,0 +1,36 @@
+import * as vscode from "vscode";
+
+import { LocationJSON } from "../core/types";
+
+export function normalizeFilePath(filePath: string): string {
+ if (!filePath) {
+ return filePath;
+ }
+
+ const trimmed = filePath.trim();
+ if (/^file:\/\//i.test(trimmed)) {
+ return trimmed.replace(/\\/g, "/");
+ }
+
+ const normalized = trimmed.replace(/\\/g, "/");
+ return normalized;
+}
+
+export function toFileUri(filePath: string): vscode.Uri {
+ const normalized = normalizeFilePath(filePath);
+ if (/^file:\/\//i.test(normalized)) {
+ return vscode.Uri.parse(normalized);
+ }
+
+ return vscode.Uri.file(normalized);
+}
+
+export function toVscodeLocation(location: LocationJSON): vscode.Location | undefined {
+ if (!location.uri || typeof location.line !== "number") {
+ return undefined;
+ }
+
+ const uri = toFileUri(location.uri);
+ const zeroBasedLine = Math.max(0, Math.floor(location.line) - 1);
+ return new vscode.Location(uri, new vscode.Position(zeroBasedLine, 0));
+}
diff --git a/src/ccs/sourcecontrol/routes.ts b/src/ccs/sourcecontrol/routes.ts
new file mode 100644
index 00000000..1561570f
--- /dev/null
+++ b/src/ccs/sourcecontrol/routes.ts
@@ -0,0 +1,9 @@
+export const BASE_PATH = "/api/sourcecontrol/vscode" as const;
+
+export const ROUTES = {
+ resolveContextExpression: () => `/resolveContextExpression`,
+ getGlobalDocumentation: () => `/getGlobalDocumentation`,
+ resolveDefinition: (namespace: string) => `/namespaces/${encodeURIComponent(namespace)}/resolveDefinition`,
+} as const;
+
+export type RouteKey = keyof typeof ROUTES;
diff --git a/src/commands/globalDocumentation.ts b/src/commands/globalDocumentation.ts
new file mode 100644
index 00000000..7ecd2c6a
--- /dev/null
+++ b/src/commands/globalDocumentation.ts
@@ -0,0 +1,60 @@
+import * as vscode from "vscode";
+
+import { AtelierAPI } from "../api";
+import { currentFile, handleError, outputChannel } from "../utils";
+
+function getSelectedOrCurrentLineText(editor: vscode.TextEditor): string {
+ const { selection, document } = editor;
+ if (!selection || selection.isEmpty) {
+ return document.lineAt(selection.active.line).text.trim();
+ }
+ return document.getText(selection).trim();
+}
+
+export async function showGlobalDocumentation(): Promise {
+ const file = currentFile();
+ const editor = vscode.window.activeTextEditor;
+
+ if (!file || !editor) {
+ return;
+ }
+
+ const selectedText = getSelectedOrCurrentLineText(editor);
+
+ if (!selectedText) {
+ void vscode.window.showErrorMessage("Selection is empty. Select text or place the cursor on a line with content.");
+ return;
+ }
+
+ const api = new AtelierAPI(file.uri);
+
+ if (!api.active) {
+ void vscode.window.showErrorMessage("No active connection to retrieve global documentation.");
+ return;
+ }
+
+ try {
+ const response = await api.getGlobalDocumentation({ selectedText });
+ const content = response?.result?.content;
+ let output = "";
+
+ if (Array.isArray(content)) {
+ output = content.join("\n");
+ } else if (typeof content === "string") {
+ output = content;
+ } else if (content && typeof content === "object") {
+ output = JSON.stringify(content, null, 2);
+ }
+
+ if (!output) {
+ void vscode.window.showInformationMessage("Global documentation did not return any content.");
+ return;
+ }
+
+ outputChannel.appendLine("==================== Global Documentation ====================");
+ outputChannel.appendLine(output);
+ outputChannel.show(true);
+ } catch (error) {
+ handleError(error, "Failed to retrieve global documentation.");
+ }
+}
diff --git a/src/extension.ts b/src/extension.ts
index 45c1a4e0..6f6c9133 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -27,6 +27,10 @@ export const incLangId = "objectscript-macros";
export const cspLangId = "objectscript-csp";
export const outputLangId = "vscode-objectscript-output";
+const dotPrefixRegex = /^(\s*(?:\.\s*)+)/;
+const dotIndentLanguages = new Set([macLangId, intLangId]);
+const dotIndentSkipDocuments = new Set();
+
import * as url from "url";
import path = require("path");
import {
@@ -159,6 +163,14 @@ import { WorkspaceNode, NodeBase } from "./explorer/nodes";
import { showPlanWebview } from "./commands/showPlanPanel";
import { isfsConfig } from "./utils/FileProviderUtil";
import { showAllClassMembers } from "./commands/showAllClassMembers";
+import {
+ PrioritizedDefinitionProvider,
+ DefinitionDocumentLinkProvider,
+ followDefinitionLinkCommand,
+ goToDefinitionLocalFirst,
+ resolveContextExpression,
+ showGlobalDocumentation,
+} from "./ccs";
const packageJson = vscode.extensions.getExtension(extensionId).packageJSON;
const extensionVersion = packageJson.version;
@@ -940,6 +952,20 @@ export async function activate(context: vscode.ExtensionContext): Promise {
const documentSelector = (...list) =>
["file", ...schemas].reduce((acc, scheme) => acc.concat(list.map((language) => ({ scheme, language }))), []);
+ const definitionDocumentLinkProvider = new DefinitionDocumentLinkProvider([
+ clsLangId,
+ macLangId,
+ intLangId,
+ incLangId,
+ ]);
+ context.subscriptions.push(
+ definitionDocumentLinkProvider,
+ vscode.languages.registerDocumentLinkProvider(
+ documentSelector(clsLangId, macLangId, intLangId, incLangId),
+ definitionDocumentLinkProvider
+ )
+ );
+
const diagnosticProvider = new ObjectScriptDiagnosticProvider();
// Gather the proposed APIs we will register to use when building with enabledApiProposals != []
@@ -1001,7 +1027,7 @@ export async function activate(context: vscode.ExtensionContext): Promise {
),
vscode.languages.registerDefinitionProvider(
documentSelector(clsLangId, macLangId, intLangId, incLangId),
- new ObjectScriptDefinitionProvider()
+ new PrioritizedDefinitionProvider(new ObjectScriptDefinitionProvider())
),
vscode.languages.registerCompletionItemProvider(
documentSelector(clsLangId, macLangId, intLangId, incLangId),
@@ -1038,6 +1064,75 @@ export async function activate(context: vscode.ExtensionContext): Promise {
}
}
+ context.subscriptions.push(
+ vscode.workspace.onDidChangeTextDocument(async (event) => {
+ if (!dotIndentLanguages.has(event.document.languageId)) {
+ return;
+ }
+
+ const docUriString = event.document.uri.toString();
+ if (dotIndentSkipDocuments.has(docUriString)) {
+ return;
+ }
+
+ const editor = vscode.window.visibleTextEditors.find((e) => e.document === event.document);
+ if (!editor) {
+ return;
+ }
+
+ for (const change of event.contentChanges) {
+ if (!change.text.includes("\n")) {
+ continue;
+ }
+
+ const newLineNumber = change.range.start.line + 1;
+ if (newLineNumber >= event.document.lineCount || newLineNumber <= 0) {
+ continue;
+ }
+
+ const previousLine = event.document.lineAt(newLineNumber - 1).text;
+ const prefixMatch = previousLine.match(dotPrefixRegex);
+ if (!prefixMatch) {
+ continue;
+ }
+
+ let insertText = prefixMatch[1];
+ if (!insertText.endsWith(" ")) {
+ insertText += " ";
+ }
+
+ const remainder = previousLine.slice(prefixMatch[1].length);
+ if (remainder.startsWith(";")) {
+ insertText += ";";
+ }
+
+ const newLine = event.document.lineAt(newLineNumber);
+ if (newLine.text.trim().length > 0) {
+ continue;
+ }
+ if (newLine.text.startsWith(insertText)) {
+ continue;
+ }
+
+ const indentMatch = newLine.text.match(/^\s*/);
+ const indentLength = indentMatch ? indentMatch[0].length : 0;
+ const replaceRange = new vscode.Range(newLine.range.start, new vscode.Position(newLineNumber, indentLength));
+
+ dotIndentSkipDocuments.add(docUriString);
+ try {
+ await editor.edit(
+ (editBuilder) => {
+ editBuilder.replace(replaceRange, insertText);
+ },
+ { undoStopBefore: false, undoStopAfter: false }
+ );
+ } finally {
+ dotIndentSkipDocuments.delete(docUriString);
+ }
+ }
+ })
+ );
+
openedClasses = workspaceState.get("openedClasses") ?? [];
/** The stringified URIs of all `isfs` documents that are currently open in a UI tab */
@@ -1191,6 +1286,36 @@ export async function activate(context: vscode.ExtensionContext): Promise {
sendCommandTelemetryEvent("copyToClipboard");
vscode.env.clipboard.writeText(command);
}),
+ vscode.commands.registerCommand("vscode-objectscript.resolveContextExpression", () => {
+ sendCommandTelemetryEvent("resolveContextExpression");
+ void resolveContextExpression();
+ }),
+ vscode.commands.registerCommand("vscode-objectscript.ccs.goToDefinition", async () => {
+ sendCommandTelemetryEvent("ccs.goToDefinition");
+ await goToDefinitionLocalFirst();
+ }),
+ vscode.commands.registerCommand(
+ followDefinitionLinkCommand,
+ async (documentUri: string, line: number, character: number) => {
+ sendCommandTelemetryEvent("ccs.followDefinitionLink");
+ if (!documentUri || typeof line !== "number" || typeof character !== "number") {
+ return;
+ }
+
+ const uri = vscode.Uri.parse(documentUri);
+ const document =
+ vscode.workspace.textDocuments.find((doc) => doc.uri.toString() === documentUri) ??
+ (await vscode.workspace.openTextDocument(uri));
+
+ const position = new vscode.Position(line, character);
+ const selectionRange = new vscode.Range(position, position);
+ const editor = await vscode.window.showTextDocument(document, { selection: selectionRange });
+ editor.selection = new vscode.Selection(position, position);
+ editor.revealRange(selectionRange);
+
+ await goToDefinitionLocalFirst();
+ }
+ ),
vscode.commands.registerCommand("vscode-objectscript.debug", (program: string, askArgs: boolean) => {
sendCommandTelemetryEvent("debug");
const startDebugging = (args) => {
@@ -1299,6 +1424,10 @@ export async function activate(context: vscode.ExtensionContext): Promise {
sendCommandTelemetryEvent("viewOthers");
viewOthers(false);
}),
+ vscode.commands.registerCommand("vscode-objectscript.getGlobalDocumentation", () => {
+ sendCommandTelemetryEvent("getGlobalDocumentation");
+ void showGlobalDocumentation();
+ }),
vscode.commands.registerCommand("vscode-objectscript.serverCommands.sourceControl", (uri?: vscode.Uri) => {
sendCommandTelemetryEvent("serverCommands.sourceControl");
mainSourceControlMenu(uri);
@@ -2098,3 +2227,4 @@ export async function deactivate(): Promise {
}
await Promise.allSettled(promises);
}
+export { outputChannel };
diff --git a/src/languageConfiguration.ts b/src/languageConfiguration.ts
index 93b9513f..06734f1b 100644
--- a/src/languageConfiguration.ts
+++ b/src/languageConfiguration.ts
@@ -2,6 +2,21 @@ import * as vscode from "vscode";
export function getLanguageConfiguration(lang: string): vscode.LanguageConfiguration {
const conf = vscode.workspace.getConfiguration("objectscript");
+ const onEnterRules: vscode.OnEnterRule[] = [];
+
+ if (lang === "objectscript-class") {
+ onEnterRules.push({
+ beforeText: /^\/\/\//,
+ action: { indentAction: vscode.IndentAction.None, appendText: "/// " },
+ });
+ }
+
+ if (["objectscript", "objectscript-int"].includes(lang)) {
+ onEnterRules.push({
+ beforeText: /^\s*;/,
+ action: { indentAction: vscode.IndentAction.None, appendText: ";" },
+ });
+ }
return {
wordPattern:
/((?<=(class|extends|as|of) )(%?\b[a-z0-9]+(\.[a-z0-9]+)*\b))|(\^[a-z0-9]+(\.[a-z0-9]+)*)|((\${1,3}|[irm]?%|\^|#)?[a-z0-9]+)/i,
@@ -40,14 +55,6 @@ export function getLanguageConfiguration(lang: string): vscode.LanguageConfigura
notIn: [vscode.SyntaxTokenType.Comment, vscode.SyntaxTokenType.String, vscode.SyntaxTokenType.RegEx],
},
],
- onEnterRules:
- lang == "objectscript-class"
- ? [
- {
- beforeText: /^\/\/\//,
- action: { indentAction: vscode.IndentAction.None, appendText: "/// " },
- },
- ]
- : undefined,
+ onEnterRules: onEnterRules.length ? onEnterRules : undefined,
};
}
diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts
index e162ec0b..99be046b 100644
--- a/src/test/suite/extension.test.ts
+++ b/src/test/suite/extension.test.ts
@@ -3,9 +3,21 @@ import { before } from "mocha";
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
+import * as vscode from "vscode";
import { window, extensions } from "vscode";
import { extensionId, smExtensionId } from "../../extension";
+async function waitForCondition(predicate: () => boolean, timeoutMs = 1000, message?: string): Promise {
+ const start = Date.now();
+ while (Date.now() - start < timeoutMs) {
+ if (predicate()) {
+ return;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ }
+ assert.fail(message ?? "Timed out waiting for condition");
+}
+
suite("Extension Test Suite", () => {
suiteSetup(async function () {
// make sure extension is activated
@@ -22,4 +34,57 @@ suite("Extension Test Suite", () => {
test("Sample test", () => {
assert.ok("All good");
});
+
+ test("Dot-prefixed statements continue on newline", async () => {
+ const document = await vscode.workspace.openTextDocument({
+ language: "objectscript",
+ content: " . Do ##class(Test).Run()",
+ });
+ const editor = await vscode.window.showTextDocument(document);
+ try {
+ await editor.edit((editBuilder) => {
+ editBuilder.insert(document.lineAt(0).range.end, "\n");
+ });
+ await waitForCondition(() => document.lineCount > 1);
+ await waitForCondition(() => document.lineAt(1).text.length > 0);
+ assert.strictEqual(document.lineAt(1).text, " . ");
+ } finally {
+ await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
+ }
+ });
+
+ test("Dot-prefixed semicolon comments continue on newline", async () => {
+ const document = await vscode.workspace.openTextDocument({
+ language: "objectscript",
+ content: " . ; Comment",
+ });
+ const editor = await vscode.window.showTextDocument(document);
+ try {
+ await editor.edit((editBuilder) => {
+ editBuilder.insert(document.lineAt(0).range.end, "\n");
+ });
+ await waitForCondition(() => document.lineCount > 1);
+ await waitForCondition(() => document.lineAt(1).text.length > 0);
+ assert.strictEqual(document.lineAt(1).text, " . ;");
+ } finally {
+ await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
+ }
+ });
+
+ test("Moving lines across dot-prefixed semicolon comments doesn't add semicolons", async () => {
+ const document = await vscode.workspace.openTextDocument({
+ language: "objectscript",
+ content: " . Do ##class(Test).Run()\n . ; Comment",
+ });
+ const editor = await vscode.window.showTextDocument(document);
+ try {
+ editor.selection = new vscode.Selection(new vscode.Position(0, 0), new vscode.Position(0, 0));
+ await vscode.commands.executeCommand("editor.action.moveLinesDownAction");
+ const expectedText = " . ; Comment\n . Do ##class(Test).Run()";
+ await waitForCondition(() => document.getText() === expectedText);
+ assert.strictEqual(document.getText(), expectedText);
+ } finally {
+ await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
+ }
+ });
});