From cb593bde29ce27023c157919255d0b4991ee3f4b Mon Sep 17 00:00:00 2001 From: Leonardo Anders <115679546+LeoAnders@users.noreply.github.com> Date: Sun, 21 Sep 2025 00:08:46 -0300 Subject: [PATCH 01/15] feat: enable cross-workspace definition lookup --- CHANGELOG.md | 155 +++++++++++++----- README.md | 28 +++- package.json | 8 + src/providers/DocumentContentProvider.ts | 85 +++++++--- src/test/suite/extension.test.ts | 84 +++++++++- .../multi-root/client/.vscode/settings.json | 12 ++ .../client/src/MultiRoot/Caller.cls | 10 ++ .../multi-root/shared/.vscode/settings.json | 9 + .../shared/src/MultiRoot/Shared.cls | 9 + test-fixtures/test.code-workspace | 10 +- 10 files changed, 341 insertions(+), 69 deletions(-) create mode 100644 test-fixtures/multi-root/client/.vscode/settings.json create mode 100644 test-fixtures/multi-root/client/src/MultiRoot/Caller.cls create mode 100644 test-fixtures/multi-root/shared/.vscode/settings.json create mode 100644 test-fixtures/multi-root/shared/src/MultiRoot/Shared.cls 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..f3b4c9fe 100644 --- a/README.md +++ b/README.md @@ -48,29 +48,51 @@ Open VS Code. Go to Extensions view (/Ctrl+Shift **Implementation developed and maintained by Consistem Sistemas** + +When working in a multi-root workspace, the extension normally searches the current workspace folder (and any sibling folders connected to the same namespace) for local copies of ObjectScript code before requesting the server version. If you keep shared source code in other workspace folders with different connection settings, set the `objectscript.export.searchOtherWorkspaceFolders` array in the consuming folder's settings so those folders are considered first. Use workspace-folder names, or specify `"*"` to search every non-`isfs` folder. + +```json +{ + "objectscript.export": { + "folder": "src", + "searchOtherWorkspaceFolders": ["shared"] + } +} +``` + +With this setting enabled, features such as Go to Definition resolve to the first matching local file across the configured workspace folders before falling back to the server copy. + ## Notes - Connection-related output appears in the 'Output' view while switched to the 'ObjectScript' channel using the drop-down menu on the view titlebar. diff --git a/package.json b/package.json index 0efb9761..5e64fcab 100644 --- a/package.json +++ b/package.json @@ -1406,6 +1406,14 @@ }, "additionalProperties": false }, + "searchOtherWorkspaceFolders": { + "markdownDescription": "Additional workspace folders to search for client-side sources when resolving ObjectScript documents. Specify `\"*\"` to search all non-isfs workspace folders in the current multi-root workspace before falling back to the server.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, "atelier": { "description": "Export source code as Atelier did it, with packages as subfolders. This setting only affects classes, routines, include files and DFI files.", "type": "boolean" diff --git a/src/providers/DocumentContentProvider.ts b/src/providers/DocumentContentProvider.ts index c9eae4a4..24857303 100644 --- a/src/providers/DocumentContentProvider.ts +++ b/src/providers/DocumentContentProvider.ts @@ -165,30 +165,73 @@ export class DocumentContentProvider implements vscode.TextDocumentContentProvid }); } } else { - const conn = config("conn", workspaceFolder); + const conn = config("conn", workspaceFolder) ?? {}; + const exportConfig = + workspaceFolder && workspaceFolder !== "" + ? (config("export", workspaceFolder) as { searchOtherWorkspaceFolders?: string[] }) + : undefined; + const searchOtherWorkspaceFolders = Array.isArray(exportConfig?.searchOtherWorkspaceFolders) + ? exportConfig.searchOtherWorkspaceFolders + .map((value) => (typeof value === "string" ? value.trim() : "")) + .filter((value) => value.length > 0) + : []; + const includeAllFolders = searchOtherWorkspaceFolders.includes("*"); + const explicitAdditionalFolders = new Set( + searchOtherWorkspaceFolders.filter((value) => value !== "*").map((value) => value.toLowerCase()) + ); if (!forceServerCopy) { - // Look for the document in the local file system - const localFile = this.findLocalUri(name, workspaceFolder); - if (localFile && (!namespace || namespace === conn.ns)) { - // Exists as a local file and we aren't viewing a different namespace on the same server, - // so return a uri that will open the local file. + const tryLocalUri = (folderName: string, allowNamespaceMismatch: boolean): vscode.Uri => { + const localFile = this.findLocalUri(name, folderName); + if (!localFile) return; + if (!allowNamespaceMismatch && namespace) { + const folderConn = config("conn", folderName) ?? {}; + if (folderConn.ns && namespace !== folderConn.ns) { + return; + } + } return localFile; - } else { - // The local file doesn't exist in this folder, so check any other - // local folders in this workspace if it's a multi-root workspace - const wFolders = vscode.workspace.workspaceFolders; - if (wFolders && wFolders.length > 1) { - // This is a multi-root workspace + }; + + // Look for the document in the local file system + const primaryLocal = tryLocalUri(workspaceFolder, false); + if (primaryLocal) { + return primaryLocal; + } + + // Check any other eligible local folders in this workspace if it's a multi-root workspace + const wFolders = vscode.workspace.workspaceFolders; + if (wFolders && wFolders.length > 1 && workspaceFolder) { + const candidates: { folder: vscode.WorkspaceFolder; allowNamespaceMismatch: boolean }[] = []; + const seen = new Set(); + const addCandidate = (folder: vscode.WorkspaceFolder, allowNamespaceMismatch: boolean): void => { + if (!notIsfs(folder.uri)) return; + if (folder.name === workspaceFolder) return; + if (seen.has(folder.name)) return; + candidates.push({ folder, allowNamespaceMismatch }); + seen.add(folder.name); + }; + + for (const wFolder of wFolders) { + if (wFolder.name === workspaceFolder) continue; + const wFolderConn = config("conn", wFolder.name) ?? {}; + if (compareConns(conn, wFolderConn) && (!namespace || namespace === wFolderConn.ns)) { + addCandidate(wFolder, false); + } + } + + if (includeAllFolders || explicitAdditionalFolders.size > 0) { for (const wFolder of wFolders) { - if (notIsfs(wFolder.uri) && wFolder.name != workspaceFolder) { - // This isn't the folder that we checked originally - const wFolderConn = config("conn", wFolder.name); - if (compareConns(conn, wFolderConn) && (!namespace || namespace === wFolderConn.ns)) { - // This folder is connected to the same server:ns combination as the original folder - const wFolderFile = this.findLocalUri(name, wFolder.name); - if (wFolderFile) return wFolderFile; - } - } + if (wFolder.name === workspaceFolder) continue; + const shouldInclude = includeAllFolders || explicitAdditionalFolders.has(wFolder.name.toLowerCase()); + if (!shouldInclude) continue; + addCandidate(wFolder, true); + } + } + + for (const candidate of candidates) { + const candidateLocal = tryLocalUri(candidate.folder.name, candidate.allowNamespaceMismatch); + if (candidateLocal) { + return candidateLocal; } } } diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index e162ec0b..d2cdb1a9 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -1,25 +1,99 @@ import * as assert from "assert"; import { before } from "mocha"; +import * as path from "path"; // You can import and use all API from the 'vscode' module // as well as import your extension to test it -import { window, extensions } from "vscode"; -import { extensionId, smExtensionId } from "../../extension"; +import * as vscode from "vscode"; +import { extensionId, smExtensionId, OBJECTSCRIPT_FILE_SCHEMA } from "../../extension"; +import { getUrisForDocument } from "../../utils/documentIndex"; + +async function waitForIndexedDocument(documentName: string, workspaceFolderName: string): Promise { + const workspaceFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === workspaceFolderName); + assert.ok(workspaceFolder, `Workspace folder '${workspaceFolderName}' was not found.`); + const start = Date.now(); + while (Date.now() - start < 10000) { + if (getUrisForDocument(documentName, workspaceFolder).length > 0) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + assert.fail(`Timed out waiting for '${documentName}' to be indexed in workspace folder '${workspaceFolderName}'.`); +} + +function getDefinitionTargets(definitions: (vscode.Location | vscode.DefinitionLink)[]): vscode.Uri[] { + return definitions + .map((definition) => ("targetUri" in definition ? definition.targetUri : definition.uri)) + .filter((uri): uri is vscode.Uri => !!uri); +} suite("Extension Test Suite", () => { suiteSetup(async function () { // make sure extension is activated - const serverManager = extensions.getExtension(smExtensionId); + const serverManager = vscode.extensions.getExtension(smExtensionId); await serverManager?.activate(); - const ext = extensions.getExtension(extensionId); + const ext = vscode.extensions.getExtension(extensionId); await ext?.activate(); }); before(() => { - window.showInformationMessage("Start all tests."); + vscode.window.showInformationMessage("Start all tests."); }); test("Sample test", () => { assert.ok("All good"); }); + + test("Go to Definition resolves to sibling workspace folder", async function () { + this.timeout(10000); + await waitForIndexedDocument("MultiRoot.Shared.cls", "shared"); + const clientFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === "client"); + assert.ok(clientFolder, "Client workspace folder not available."); + const callerUri = vscode.Uri.joinPath(clientFolder.uri, "src", "MultiRoot", "Caller.cls"); + const document = await vscode.workspace.openTextDocument(callerUri); + await vscode.window.showTextDocument(document); + + const target = "MultiRoot.Shared"; + const sharedOffset = document.getText().indexOf(target); + assert.notStrictEqual(sharedOffset, -1, "Shared class reference not found in Caller.cls"); + const position = document.positionAt(sharedOffset + target.indexOf("Shared") + 1); + const definitions = (await vscode.commands.executeCommand( + "vscode.executeDefinitionProvider", + callerUri, + position + )) as (vscode.Location | vscode.DefinitionLink)[]; + assert.ok(definitions?.length, "Expected at least one definition result"); + const targetUris = getDefinitionTargets(definitions); + const sharedTargetSuffix = path.join("shared", "src", "MultiRoot", "Shared.cls"); + assert.ok( + targetUris.some((uri) => uri.scheme === "file" && uri.fsPath.endsWith(sharedTargetSuffix)), + "Expected Go to Definition to resolve to the shared workspace folder" + ); + }); + + test("Go to Definition falls back to server URI when local copy missing", async function () { + this.timeout(10000); + await waitForIndexedDocument("MultiRoot.Shared.cls", "shared"); + const clientFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === "client"); + assert.ok(clientFolder, "Client workspace folder not available."); + const callerUri = vscode.Uri.joinPath(clientFolder.uri, "src", "MultiRoot", "Caller.cls"); + const document = await vscode.workspace.openTextDocument(callerUri); + await vscode.window.showTextDocument(document); + + const target = "MultiRoot.ServerOnly"; + const offset = document.getText().indexOf(target); + assert.notStrictEqual(offset, -1, "Server-only class reference not found in Caller.cls"); + const position = document.positionAt(offset + target.indexOf("ServerOnly") + 1); + const definitions = (await vscode.commands.executeCommand( + "vscode.executeDefinitionProvider", + callerUri, + position + )) as (vscode.Location | vscode.DefinitionLink)[]; + assert.ok(definitions?.length, "Expected definition result when resolving missing class"); + const targetUris = getDefinitionTargets(definitions); + assert.ok( + targetUris.some((uri) => uri.scheme === OBJECTSCRIPT_FILE_SCHEMA), + "Expected Go to Definition to return a server URI when no local copy exists" + ); + }); }); diff --git a/test-fixtures/multi-root/client/.vscode/settings.json b/test-fixtures/multi-root/client/.vscode/settings.json new file mode 100644 index 00000000..c3b581df --- /dev/null +++ b/test-fixtures/multi-root/client/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "objectscript.conn": { + "active": true, + "ns": "USER" + }, + "objectscript.export": { + "folder": "src", + "searchOtherWorkspaceFolders": [ + "shared" + ] + } +} diff --git a/test-fixtures/multi-root/client/src/MultiRoot/Caller.cls b/test-fixtures/multi-root/client/src/MultiRoot/Caller.cls new file mode 100644 index 00000000..79cad05b --- /dev/null +++ b/test-fixtures/multi-root/client/src/MultiRoot/Caller.cls @@ -0,0 +1,10 @@ +Class MultiRoot.Caller Extends %RegisteredObject +{ + +ClassMethod Test() +{ + Do ##class(MultiRoot.Shared).Ping() + Do ##class(MultiRoot.ServerOnly).Ping() +} + +} diff --git a/test-fixtures/multi-root/shared/.vscode/settings.json b/test-fixtures/multi-root/shared/.vscode/settings.json new file mode 100644 index 00000000..4753cef3 --- /dev/null +++ b/test-fixtures/multi-root/shared/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "objectscript.conn": { + "active": false, + "ns": "SAMPLES" + }, + "objectscript.export": { + "folder": "src" + } +} diff --git a/test-fixtures/multi-root/shared/src/MultiRoot/Shared.cls b/test-fixtures/multi-root/shared/src/MultiRoot/Shared.cls new file mode 100644 index 00000000..d176babf --- /dev/null +++ b/test-fixtures/multi-root/shared/src/MultiRoot/Shared.cls @@ -0,0 +1,9 @@ +Class MultiRoot.Shared Extends %RegisteredObject +{ + +ClassMethod Ping() +{ + Quit +} + +} diff --git a/test-fixtures/test.code-workspace b/test-fixtures/test.code-workspace index ba637669..e49cb155 100644 --- a/test-fixtures/test.code-workspace +++ b/test-fixtures/test.code-workspace @@ -1,15 +1,19 @@ { "folders": [ { - "path": "." + "name": "client", + "path": "multi-root/client" }, + { + "name": "shared", + "path": "multi-root/shared" + } ], "settings": { "objectscript.conn": { "active": false }, "objectscript.ignoreInstallServerManager": true, - "intersystems.servers": { - } + "intersystems.servers": {} } } From f8de9803f8587afa3cbdfb0ad94ebda656672033 Mon Sep 17 00:00:00 2001 From: LeoAnders Date: Sun, 21 Sep 2025 23:37:42 -0300 Subject: [PATCH 02/15] feat: add ObjectScript enter rules for semicolon continuation --- src/languageConfiguration.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) 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, }; } From 2edfa33a430032641d6feecf6eb69e18702e0dfd Mon Sep 17 00:00:00 2001 From: LeoAnders Date: Mon, 22 Sep 2025 13:16:34 -0300 Subject: [PATCH 03/15] feat: auto-indent dot syntax on enter for `.mac` / `.int` routines --- src/extension.ts | 70 ++++++++++++++++++++++++++++++++ src/test/suite/extension.test.ts | 47 +++++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index 45c1a4e0..8d2c0220 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 { @@ -1038,6 +1042,72 @@ 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.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 */ diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index d2cdb1a9..3f4ba604 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -21,6 +21,17 @@ async function waitForIndexedDocument(documentName: string, workspaceFolderName: assert.fail(`Timed out waiting for '${documentName}' to be indexed in workspace folder '${workspaceFolderName}'.`); } +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"); +} + function getDefinitionTargets(definitions: (vscode.Location | vscode.DefinitionLink)[]): vscode.Uri[] { return definitions .map((definition) => ("targetUri" in definition ? definition.targetUri : definition.uri)) @@ -44,6 +55,42 @@ suite("Extension Test Suite", () => { 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("Go to Definition resolves to sibling workspace folder", async function () { this.timeout(10000); await waitForIndexedDocument("MultiRoot.Shared.cls", "shared"); From 2af7617a6cae600b55888dc21176708b6e64db9f Mon Sep 17 00:00:00 2001 From: Ricardo Alves dos Santos Junior Date: Tue, 23 Sep 2025 14:57:38 -0300 Subject: [PATCH 04/15] feat: add resolveContextExpression command `Ctrl+Alt+Space` to call API and insert returned code --- package.json | 11 +++++ src/commands/contextHelp.ts | 87 +++++++++++++++++++++++++++++++++++++ src/extension.ts | 5 +++ 3 files changed, 103 insertions(+) create mode 100644 src/commands/contextHelp.ts diff --git a/package.json b/package.json index 5e64fcab..2142de28 100644 --- a/package.json +++ b/package.json @@ -839,6 +839,11 @@ "command": "vscode-objectscript.export", "title": "Export Code from Server" }, + { + "category": "ObjectScript", + "command": "vscode-objectscript.resolveContextExpression", + "title": "Resolve Context Expression" + }, { "category": "ObjectScript", "command": "vscode-objectscript.compile", @@ -1219,6 +1224,12 @@ "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.viewOthers", "key": "Ctrl+Shift+V", diff --git a/src/commands/contextHelp.ts b/src/commands/contextHelp.ts new file mode 100644 index 00000000..e2b83d91 --- /dev/null +++ b/src/commands/contextHelp.ts @@ -0,0 +1,87 @@ +import axios from "axios"; +import * as https from "https"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { AtelierAPI } from "../api"; +import { handleError } from "../utils"; + +interface ResolveContextExpressionResponse { + status?: string; + textExpression?: string; + message?: string; +} + +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); + const api = new AtelierAPI(document.uri); + const { host, port, username, password, https: useHttps, pathPrefix } = api.config; + + if (!host || !port) { + void vscode.window.showErrorMessage("No active InterSystems server connection for this file."); + return; + } + + const normalizedPrefix = pathPrefix ? (pathPrefix.startsWith("/") ? pathPrefix : `/${pathPrefix}`) : ""; + + const baseUrl = `${useHttps ? "https" : "http"}://${host}:${port}${encodeURI(normalizedPrefix)}`; + const url = `${baseUrl}/api/sourcecontrol/vscode/resolveContextExpression`; + + const httpsAgent = new https.Agent({ + rejectUnauthorized: vscode.workspace.getConfiguration("http").get("proxyStrictSSL"), + }); + + try { + const response = await axios.post( + url, + { + routine, + contextExpression, + }, + { + headers: { + "Content-Type": "application/json", + }, + auth: + typeof username === "string" && typeof password === "string" + ? { + username, + password, + } + : undefined, + httpsAgent, + } + ); + + const data = response.data ?? {}; + 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 lineRange = document.lineAt(selection.active.line).range; + await editor.edit((editBuilder) => { + editBuilder.replace(lineRange, 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."); + } +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 8d2c0220..8fcb97f6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -163,6 +163,7 @@ import { WorkspaceNode, NodeBase } from "./explorer/nodes"; import { showPlanWebview } from "./commands/showPlanPanel"; import { isfsConfig } from "./utils/FileProviderUtil"; import { showAllClassMembers } from "./commands/showAllClassMembers"; +import { resolveContextExpression } from "./commands/contextHelp"; const packageJson = vscode.extensions.getExtension(extensionId).packageJSON; const extensionVersion = packageJson.version; @@ -1261,6 +1262,10 @@ 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.debug", (program: string, askArgs: boolean) => { sendCommandTelemetryEvent("debug"); const startDebugging = (args) => { From 3e81214b0c89ca5ef62d6e07135b39d3e103c394 Mon Sep 17 00:00:00 2001 From: Ricardo Alves dos Santos Junior Date: Tue, 23 Sep 2025 15:10:07 -0300 Subject: [PATCH 05/15] fix: error Insert `enter` prettier/prettier --- src/commands/contextHelp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/contextHelp.ts b/src/commands/contextHelp.ts index e2b83d91..68c9e956 100644 --- a/src/commands/contextHelp.ts +++ b/src/commands/contextHelp.ts @@ -84,4 +84,4 @@ export async function resolveContextExpression(): Promise { } catch (error) { handleError(error, "Failed to resolve context expression."); } -} \ No newline at end of file +} From 91f804272b9b3acce747bf08166386f1e923ffbb Mon Sep 17 00:00:00 2001 From: Ricardo Alves <112292689+RicardoASJunior@users.noreply.github.com> Date: Thu, 25 Sep 2025 22:44:14 -0300 Subject: [PATCH 06/15] Chore API restructure to use request util (#9) * chore (api): restructure to use request util * docs: add changelog entry for API refactor * style: fix prettier formatting issues --- src/api/ccs/sourceControl.ts | 53 ++++++++++++++++++++++++++ src/commands/{ => ccs}/contextHelp.ts | 54 ++++++++------------------- src/extension.ts | 2 +- 3 files changed, 70 insertions(+), 39 deletions(-) create mode 100644 src/api/ccs/sourceControl.ts rename src/commands/{ => ccs}/contextHelp.ts (51%) diff --git a/src/api/ccs/sourceControl.ts b/src/api/ccs/sourceControl.ts new file mode 100644 index 00000000..fd65ad88 --- /dev/null +++ b/src/api/ccs/sourceControl.ts @@ -0,0 +1,53 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; +import * as https from "https"; +import * as vscode from "vscode"; + +import { AtelierAPI } from "../"; + +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 baseUrl = `${useHttps ? "https" : "http"}://${host}:${port}${encodeURI(normalizedPrefix)}`; + + const httpsAgent = new https.Agent({ + rejectUnauthorized: vscode.workspace.getConfiguration("http").get("proxyStrictSSL"), + }); + + const client = axios.create({ + baseURL: `${baseUrl}/api/sourcecontrol/vscode`, + headers: { + "Content-Type": "application/json", + }, + httpsAgent, + auth: + typeof username === "string" && typeof password === "string" + ? { + username, + password, + } + : undefined, + }); + + return new SourceControlApi(client); + } + + public post>( + endpoint: string, + data?: unknown, + config?: AxiosRequestConfig + ): Promise { + return this.client.post(endpoint, data, config); + } +} diff --git a/src/commands/contextHelp.ts b/src/commands/ccs/contextHelp.ts similarity index 51% rename from src/commands/contextHelp.ts rename to src/commands/ccs/contextHelp.ts index 68c9e956..7e78c0ca 100644 --- a/src/commands/contextHelp.ts +++ b/src/commands/ccs/contextHelp.ts @@ -1,10 +1,9 @@ -import axios from "axios"; -import * as https from "https"; import * as path from "path"; import * as vscode from "vscode"; -import { AtelierAPI } from "../api"; -import { handleError } from "../utils"; +import { AtelierAPI } from "../../api"; +import { SourceControlApi } from "../../api/ccs/sourceControl"; +import { handleError } from "../../utils"; interface ResolveContextExpressionResponse { status?: string; @@ -30,52 +29,31 @@ export async function resolveContextExpression(): Promise { const routine = path.basename(document.fileName); const api = new AtelierAPI(document.uri); - const { host, port, username, password, https: useHttps, pathPrefix } = api.config; - if (!host || !port) { - void vscode.window.showErrorMessage("No active InterSystems server connection for this file."); + let sourceControlApi: SourceControlApi; + try { + sourceControlApi = SourceControlApi.fromAtelierApi(api); + } catch (error) { + void vscode.window.showErrorMessage(error instanceof Error ? error.message : String(error)); return; } - const normalizedPrefix = pathPrefix ? (pathPrefix.startsWith("/") ? pathPrefix : `/${pathPrefix}`) : ""; - - const baseUrl = `${useHttps ? "https" : "http"}://${host}:${port}${encodeURI(normalizedPrefix)}`; - const url = `${baseUrl}/api/sourcecontrol/vscode/resolveContextExpression`; - - const httpsAgent = new https.Agent({ - rejectUnauthorized: vscode.workspace.getConfiguration("http").get("proxyStrictSSL"), - }); - try { - const response = await axios.post( - url, - { - routine, - contextExpression, - }, - { - headers: { - "Content-Type": "application/json", - }, - auth: - typeof username === "string" && typeof password === "string" - ? { - username, - password, - } - : undefined, - httpsAgent, - } - ); + const response = await sourceControlApi.post("/resolveContextExpression", { + routine, + contextExpression, + }); const data = response.data ?? {}; 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 lineRange = document.lineAt(selection.active.line).range; + const rangeToReplace = selection.isEmpty + ? document.lineAt(selection.active.line).range + : new vscode.Range(selection.start, selection.end); await editor.edit((editBuilder) => { - editBuilder.replace(lineRange, formattedTextExpression); + editBuilder.replace(rangeToReplace, formattedTextExpression); }); } else { const errorMessage = data.message || "Failed to resolve context expression."; diff --git a/src/extension.ts b/src/extension.ts index 8fcb97f6..e0cc8ec0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -162,8 +162,8 @@ import { import { WorkspaceNode, NodeBase } from "./explorer/nodes"; import { showPlanWebview } from "./commands/showPlanPanel"; import { isfsConfig } from "./utils/FileProviderUtil"; +import { resolveContextExpression } from "./commands/ccs/contextHelp"; import { showAllClassMembers } from "./commands/showAllClassMembers"; -import { resolveContextExpression } from "./commands/contextHelp"; const packageJson = vscode.extensions.getExtension(extensionId).packageJSON; const extensionVersion = packageJson.version; From 42e9e9243b1f98fad409c51f1eb069c6f898b095 Mon Sep 17 00:00:00 2001 From: LeoAnders Date: Thu, 25 Sep 2025 23:53:30 -0300 Subject: [PATCH 07/15] ci: add `target_commitish` to correctly reference commit in prerelease tags --- .github/workflows/prerelease.yml | 1 + 1 file changed, 1 insertion(+) 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 }} From 4e35432706a0d2412cbd0bd42416c37bb51dca94 Mon Sep 17 00:00:00 2001 From: LeoAnders Date: Mon, 29 Sep 2025 14:41:33 -0300 Subject: [PATCH 08/15] refactor: reorganize CCS module layout --- src/{commands/ccs => ccs/commands}/contextHelp.ts | 2 +- src/ccs/index.ts | 2 ++ src/{api/ccs/sourceControl.ts => ccs/sourcecontrol/client.ts} | 3 +-- src/extension.ts | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) rename src/{commands/ccs => ccs/commands}/contextHelp.ts (97%) create mode 100644 src/ccs/index.ts rename src/{api/ccs/sourceControl.ts => ccs/sourcecontrol/client.ts} (97%) diff --git a/src/commands/ccs/contextHelp.ts b/src/ccs/commands/contextHelp.ts similarity index 97% rename from src/commands/ccs/contextHelp.ts rename to src/ccs/commands/contextHelp.ts index 7e78c0ca..d4944bb2 100644 --- a/src/commands/ccs/contextHelp.ts +++ b/src/ccs/commands/contextHelp.ts @@ -2,8 +2,8 @@ import * as path from "path"; import * as vscode from "vscode"; import { AtelierAPI } from "../../api"; -import { SourceControlApi } from "../../api/ccs/sourceControl"; import { handleError } from "../../utils"; +import { SourceControlApi } from "../sourcecontrol/client"; interface ResolveContextExpressionResponse { status?: string; diff --git a/src/ccs/index.ts b/src/ccs/index.ts new file mode 100644 index 00000000..aabc9d77 --- /dev/null +++ b/src/ccs/index.ts @@ -0,0 +1,2 @@ +export { SourceControlApi } from "./sourcecontrol/client"; +export { resolveContextExpression } from "./commands/contextHelp"; diff --git a/src/api/ccs/sourceControl.ts b/src/ccs/sourcecontrol/client.ts similarity index 97% rename from src/api/ccs/sourceControl.ts rename to src/ccs/sourcecontrol/client.ts index fd65ad88..05b6c3eb 100644 --- a/src/api/ccs/sourceControl.ts +++ b/src/ccs/sourcecontrol/client.ts @@ -1,8 +1,7 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; import * as https from "https"; import * as vscode from "vscode"; - -import { AtelierAPI } from "../"; +import { AtelierAPI } from "../../api"; export class SourceControlApi { private readonly client: AxiosInstance; diff --git a/src/extension.ts b/src/extension.ts index e0cc8ec0..001b8403 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -162,8 +162,8 @@ import { import { WorkspaceNode, NodeBase } from "./explorer/nodes"; import { showPlanWebview } from "./commands/showPlanPanel"; import { isfsConfig } from "./utils/FileProviderUtil"; -import { resolveContextExpression } from "./commands/ccs/contextHelp"; import { showAllClassMembers } from "./commands/showAllClassMembers"; +import { resolveContextExpression } from "./ccs"; const packageJson = vscode.extensions.getExtension(extensionId).packageJSON; const extensionVersion = packageJson.version; From 9f0208899c06b4e83a66164c467ee594d83b5b56 Mon Sep 17 00:00:00 2001 From: LeoAnders Date: Mon, 29 Sep 2025 15:31:28 -0300 Subject: [PATCH 09/15] feat: add `core` and centralized `config` scaffolds --- src/ccs/config/schema.md | 13 ++++++ src/ccs/config/settings.ts | 51 ++++++++++++++++++++++++ src/ccs/core/http.ts | 81 ++++++++++++++++++++++++++++++++++++++ src/ccs/core/logging.ts | 52 ++++++++++++++++++++++++ src/ccs/core/types.ts | 10 +++++ 5 files changed, 207 insertions(+) create mode 100644 src/ccs/config/schema.md create mode 100644 src/ccs/config/settings.ts create mode 100644 src/ccs/core/http.ts create mode 100644 src/ccs/core/logging.ts create mode 100644 src/ccs/core/types.ts 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..0ded60a9 --- /dev/null +++ b/src/ccs/core/types.ts @@ -0,0 +1,10 @@ +export interface ResolveContextExpressionResponse { + status?: string; + textExpression?: string; + message?: string; +} + +export interface SourceControlError { + message: string; + cause?: unknown; +} From 90d0c39fc68bcc8ad05b2dddb1722e51fae98e46 Mon Sep 17 00:00:00 2001 From: LeoAnders Date: Mon, 29 Sep 2025 16:59:04 -0300 Subject: [PATCH 10/15] feat: add ContextExpressionClient and centralize route handling - Introduced ContextExpressionClient to encapsulate API calls for \/resolveContextExpression\, applying settings, timeout and logging - Updated contextHelp.ts to consume the new client instead of creating SourceControlApi manually - Added routes.ts with BASE_PATH and resolveContextExpression route - Updated extension.ts to import CCS commands/providers from barrel - Adapted SourceControlApi client to use centralized routes (only resolveContextExpression for now) --- src/ccs/commands/contextHelp.ts | 25 ++------- src/ccs/index.ts | 3 ++ src/ccs/sourcecontrol/client.ts | 53 +++++++++--------- .../sourcecontrol/contextExpressionClient.ts | 54 +++++++++++++++++++ src/ccs/sourcecontrol/routes.ts | 7 +++ 5 files changed, 97 insertions(+), 45 deletions(-) create mode 100644 src/ccs/sourcecontrol/contextExpressionClient.ts create mode 100644 src/ccs/sourcecontrol/routes.ts diff --git a/src/ccs/commands/contextHelp.ts b/src/ccs/commands/contextHelp.ts index d4944bb2..3cd456e6 100644 --- a/src/ccs/commands/contextHelp.ts +++ b/src/ccs/commands/contextHelp.ts @@ -1,15 +1,10 @@ import * as path from "path"; import * as vscode from "vscode"; -import { AtelierAPI } from "../../api"; +import { ContextExpressionClient } from "../sourcecontrol/contextExpressionClient"; import { handleError } from "../../utils"; -import { SourceControlApi } from "../sourcecontrol/client"; -interface ResolveContextExpressionResponse { - status?: string; - textExpression?: string; - message?: string; -} +const sharedClient = new ContextExpressionClient(); export async function resolveContextExpression(): Promise { const editor = vscode.window.activeTextEditor; @@ -28,23 +23,11 @@ export async function resolveContextExpression(): Promise { } const routine = path.basename(document.fileName); - const api = new AtelierAPI(document.uri); - - let sourceControlApi: SourceControlApi; - try { - sourceControlApi = SourceControlApi.fromAtelierApi(api); - } catch (error) { - void vscode.window.showErrorMessage(error instanceof Error ? error.message : String(error)); - return; - } try { - const response = await sourceControlApi.post("/resolveContextExpression", { - routine, - contextExpression, - }); + const response = await sharedClient.resolve(document, { routine, contextExpression }); + const data = response ?? {}; - const data = response.data ?? {}; 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); diff --git a/src/ccs/index.ts b/src/ccs/index.ts index aabc9d77..d8fcf14e 100644 --- a/src/ccs/index.ts +++ b/src/ccs/index.ts @@ -1,2 +1,5 @@ +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 { ContextExpressionClient } from "./sourcecontrol/contextExpressionClient"; diff --git a/src/ccs/sourcecontrol/client.ts b/src/ccs/sourcecontrol/client.ts index 05b6c3eb..ba4c05af 100644 --- a/src/ccs/sourcecontrol/client.ts +++ b/src/ccs/sourcecontrol/client.ts @@ -1,7 +1,10 @@ -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; -import * as https from "https"; -import * as vscode from "vscode"; +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; @@ -18,35 +21,37 @@ export class SourceControlApi { } const normalizedPrefix = pathPrefix ? (pathPrefix.startsWith("/") ? pathPrefix : `/${pathPrefix}`) : ""; - const baseUrl = `${useHttps ? "https" : "http"}://${host}:${port}${encodeURI(normalizedPrefix)}`; - - const httpsAgent = new https.Agent({ - rejectUnauthorized: vscode.workspace.getConfiguration("http").get("proxyStrictSSL"), - }); - - const client = axios.create({ - baseURL: `${baseUrl}/api/sourcecontrol/vscode`, - headers: { - "Content-Type": "application/json", - }, - httpsAgent, - auth: - typeof username === "string" && typeof password === "string" - ? { - username, - password, - } - : undefined, + 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>( - endpoint: string, + route: string, data?: unknown, config?: AxiosRequestConfig ): Promise { - return this.client.post(endpoint, data, config); + return this.client.post(route, data, config); } } diff --git a/src/ccs/sourcecontrol/contextExpressionClient.ts b/src/ccs/sourcecontrol/contextExpressionClient.ts new file mode 100644 index 00000000..0ab4027b --- /dev/null +++ b/src/ccs/sourcecontrol/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/routes.ts b/src/ccs/sourcecontrol/routes.ts new file mode 100644 index 00000000..b6a6736e --- /dev/null +++ b/src/ccs/sourcecontrol/routes.ts @@ -0,0 +1,7 @@ +export const BASE_PATH = "/api/sourcecontrol/vscode" as const; + +export const ROUTES = { + resolveContextExpression: () => `/resolveContextExpression`, +} as const; + +export type RouteKey = keyof typeof ROUTES; From f31986f5d2069df03f0740b20f42fdb37541d37e Mon Sep 17 00:00:00 2001 From: LeoAnders Date: Mon, 29 Sep 2025 17:24:54 -0300 Subject: [PATCH 11/15] feat: reorganize SourceControl API with dedicated clients folder --- src/ccs/commands/contextHelp.ts | 2 +- src/ccs/index.ts | 2 +- .../{ => clients}/contextExpressionClient.ts | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) rename src/ccs/sourcecontrol/{ => clients}/contextExpressionClient.ts (81%) diff --git a/src/ccs/commands/contextHelp.ts b/src/ccs/commands/contextHelp.ts index 3cd456e6..40861e76 100644 --- a/src/ccs/commands/contextHelp.ts +++ b/src/ccs/commands/contextHelp.ts @@ -1,7 +1,7 @@ import * as path from "path"; import * as vscode from "vscode"; -import { ContextExpressionClient } from "../sourcecontrol/contextExpressionClient"; +import { ContextExpressionClient } from "../sourcecontrol/clients/contextExpressionClient"; import { handleError } from "../../utils"; const sharedClient = new ContextExpressionClient(); diff --git a/src/ccs/index.ts b/src/ccs/index.ts index d8fcf14e..4bc24a85 100644 --- a/src/ccs/index.ts +++ b/src/ccs/index.ts @@ -2,4 +2,4 @@ export { getCcsSettings, isFlagEnabled, type CcsSettings } from "./config/settin export { logDebug, logError, logInfo, logWarn } from "./core/logging"; export { SourceControlApi } from "./sourcecontrol/client"; export { resolveContextExpression } from "./commands/contextHelp"; -export { ContextExpressionClient } from "./sourcecontrol/contextExpressionClient"; +export { ContextExpressionClient } from "./sourcecontrol/clients/contextExpressionClient"; diff --git a/src/ccs/sourcecontrol/contextExpressionClient.ts b/src/ccs/sourcecontrol/clients/contextExpressionClient.ts similarity index 81% rename from src/ccs/sourcecontrol/contextExpressionClient.ts rename to src/ccs/sourcecontrol/clients/contextExpressionClient.ts index 0ab4027b..50461039 100644 --- a/src/ccs/sourcecontrol/contextExpressionClient.ts +++ b/src/ccs/sourcecontrol/clients/contextExpressionClient.ts @@ -1,11 +1,11 @@ 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"; +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; From e66ddd1e2226109f32021b06bf2553e4fb7a8369 Mon Sep 17 00:00:00 2001 From: Ricardo Alves <112292689+RicardoASJunior@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:13:25 -0300 Subject: [PATCH 12/15] Fix prevent unwanted semicolon insertion (#13) Fix unwanted semicolon insertion in ObjectScript dot-prefixed blocks: auto-continuation now inserts the copied prefix only when the target line is empty and preserves ; only if it existed on the previous line, and a regression test ensures moving lines across dot-prefixed semicolon comments does not inject semicolons. --- src/extension.ts | 3 +++ src/test/suite/extension.test.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index 001b8403..e504aa28 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1086,6 +1086,9 @@ export async function activate(context: vscode.ExtensionContext): Promise { } const newLine = event.document.lineAt(newLineNumber); + if (newLine.text.trim().length > 0) { + continue; + } if (newLine.text.startsWith(insertText)) { continue; } diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 3f4ba604..05016507 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -91,6 +91,22 @@ suite("Extension Test Suite", () => { } }); + 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"); + } + }); test("Go to Definition resolves to sibling workspace folder", async function () { this.timeout(10000); await waitForIndexedDocument("MultiRoot.Shared.cls", "shared"); From 4e750b2c41e6bb7ad81300eeef913e41fc7a076d Mon Sep 17 00:00:00 2001 From: Ricardo Alves <112292689+RicardoASJunior@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:24:25 -0300 Subject: [PATCH 13/15] Feat add Ctrl+Q to fetch global documentation from selection and print to Output (#17) Add Ctrl+Q shortcut to fetch global documentation for the current selection/line and print the response to the Output. --- package.json | 22 +++++++ src/api/index.ts | 4 ++ src/ccs/commands/globalDocumentation.ts | 48 +++++++++++++++ src/ccs/core/types.ts | 4 ++ src/ccs/index.ts | 2 + .../clients/globalDocumentationClient.ts | 47 +++++++++++++++ src/ccs/sourcecontrol/routes.ts | 1 + src/commands/globalDocumentation.ts | 60 +++++++++++++++++++ src/extension.ts | 6 +- 9 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 src/ccs/commands/globalDocumentation.ts create mode 100644 src/ccs/sourcecontrol/clients/globalDocumentationClient.ts create mode 100644 src/commands/globalDocumentation.ts diff --git a/package.json b/package.json index 2142de28..759422db 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" @@ -844,6 +848,12 @@ "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.compile", @@ -943,6 +953,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", @@ -1230,6 +1246,12 @@ "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/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/core/types.ts b/src/ccs/core/types.ts index 0ded60a9..6c821c53 100644 --- a/src/ccs/core/types.ts +++ b/src/ccs/core/types.ts @@ -8,3 +8,7 @@ export interface SourceControlError { message: string; cause?: unknown; } +export interface GlobalDocumentationResponse { + content?: string | string[] | Record | null; + message?: string; +} diff --git a/src/ccs/index.ts b/src/ccs/index.ts index 4bc24a85..0ce8af2e 100644 --- a/src/ccs/index.ts +++ b/src/ccs/index.ts @@ -2,4 +2,6 @@ export { getCcsSettings, isFlagEnabled, type CcsSettings } from "./config/settin 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"; 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/routes.ts b/src/ccs/sourcecontrol/routes.ts index b6a6736e..c379d7d5 100644 --- a/src/ccs/sourcecontrol/routes.ts +++ b/src/ccs/sourcecontrol/routes.ts @@ -2,6 +2,7 @@ export const BASE_PATH = "/api/sourcecontrol/vscode" as const; export const ROUTES = { resolveContextExpression: () => `/resolveContextExpression`, + getGlobalDocumentation: () => `/getGlobalDocumentation`, } 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 e504aa28..1edc652d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -163,7 +163,7 @@ import { WorkspaceNode, NodeBase } from "./explorer/nodes"; import { showPlanWebview } from "./commands/showPlanPanel"; import { isfsConfig } from "./utils/FileProviderUtil"; import { showAllClassMembers } from "./commands/showAllClassMembers"; -import { resolveContextExpression } from "./ccs"; +import { resolveContextExpression, showGlobalDocumentation } from "./ccs"; const packageJson = vscode.extensions.getExtension(extensionId).packageJSON; const extensionVersion = packageJson.version; @@ -1377,6 +1377,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); From 5b170fdf9917aa3b98d55ea0fdde0d2cc137cb46 Mon Sep 17 00:00:00 2001 From: Leonardo Anders <115679546+LeoAnders@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:23:09 -0300 Subject: [PATCH 14/15] Unify Go to Definition(F12) and Ctrl+Click through CCS API resolution (#20) * revert: remove cross-workspace definition lookup (45b7b82) * feat: add API-based Go to Definition command and integrate with keybinding - Introduced DefinitionResolverClient for REST API resolution - Implemented definitionLookup feature (extractQuery + lookup) for robust query handling - Added `PrioritizedDefinitionProvider` to prefer CCS resolver before fallback - Implemented new command vscode-objectscript.ccs.goToDefinition with API-first fallback - Updated package.json to bind F12 and menus to the new command for ObjectScript files - Registered new command in extension.ts and integrated telemetry - New goToDefinitionLocalFirst command integrates CCS API before native definition * feat: support cross-namespace definition lookup and request logging * feat: implement full Ctrl+Click support with `CCS API` resolution * feat: remove visual underline from `DocumentLinks` in `DefinitionDocumentLinkProvider` --- README.md | 17 - package.json | 42 ++- src/ccs/commands/goToDefinitionLocalFirst.ts | 26 ++ src/ccs/core/types.ts | 7 + .../features/definitionLookup/extractQuery.ts | 311 ++++++++++++++++++ src/ccs/features/definitionLookup/lookup.ts | 82 +++++ src/ccs/index.ts | 14 + .../DefinitionDocumentLinkProvider.ts | 189 +++++++++++ .../PrioritizedDefinitionProvider.ts | 34 ++ .../clients/resolveDefinitionClient.ts | 130 ++++++++ src/ccs/sourcecontrol/paths.ts | 36 ++ src/ccs/sourcecontrol/routes.ts | 1 + src/extension.ts | 52 ++- src/providers/DocumentContentProvider.ts | 85 ++--- src/test/suite/extension.test.ts | 82 +---- .../multi-root/client/.vscode/settings.json | 12 - .../client/src/MultiRoot/Caller.cls | 10 - .../multi-root/shared/.vscode/settings.json | 9 - .../shared/src/MultiRoot/Shared.cls | 9 - test-fixtures/test.code-workspace | 10 +- 20 files changed, 943 insertions(+), 215 deletions(-) create mode 100644 src/ccs/commands/goToDefinitionLocalFirst.ts create mode 100644 src/ccs/features/definitionLookup/extractQuery.ts create mode 100644 src/ccs/features/definitionLookup/lookup.ts create mode 100644 src/ccs/providers/DefinitionDocumentLinkProvider.ts create mode 100644 src/ccs/providers/PrioritizedDefinitionProvider.ts create mode 100644 src/ccs/sourcecontrol/clients/resolveDefinitionClient.ts create mode 100644 src/ccs/sourcecontrol/paths.ts delete mode 100644 test-fixtures/multi-root/client/.vscode/settings.json delete mode 100644 test-fixtures/multi-root/client/src/MultiRoot/Caller.cls delete mode 100644 test-fixtures/multi-root/shared/.vscode/settings.json delete mode 100644 test-fixtures/multi-root/shared/src/MultiRoot/Shared.cls diff --git a/README.md b/README.md index f3b4c9fe..a6c7427e 100644 --- a/README.md +++ b/README.md @@ -76,23 +76,6 @@ intersystems-community.vscode-objectscript version X.Y.Z-beta.1 activating with After a subsequent update of the extension from Marketplace you will only have to download and install the new `vscode-objectscript-X.Y.Z-beta.1` VSIX. None of the other steps above are needed again. -## Cross-workspace Go to Definition - -> **Implementation developed and maintained by Consistem Sistemas** - -When working in a multi-root workspace, the extension normally searches the current workspace folder (and any sibling folders connected to the same namespace) for local copies of ObjectScript code before requesting the server version. If you keep shared source code in other workspace folders with different connection settings, set the `objectscript.export.searchOtherWorkspaceFolders` array in the consuming folder's settings so those folders are considered first. Use workspace-folder names, or specify `"*"` to search every non-`isfs` folder. - -```json -{ - "objectscript.export": { - "folder": "src", - "searchOtherWorkspaceFolders": ["shared"] - } -} -``` - -With this setting enabled, features such as Go to Definition resolve to the first matching local file across the configured workspace folders before falling back to the server copy. - ## Notes - Connection-related output appears in the 'Output' view while switched to the 'ObjectScript' channel using the drop-down menu on the view titlebar. diff --git a/package.json b/package.json index 759422db..2c2f2b77 100644 --- a/package.json +++ b/package.json @@ -482,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", @@ -529,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", @@ -854,6 +873,16 @@ "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", @@ -1228,6 +1257,11 @@ } ], "keybindings": [ + { + "command": "vscode-objectscript.ccs.goToDefinition", + "key": "F12", + "when": "editorTextFocus && editorLangId =~ /^objectscript/" + }, { "command": "vscode-objectscript.compile", "key": "Ctrl+F7", @@ -1439,14 +1473,6 @@ }, "additionalProperties": false }, - "searchOtherWorkspaceFolders": { - "markdownDescription": "Additional workspace folders to search for client-side sources when resolving ObjectScript documents. Specify `\"*\"` to search all non-isfs workspace folders in the current multi-root workspace before falling back to the server.", - "type": "array", - "items": { - "type": "string" - }, - "default": [] - }, "atelier": { "description": "Export source code as Atelier did it, with packages as subfolders. This setting only affects classes, routines, include files and DFI files.", "type": "boolean" 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/core/types.ts b/src/ccs/core/types.ts index 6c821c53..707eb8ba 100644 --- a/src/ccs/core/types.ts +++ b/src/ccs/core/types.ts @@ -1,3 +1,10 @@ +export interface LocationJSON { + uri?: string; + line?: number; +} + +export type ResolveDefinitionResponse = LocationJSON; + export interface ResolveContextExpressionResponse { status?: string; textExpression?: 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 index 0ce8af2e..88212849 100644 --- a/src/ccs/index.ts +++ b/src/ccs/index.ts @@ -5,3 +5,17 @@ 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/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 index c379d7d5..1561570f 100644 --- a/src/ccs/sourcecontrol/routes.ts +++ b/src/ccs/sourcecontrol/routes.ts @@ -3,6 +3,7 @@ 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/extension.ts b/src/extension.ts index 1edc652d..6f6c9133 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -163,7 +163,14 @@ import { WorkspaceNode, NodeBase } from "./explorer/nodes"; import { showPlanWebview } from "./commands/showPlanPanel"; import { isfsConfig } from "./utils/FileProviderUtil"; import { showAllClassMembers } from "./commands/showAllClassMembers"; -import { resolveContextExpression, showGlobalDocumentation } from "./ccs"; +import { + PrioritizedDefinitionProvider, + DefinitionDocumentLinkProvider, + followDefinitionLinkCommand, + goToDefinitionLocalFirst, + resolveContextExpression, + showGlobalDocumentation, +} from "./ccs"; const packageJson = vscode.extensions.getExtension(extensionId).packageJSON; const extensionVersion = packageJson.version; @@ -945,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 != [] @@ -1006,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), @@ -1269,6 +1290,32 @@ export async function activate(context: vscode.ExtensionContext): Promise { 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) => { @@ -2180,3 +2227,4 @@ export async function deactivate(): Promise { } await Promise.allSettled(promises); } +export { outputChannel }; diff --git a/src/providers/DocumentContentProvider.ts b/src/providers/DocumentContentProvider.ts index 24857303..c9eae4a4 100644 --- a/src/providers/DocumentContentProvider.ts +++ b/src/providers/DocumentContentProvider.ts @@ -165,73 +165,30 @@ export class DocumentContentProvider implements vscode.TextDocumentContentProvid }); } } else { - const conn = config("conn", workspaceFolder) ?? {}; - const exportConfig = - workspaceFolder && workspaceFolder !== "" - ? (config("export", workspaceFolder) as { searchOtherWorkspaceFolders?: string[] }) - : undefined; - const searchOtherWorkspaceFolders = Array.isArray(exportConfig?.searchOtherWorkspaceFolders) - ? exportConfig.searchOtherWorkspaceFolders - .map((value) => (typeof value === "string" ? value.trim() : "")) - .filter((value) => value.length > 0) - : []; - const includeAllFolders = searchOtherWorkspaceFolders.includes("*"); - const explicitAdditionalFolders = new Set( - searchOtherWorkspaceFolders.filter((value) => value !== "*").map((value) => value.toLowerCase()) - ); + const conn = config("conn", workspaceFolder); if (!forceServerCopy) { - const tryLocalUri = (folderName: string, allowNamespaceMismatch: boolean): vscode.Uri => { - const localFile = this.findLocalUri(name, folderName); - if (!localFile) return; - if (!allowNamespaceMismatch && namespace) { - const folderConn = config("conn", folderName) ?? {}; - if (folderConn.ns && namespace !== folderConn.ns) { - return; - } - } - return localFile; - }; - // Look for the document in the local file system - const primaryLocal = tryLocalUri(workspaceFolder, false); - if (primaryLocal) { - return primaryLocal; - } - - // Check any other eligible local folders in this workspace if it's a multi-root workspace - const wFolders = vscode.workspace.workspaceFolders; - if (wFolders && wFolders.length > 1 && workspaceFolder) { - const candidates: { folder: vscode.WorkspaceFolder; allowNamespaceMismatch: boolean }[] = []; - const seen = new Set(); - const addCandidate = (folder: vscode.WorkspaceFolder, allowNamespaceMismatch: boolean): void => { - if (!notIsfs(folder.uri)) return; - if (folder.name === workspaceFolder) return; - if (seen.has(folder.name)) return; - candidates.push({ folder, allowNamespaceMismatch }); - seen.add(folder.name); - }; - - for (const wFolder of wFolders) { - if (wFolder.name === workspaceFolder) continue; - const wFolderConn = config("conn", wFolder.name) ?? {}; - if (compareConns(conn, wFolderConn) && (!namespace || namespace === wFolderConn.ns)) { - addCandidate(wFolder, false); - } - } - - if (includeAllFolders || explicitAdditionalFolders.size > 0) { + const localFile = this.findLocalUri(name, workspaceFolder); + if (localFile && (!namespace || namespace === conn.ns)) { + // Exists as a local file and we aren't viewing a different namespace on the same server, + // so return a uri that will open the local file. + return localFile; + } else { + // The local file doesn't exist in this folder, so check any other + // local folders in this workspace if it's a multi-root workspace + const wFolders = vscode.workspace.workspaceFolders; + if (wFolders && wFolders.length > 1) { + // This is a multi-root workspace for (const wFolder of wFolders) { - if (wFolder.name === workspaceFolder) continue; - const shouldInclude = includeAllFolders || explicitAdditionalFolders.has(wFolder.name.toLowerCase()); - if (!shouldInclude) continue; - addCandidate(wFolder, true); - } - } - - for (const candidate of candidates) { - const candidateLocal = tryLocalUri(candidate.folder.name, candidate.allowNamespaceMismatch); - if (candidateLocal) { - return candidateLocal; + if (notIsfs(wFolder.uri) && wFolder.name != workspaceFolder) { + // This isn't the folder that we checked originally + const wFolderConn = config("conn", wFolder.name); + if (compareConns(conn, wFolderConn) && (!namespace || namespace === wFolderConn.ns)) { + // This folder is connected to the same server:ns combination as the original folder + const wFolderFile = this.findLocalUri(name, wFolder.name); + if (wFolderFile) return wFolderFile; + } + } } } } diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 05016507..99be046b 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -1,25 +1,11 @@ import * as assert from "assert"; import { before } from "mocha"; -import * as path from "path"; // You can import and use all API from the 'vscode' module // as well as import your extension to test it import * as vscode from "vscode"; -import { extensionId, smExtensionId, OBJECTSCRIPT_FILE_SCHEMA } from "../../extension"; -import { getUrisForDocument } from "../../utils/documentIndex"; - -async function waitForIndexedDocument(documentName: string, workspaceFolderName: string): Promise { - const workspaceFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === workspaceFolderName); - assert.ok(workspaceFolder, `Workspace folder '${workspaceFolderName}' was not found.`); - const start = Date.now(); - while (Date.now() - start < 10000) { - if (getUrisForDocument(documentName, workspaceFolder).length > 0) { - return; - } - await new Promise((resolve) => setTimeout(resolve, 100)); - } - assert.fail(`Timed out waiting for '${documentName}' to be indexed in workspace folder '${workspaceFolderName}'.`); -} +import { window, extensions } from "vscode"; +import { extensionId, smExtensionId } from "../../extension"; async function waitForCondition(predicate: () => boolean, timeoutMs = 1000, message?: string): Promise { const start = Date.now(); @@ -32,23 +18,17 @@ async function waitForCondition(predicate: () => boolean, timeoutMs = 1000, mess assert.fail(message ?? "Timed out waiting for condition"); } -function getDefinitionTargets(definitions: (vscode.Location | vscode.DefinitionLink)[]): vscode.Uri[] { - return definitions - .map((definition) => ("targetUri" in definition ? definition.targetUri : definition.uri)) - .filter((uri): uri is vscode.Uri => !!uri); -} - suite("Extension Test Suite", () => { suiteSetup(async function () { // make sure extension is activated - const serverManager = vscode.extensions.getExtension(smExtensionId); + const serverManager = extensions.getExtension(smExtensionId); await serverManager?.activate(); - const ext = vscode.extensions.getExtension(extensionId); + const ext = extensions.getExtension(extensionId); await ext?.activate(); }); before(() => { - vscode.window.showInformationMessage("Start all tests."); + window.showInformationMessage("Start all tests."); }); test("Sample test", () => { @@ -107,56 +87,4 @@ suite("Extension Test Suite", () => { await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); } }); - test("Go to Definition resolves to sibling workspace folder", async function () { - this.timeout(10000); - await waitForIndexedDocument("MultiRoot.Shared.cls", "shared"); - const clientFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === "client"); - assert.ok(clientFolder, "Client workspace folder not available."); - const callerUri = vscode.Uri.joinPath(clientFolder.uri, "src", "MultiRoot", "Caller.cls"); - const document = await vscode.workspace.openTextDocument(callerUri); - await vscode.window.showTextDocument(document); - - const target = "MultiRoot.Shared"; - const sharedOffset = document.getText().indexOf(target); - assert.notStrictEqual(sharedOffset, -1, "Shared class reference not found in Caller.cls"); - const position = document.positionAt(sharedOffset + target.indexOf("Shared") + 1); - const definitions = (await vscode.commands.executeCommand( - "vscode.executeDefinitionProvider", - callerUri, - position - )) as (vscode.Location | vscode.DefinitionLink)[]; - assert.ok(definitions?.length, "Expected at least one definition result"); - const targetUris = getDefinitionTargets(definitions); - const sharedTargetSuffix = path.join("shared", "src", "MultiRoot", "Shared.cls"); - assert.ok( - targetUris.some((uri) => uri.scheme === "file" && uri.fsPath.endsWith(sharedTargetSuffix)), - "Expected Go to Definition to resolve to the shared workspace folder" - ); - }); - - test("Go to Definition falls back to server URI when local copy missing", async function () { - this.timeout(10000); - await waitForIndexedDocument("MultiRoot.Shared.cls", "shared"); - const clientFolder = vscode.workspace.workspaceFolders?.find((wf) => wf.name === "client"); - assert.ok(clientFolder, "Client workspace folder not available."); - const callerUri = vscode.Uri.joinPath(clientFolder.uri, "src", "MultiRoot", "Caller.cls"); - const document = await vscode.workspace.openTextDocument(callerUri); - await vscode.window.showTextDocument(document); - - const target = "MultiRoot.ServerOnly"; - const offset = document.getText().indexOf(target); - assert.notStrictEqual(offset, -1, "Server-only class reference not found in Caller.cls"); - const position = document.positionAt(offset + target.indexOf("ServerOnly") + 1); - const definitions = (await vscode.commands.executeCommand( - "vscode.executeDefinitionProvider", - callerUri, - position - )) as (vscode.Location | vscode.DefinitionLink)[]; - assert.ok(definitions?.length, "Expected definition result when resolving missing class"); - const targetUris = getDefinitionTargets(definitions); - assert.ok( - targetUris.some((uri) => uri.scheme === OBJECTSCRIPT_FILE_SCHEMA), - "Expected Go to Definition to return a server URI when no local copy exists" - ); - }); }); diff --git a/test-fixtures/multi-root/client/.vscode/settings.json b/test-fixtures/multi-root/client/.vscode/settings.json deleted file mode 100644 index c3b581df..00000000 --- a/test-fixtures/multi-root/client/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "objectscript.conn": { - "active": true, - "ns": "USER" - }, - "objectscript.export": { - "folder": "src", - "searchOtherWorkspaceFolders": [ - "shared" - ] - } -} diff --git a/test-fixtures/multi-root/client/src/MultiRoot/Caller.cls b/test-fixtures/multi-root/client/src/MultiRoot/Caller.cls deleted file mode 100644 index 79cad05b..00000000 --- a/test-fixtures/multi-root/client/src/MultiRoot/Caller.cls +++ /dev/null @@ -1,10 +0,0 @@ -Class MultiRoot.Caller Extends %RegisteredObject -{ - -ClassMethod Test() -{ - Do ##class(MultiRoot.Shared).Ping() - Do ##class(MultiRoot.ServerOnly).Ping() -} - -} diff --git a/test-fixtures/multi-root/shared/.vscode/settings.json b/test-fixtures/multi-root/shared/.vscode/settings.json deleted file mode 100644 index 4753cef3..00000000 --- a/test-fixtures/multi-root/shared/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "objectscript.conn": { - "active": false, - "ns": "SAMPLES" - }, - "objectscript.export": { - "folder": "src" - } -} diff --git a/test-fixtures/multi-root/shared/src/MultiRoot/Shared.cls b/test-fixtures/multi-root/shared/src/MultiRoot/Shared.cls deleted file mode 100644 index d176babf..00000000 --- a/test-fixtures/multi-root/shared/src/MultiRoot/Shared.cls +++ /dev/null @@ -1,9 +0,0 @@ -Class MultiRoot.Shared Extends %RegisteredObject -{ - -ClassMethod Ping() -{ - Quit -} - -} diff --git a/test-fixtures/test.code-workspace b/test-fixtures/test.code-workspace index e49cb155..ba637669 100644 --- a/test-fixtures/test.code-workspace +++ b/test-fixtures/test.code-workspace @@ -1,19 +1,15 @@ { "folders": [ { - "name": "client", - "path": "multi-root/client" + "path": "." }, - { - "name": "shared", - "path": "multi-root/shared" - } ], "settings": { "objectscript.conn": { "active": false }, "objectscript.ignoreInstallServerManager": true, - "intersystems.servers": {} + "intersystems.servers": { + } } } From d347d3267cb65538609a3a509025e4b0fffdf3c3 Mon Sep 17 00:00:00 2001 From: LeoAnders Date: Mon, 6 Oct 2025 23:58:00 -0300 Subject: [PATCH 15/15] chore: add dedicated CHANGELOG.md for internal Consistem updates --- src/ccs/CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/ccs/CHANGELOG.md 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)