See WIP.md for the maintenance guide. This file covers the planned twinBASIC IDE add-in that shows the documentation for the symbol under the cursor, and the harness that tests IDE add-ins by machine, which the add-in is developed against.
Status. Stages 1 to 3 stand. Stage 4, the add-in itself in add-in/, has
increments 1 to 9 built and tested: F1 to a page, the help pane, the compiler's hover to tell
which Add, a name with no page shown by its declaration and [Description], offline
help served from an archive in the DLL, hover help, a detached window, a settings panel,
and only the packages the project references.
addin-test.batoperates Samples 10 and 15 end to end and leaves the registry as it found it.- All of Stage 2's questions, P1 to P21, are answered. Most are held by probe lanes that fail when a later IDE build behaves differently.
- Every build publishes
tB/symbols.json, 5,536 names at 4,086 URLs, under a drift guard that fails the build when one of its URLs goes. - The site reads a
themeparameter, and the help pane passes the IDE's theme with it, and apaneparameter, with which a page shows without the site's navigation and header (Stage 4, increment 2).
Facts about the IDE are from BETA 983, and were re-checked on BETA 995 where the text says
so. An offset like main.js@611152 is a byte offset into the one-line ide/main.js. Offsets
move with every build, so in any other build search for the quoted text instead. Each fact is
one of three kinds:
- stated plainly: read in the IDE's code or registry;
- marked (reported): from a static reading or a harvested finding, not re-checked;
- marked with a probe number (P1 to P21): only running the IDE can settle it. Stage 2 lists them.
- Help for the symbol under the cursor. A key press opens the documentation page for the identifier at the cursor, or for the selection. F1, unless P1/P2 rule it out.
- A help pane. A dockable tool window with search and a page view, so that reading the documentation does not mean leaving the IDE.
- Theme-aware. It follows the IDE's light, dark and classic themes.
It also makes a documentation promise real. Permanent
Links says that "the IDE help system" relies on the
/tB/ URLs, but the IDE has no documentation help today: its scripts contain neither
docs.twinbasic.com nor /tB/ (reported). This add-in is the first real consumer of that
contract. WIP.Authoring.md
waits on it before [Description] text and these pages can be connected.
WIP.tbIDE.md has the whole API. An add-in is a Standard DLL that exports
tbCreateCompilerAddin(Host) As AddIn. Through Host it gets toolbar buttons, tool windows
built through a DOM API, KeyboardShortcuts.Add, the active CodeEditor (text, selection,
cursor), the project's virtual file system, the theme, the DEBUG CONSOLE, notifications and
message boxes. It has no call to open a URL, none to run an IDE command, and none to ask
the compiler what a symbol is. Each of those gaps shapes a stage below.
- The compiler loads add-ins, not the page.
bin/twinBASIC_win64.dllbuilds the search path at run time from the pieces\,addins,\,win64,\,*.dll, so its root folders cannot be read off the binary. There are two. One is the install's: the add-in samples build into${IdePath}\addins\${Architecture}\, and the shipped install hasaddins\win32\andaddins\win64\besidebin\, each holdingtbGlobalSearchAddIn1.dll. So every IDE thattbbuild,tbrunandexamples.batstart loads the Global Search add-in, unless it is a private copy with emptyaddinsfolders (loadedAddinsintb-ide-addins.mjsasks the compiler: the real install reportsGlobalSearchAddIn AddIn, the copy reports none). The other is the user's, in the next item. - The compiler also loads the add-ins in
%APPDATA%\twinBASIC\addins\<arch>(P6). The page makes that folder at startup. It gives the host'sCreateCommonFolders(main.js@961019) the text%APPDATA%\twinBASIC, which the host expands in the IDE's own environment, createspackages,themes,locale,addins\win32andaddins\win64in, and returns. The page keeps the path ascommonFolderRootPathand passes it to the compiler when it starts it and when it asks it to load the add-ins (main.js@1047705and@1048667). Measured on BETA 983 and 995 by test/addin/appdata.test.mjs, with a probe add-in that prints the file it was loaded from: an IDE started withAPPDATAnaming a folder of the lane's own loaded the probe from<APPDATA>\twinBASIC\addins\win32, and not a second copy placed inaddinsitself. The compiler loads from the folder it is sent, not from its own%APPDATA%: withcommonFolderRootPathpointed at a second folder and the compiler restarted, it loaded the copy there, while the probe still read the first folder in its ownAPPDATA. So a DLL in the user's folder loads into every IDE the user starts, from any install, and into every IDEtbbuild,tbrunandexamples.batstart. An IDE started withAPPDATAnaming another folder loads none of the user's add-ins, which is how the add-in lanes keep them out (Stage 1, item 3). The FAQ's answer to "Does twinBASIC support addins?" names thewin32andwin64folders and says that a DLL placed inaddins\itself is not loaded. - An add-in runs inside the compiler's process (P10). The process id an add-in read with
GetCurrentProcessIdwas that oftwinBASIC_win32_noDEP.exe, whichtwinBASIC.exestarts as a direct child, beside the page servertwinBASIC_win32.exe --ide=<pid>. So an add-in sees the environment the IDE was started with. A compiler restart is a new process: the toolbar's restart button (#restartIcon, bound totbCompiler_Restart, which callsroot.forceTerminate()) ended the compiler, and the next one, with a new process id, loaded the add-in again and ran itsOnProjectLoadeda second time. The DEBUG CONSOLE was not cleared; the IDE addedrestarting from MEMORY [<project>]. - Load failures have their own messages in the compiler's strings:
Failed to load addin. LoadLibrary() failed.,Entry point not found. Addin may have been compiled for a newer version of the twinBASIC IDE.,Entry point 'tbCreateCompilerAddin' call failed.and...returned an object that does not implement interface IAddInV1.They go to the DEBUG CONSOLE, after the file's name in brackets --- measured with a 32-bit add-in inaddins\win64:[InFolder_win64.dll] Failed to load addin. LoadLibrary() failed.An add-in that failed to load is still in the compiler's list, asUnknown Addin, so a test looks for the name it expects rather than counting. - Holding Shift while a project opens skips the add-ins. The page asks the compiler to
load them only when
shiftKeyDownis false, and otherwise writes[IDE] SHIFT KEY DETECTED: DISABLED LOADING OF ADDINSto the DEBUG CONSOLE (main.js@1048443).shiftKeyDownfollows the keymap'stbMisc_ShiftKeyStateDownand...Upactions, so a test that presses Shift must not do it while a project is opening. - The linker exports
tbCreateCompilerAddinastbCreateCompilerAddin_v3, and the loader takes three names (P14). Read in the compiler's code and measured on BETA 983 and 995 by test/addin/entry.test.mjs. The loader --- in BETA 983 the code at0x1EAB6275intwinBASIC_win32.dll, and in another build the code that pushes the address of the stringtbCreateCompilerAddin_v2, found withdumpbin /disasm; the same intwinBASIC_win64.dll--- asksGetProcAddressfortbCreateCompilerAddin, thentbCreateCompilerAddin_v2, thentbCreateCompilerAddin_v3, and calls the first it finds the same way whichever it is: one argument, theHost,stdcallon win32. It asks what that returns forIAddInV1---{F1BAB9A7-09A3-436C-8B57-A57A76C5DF98}, the package's own --- and reads itsName. For each[DllExport]function, the linker swaps the export name fortbCreateCompilerAddin_v3when the function's name istbCreateCompilerAddin, and exports nothing under the name written. So the three names are not three signatures: the suffix is a version stamp, and an IDE whose loader does not know the stamp refuses the DLL. With the probe's one export patched in four copies, the plain name,_v2and_v3all loaded, and_v4did not, with[EntryV4.dll] Failed to load addin. Entry point not found. Addin may have been compiled for a newer version of the twinBASIC IDE.in the DEBUG CONSOLE and anUnknown Addinin the compiler's list. Both of P7's builds, win32 and win64, exported_v3alone. Every install on this machine, BETA 947 to 995, has the same three names in its loader, and each one's shipped Global Search add-in exports_v3alone, so both stamps are older than BETA 947. - Add-ins cannot be switched off. The Add-Ins menu lists the loaded add-ins with ticks,
and every item calls
notSupportedMenuOption()(reported). - A compiler restart loads every add-in again, from the file in its folder then (P9).
Measured on BETA 983 and 995 by test/addin/reload.test.mjs. A
restart is the toolbar's restart button, every switch of the build target (below), and the
IDE's own restart after a compiler crash. The page's
restartCompiler(main.js@153451) first takes away what every add-in added:removeAddinAlterationsremoves their toolbar buttons and shortcuts, and hides each tool window's body behind the text(currently unavailable), leaving the window where it was. It ends the old compiler withtaskkill /F(forceTerminate,main.js@1050553), so no add-in'sClass_Terminateruns: the probe wrote a line to a file from it, which it did for an object it dropped as it loaded and never for itself. The new compiler then loads the add-ins as it starts, and each runsOnProjectLoadedagain, in a new process, so an add-in keeps no state across a restart. A window it adds again under the same id is the same window, emptied and shown again (below, under Tool windows). So the rebuild loop works without ending the IDE: rename the loaded DLL aside, which P8 allows, put the new build in its place, and restart the compiler. With build A renamed aside, a restart loaded nothing, left no button or shortcut, and left both of A's windows showing the text; the renamed file could be deleted, since the compiler that held it had ended. With build B then copied in under A's name, the next restart loaded B, whose first line came 1.0 s after the click (4.4 s with another lane building at the same time); B had one button, one shortcut that fired B alone, and A's two windows, filled with B's content. A restart's compile settled after 6.6 s, against 7.6 s for a new IDE to open the project and settle its compile, so a harness saves little by the loop; a person keeps the IDE, its open files and its layout. - The build target picks the compiler, and the compiler picks the folder (P7). The IDE
remembers the target of each project in the shared registry, as one JSON object in
IDESettings\targetArchitectureMemorykeyed by project path, and opens a project in the target remembered for it --- or, with none, in the first on its list, win32. With a differently named DLL in each folder, a project with no memory gottwinBASIC_win32_noDEP.exe, which loadedaddins\win32alone; a project remembered as win64 gottwinBASIC_win64_noDEP.exewithtwinBASIC_nativedbg_win64.exe, which triedaddins\win64alone. Switching the target of an open project (Ctrl+F1 / Ctrl+F2) restarts the compiler in the other bitness, and the new one loads the other folders.changedActiveBuildConfiginide/main2.jsrecords the new target and kills the compiler. Measured on BETA 983 and 995 by test/addin/arch.test.mjs, with a 32-bit and a 64-bit build of a probe in both folders of their bitness, the install's and%APPDATA%'s: the project opened in win32, whose compiler loaded the two 32-bit copies alone; a switch to win64 startedtwinBASIC_win64_noDEP.exe, which loaded the two 64-bit copies alone, each reporting that it ran 64-bit; and a switch back loaded the 32-bit ones again. A switch is a restart, with all that the item above says of one. A shipped add-in needs both builds, and one that should keep working across a switch needs both installed.
Read at main.js@608242, @610953 and @611152, and measured on BETA 983 and 995 by P1 and P2,
whose lane is test/addin/keys.test.mjs.
KeyboardShortcuts.Addlowercases the key string, deletes its whitespace and stores it as it is:{CTRL}{SHIFT}d,{SHIFT}DandF1were stored as{ctrl}{shift}d,{shift}dandf1. Matching is a plain string comparison against{ctrl}+{shift}+{alt}+ the key, built in that order --- so{SHIFT}{CTRL}dcould never match, whatever else is true.- Add-in shortcuts are matched on key-up, in
document.onkeyup, after the IDE's own handling of that key-up. The built-in bindings run on key-down, in a capture-phase listener. The key-up only dispatches if the same key's key-down was recorded less than 500 ms earlier. - The key-down is recorded only when Ctrl and Alt are not held:
if((!e.ctrlKey||e.key==="Control")&&(!e.altKey||e.key==="Alt")){realKeyPresses[o]=performance.now()}. So a shortcut containing{ctrl}or{alt}does not fire (P1). Pressed with nothing focused,{ctrl}{shift}d,{ctrl}dand{alt}ffired nothing, whiled,{shift}d,f1and{shift}f1all fired. A record is never cleared, so such a shortcut does fire when the same key was pressed on its own less than 500 ms before: D, then Ctrl+D and Ctrl+Shift+D, and F, then Alt+F, fired all three. So the SDK's own example,{CTRL}{SHIFT}d, does not work. The bug is in BUGS-TO-REPORT.md, and the published KeyboardShortcuts page has a NOTE, the prefix-order rule, which keys fire, and an example on Shift+F12. - F1 fires, and is shared with the IDE (P2). The default keymap binds it
to
tbHelp_ToggleExpandSignatureHelpon key-down (Window.md lists the keymap), a command that acts only while signature help is showing. The add-in'sf1fired with the focus in the code editor, in the DEBUG CONSOLE's entry box and on nothing; it typed nothing, and Monaco's command palette, which Monaco binds to F1, did not open --- the key-down handler callspreventDefault()andstopPropagation()for F1 to F12 (specialKeyMustNotPropagate), so Monaco never sees them. With signature help showing, F1 did both things: the IDE expanded the signature help, and the add-in'sf1fired. The IDE also wrotecommand failed: "tbHelp_ToggleExpandSignatureHelp"to the DEBUG CONSOLE, a bug of its own (BUGS-TO-REPORT.md). An add-in cannot stop the built-in. - A shortcut on a key that types fires as the user types.
dtyped into the code editor, and into the DEBUG CONSOLE's entry box, went in and fired the add-in'sd. - Keys with no binding in the default keymap: Shift+F1, F4, Shift+F4, Shift+F5, Shift+F6, F7 and Shift+F12. A user can rebind any of them.
- Both handlers return at once while a modal dialog or the rename widget is open.
- Letter keys are named from
e.code(KeyDgivesd), every other key frome.key(F1givesf1). So Shift+1 arrives as{shift}!on a US layout.
Read at main.js@1002292 (toolWindowElementAddChild) and @1005960
(toolWindowElementSetProperty), and measured on BETA 983 and 995 by P3, P4 and P12, whose lane is
test/addin/panes.test.mjs.
- A tool window is part of the main document, inside an open shadow root, not an iframe.
On Samples 10 and 15,
toolWindowsByIdis keyed by the second argument the add-in gaveToolWindows.Add("GlobalSearchAddInData","WaynesWindowData"), itsbodyElementis in the shadow root, and the root's host is#toolWindow<n>(#toolWindow900). A window an add-in created and has not shown is there already, and every element in it has no size. A window's content can be taller than the window: Sample 10's eleventh button had a size and a place, but its place was under the window's bottom edge, where a click lands on the resize handle. - A window's id is its identity, and a window given none shares the id
"".createToolWindow(main.js@992211) files a window under the second argument ofToolWindows.Add, andcreateToolWindowByIdreturns the window the page already has under that id, with its body emptied, rather than a new one; the add-in's newToolWindowis then bound to it, since the page answers with its number. P9's window given no id was under"", and the last test of panes.test.mjs opened two windows given no id and got one, titled by the second, holding the second's element and what was then added through the first's object, while the first's own element was gone. So every tool window needs an id of its own; the published ToolWindows page has an IMPORTANT saying so. The same rule is why a restart does no harm to a window with an id: the add-in'sAddafter the restart gets its old window back, emptied and shown again (P9). None of the shipped samples leaves the id out. - An add-in's toolbar button is
#addinButton-<id>, with the id the add-in gaveAddButton, inside#rootMenu2, and its caption as itstitle. HtmlElements.Add(id, tagName)accepts any tag. The four IDE widget tags (chartjs,monaco,listview,virtuallistview) become adivwith extra setup; every other name goes straight todocument.createElement, soiframeis not refused. A parent is found withquerySelector(":scope #"+id), so element ids must be valid CSS identifiers.- Showing a window sets its root's
displaytoblock.toolWindowSetVisiblesetsbodyElement.style.displayto"block"or"none", so a flex or grid layout an add-in puts on the root is gone the moment the window shows: the probe'sdisplay: flexreadblockafterVisible = True, and its iframe kept the default 150 px height. Sample 10 lays its root out as a flex column and gets a block. A child of the root with its owndisplay: flexandheight: 100%, under a root withheight: 100%, keeps the layout; the published ToolWindow page says so. - Property sets go straight to the DOM, as
r[a]=e.value:innerHTML,srcandsrcdocpass through unchanged. A property whose name starts withonis dropped silently, and the call still reports success (P4): the probe's.onclick = "..."raised no error, and the element had neither anonclickproperty nor attribute. The test is on the last name of the path. A step in a property path that is an array is called as a function, so DOM methods can be reached too. - An inline handler inside
innerHTMLruns as the IDE page's own script (P4). It is an attribute, not a property, so it is not dropped. An<img src='data:,' onerror='...'>set throughinnerHTMLran its handler at once, with nothing clicked, and an inlineonclickran when clicked; both sawtypeof openEditors === "object", a global of the IDE's page. That is the one way an add-in can call the page's internals; see Open decisions for where it may be used. Sample 15 already relies on it: each search result it gives its list view'saddItemis HTML with an inlineonclick='raiseEvent("onClickMatch", event, true, path, line, column)', and a click on one runs it. The event travels only from the element that carries the handler: Sample 15's[line,col]label sits beside the clickable line rather than inside it, so a click on the label reaches the handler of the whole file's entry, which opens the file's first match. Text from a file or the user must be escaped before it goes into such HTML; the published HtmlElementProperties page says so. - Events. A name the element has as a property or as
on<name>gets a realaddEventListener, and a copy of the event goes back to the add-in through the compiler, withtargetreduced to itsidandvalue(copyEvent). Any other name is stored as a function on the element, or on the object at the end of the property path, under that name (toolWindowElementSetPropertyCallback) --- that function is whatraiseEventcalls.raiseEventclimbsparentNodeto the first node with arootEventHandlerand callsrootEventHandler[name](event). Only three kinds of node have one: alistvieworvirtuallistviewcontainer (the list view object), the shadow root of anAddMonacoWidgetwidget (the widget's own element), and one of the IDE's dialogs. SoraiseEventfrom plain tool-window HTML throws (P12):TypeError: Cannot read properties of null (reading 'rootEventHandler'), at the shadow root, whoseparentNodeisnull, and the add-in's listener is not called. The stored function can be called directly:onclick='this.parentNode.p12Event(event)'reached the listener its parent registered as"p12Event", witheventInfo.target.id=p12direct. So:AddEventListeneron elements with ids for a few controls; a list view withraiseEventin its items for a list; the direct call for HTML set throughinnerHTMLoutside a list view.
-
The external browser.
ShellExecuteWfrom the add-in DLL. Certain to work; it leaves the IDE. The IDE itself opens links withhostAppObject.Shell('cmd.exe /c start "link" "'+url+'"',1)(reported) --- a host object that only page script can reach. -
An
iframein a tool window --- it works (P3, BETA 983 and 995). Nothing refuses the tag. No file underide\sets a Content-Security-Policy --- there is noContent-Security-Policy,http-equivorframe-ancestorsin anyide\*.htm--- and neither does the compiler's HTTP header template (reported). The host DLL,bin/twinBASIC_ide_win32.dll, names no WebView2 navigation event at all, so nothing intercepts a frame's navigation. So whole pages inside the IDE are the cheapest option, and the site's own navigation and search come with it. With the probe lane serving pages itself onlocalhost:- the frame loaded the page whose URL the add-in set as
src(the server sawsec-fetch-dest: iframe), followed a link in the page, and moved again when the add-in setsrca second time; the add-in's"load"listener heard every load; - the mouse wheel over the frame scrolled the page;
- with a wrapper's flex layout the frame filled the window below the other elements;
- keys pressed with the focus in the frame go to the page: F1 there did not fire the
add-in's
f1, and did once the focus was back in the IDE's own document; - the page's colour scheme is WebView2's, never the IDE's. The IDE sets none on its
WebView2 --- no
ColorSchemeinmain.js, noPreferredColorSchemein the host DLL --- so a page'sprefers-color-schemeis Windows' app mode by default. On the lab machine all three were dark, so the lane cannot tell them apart; a lab IDE whose WebView2 was told to prefer light (--blink-settings=preferredColorScheme=1added to itsWEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS) showed the framed page light, and its own page reported light, while the IDE's theme stayed dark. The site followsprefers-color-schemeunless its own toggle has stored a choice, so a page given no theme matches the IDE only when Windows' app mode happens to agree; the help pane passes one (Stage 4, increment 2).
The live site works in the frame too (checked in the lab, not a lane). The KeyboardShortcuts page on docs.twinbasic.com loaded in 0.9 s as a cross-site frame, with a process and a DevTools target of its own (type
iframein/json/list; the page'sPage.getFrameTreedoes not list it). It applied its own dark theme, had its search box andlunr, scrolled under the wheel, and a link to Host navigated the frame. The site, served by GitHub Pages, sends neitherX-Frame-Optionsnor a CSP (curl -I), and no page of the built site has atarget="_blank"link, so its links stay in the frame. The host DLL does nameNewWindowRequested, and what it does with a new window was not tried, since it might start a browser. A link to a site that refuses to be framed, such as GitHub, would show the browser's error page in the pane; not tried either. - the frame loaded the page whose URL the add-in set as
-
The IDE's WEBPAGE panel, a second native WebView2 whose default URL is
https://www.google.com, controlled throughhostAppObject.SetAdditionalWebview2Urland thetbWebpage_ShowPanelcommand (reported). Page internals only. -
The IDE's markdown preview, which opens
.mdfiles found under a package (viewFileAsMarkdownPreview) (reported). Our pages use kramdown extensions the preview does not know. A last resort for offline use.
Offline is harder. _site-offline/ works over file://, and a page served from
http://localhost cannot frame a file:// URL. The options are srcdoc with rewritten
links, or files under the IDE's ide\ folder. The IDE serves any file placed there (P13,
BETA 983 and 995), measured by test/addin/ideserver.test.mjs
with files put in a lane's copy of the install:
- The server is the page server,
bin\twinBASIC_win32.exe --ide=<pid>, not the compiler: it was the process listening on the page's port. The page's URL starts its path with a key the IDE makes for the run, a GUID, and the page's base URL points there, so a relative URL is a path underide\. - Fifteen files of the kinds the offline site is made of came back byte for byte: pages,
stylesheets, scripts, images and fonts, in folders two deep, a name with a space in it,
4 MB of JavaScript, and a file written after the IDE had started. A frame given the
relative
srcp13/page.htmlshowed the page, with its stylesheet and script working. - Three things differ from a real web server. A query string makes any request a 404,
so
page.html?theme=darkis not found, and the site'sthemeparameter cannot be a query on this route; a fragment is not sent, and does no harm..html,.json,.jpg,.woff2,.mjsand.txtcome with noContent-Type--- the browser sniffs the page and it renders --- while.htm,.css,.js,.svg,.pngand.gifget the usual types. A folder is not a page, and nothing is served without the key. - A page served this way is on the IDE page's own origin, so its script can reach the
IDE's internals: the framed page read
typeof parent.openEditorsas"object". Only our own pages, then, andsandboxon the frame if that matters.
So the offline site would work copied into ide\, with one cost the live site does not
have: writing into the install, which every new build replaces. The add-in serves the
offline site itself instead (Stage 4, increment 5), so this route is not taken.
-
The add-in API gives the editor's text, selection and cursor, and nothing about symbols. Hence the add-in's own word extraction (Stage 4) and, for context, the compiler's hover asked through the page (increment 3), with its own parser of the project's source deferred.
-
The compiler knows, and names the package, the container and the kind (P5). Measured on BETA 983 and 995 by test/addin/symbols.test.mjs, which puts each question the way the IDE's own code does. Hover (
main.js@842393) returns markdown: for a procedure, its declaration, then a heading naming where it is declared, then its[Description]text, which for a VBA function is several paragraphs:Function MsgBox ( ByRef Prompt As Variant, ... ) As VbMsgBoxResult ___ ## **MsgBox** `in VBA.Interaction`The heading gives the package and the module for a function (
VBA.Interaction,VBA.Conversion,SymbolsProbe.Symbolsfor the project's own), and the package and the interface for a class's member:c.Addisin VBA._Collection, andHost.ToolWindows.Addin tbIDE.IToolWindowsV1. Those are the classes' default interfaces, not the names the documentation's pages have; hover over the class itself says*class* **Collection** ... in package VBAand lists*[default]* VBA._Collection. Other kinds have forms of their own, withinafter the name on the first line: an enumeration*enum* **VbMsgBoxStyle** ... \in component VBA.Constants`, its valueenum-value vbOKOnly = 0 ... `in VBA.Constants.VbMsgBoxStyle`, a constantconstant vbCrLf As String = ...then ``in VBA.Constants``. A VB control's member is named by its control's interface,cb.Valuein VB._CheckBox, which theinterfacesmap lacks, and aPropertyBag'sReadPropertyby an interface its default one inherits,in VBRUN.PropertyBag_VB5. Some members name **another procedure**, the one behind them:Err.NumberisGetErrNumberin VBA._HiddenModule,App.PathGetAppPathandScreen.WidthScreenGetWidth, bothin VB.HiddenModule2. A member insideWithis named as it would be written in full, and a function called as a statement, with or without parentheses orCall, as it is in an expression. A comment gets the empty answer a statement gets. The symbols lane asserts the VBA and VBRUN forms, the help lane what the add-in does with the VB and tbIDE ones. A variable gives its declaration,local variable Dim c As Collection.Debug,Debug.Printand a statement such asDimgive nothing, and a type such asLonga line about it. BETA 983 gives no hover for those three, and BETA 987 and 995 a hover whose text is empty, so the add-in must treat both as nothing. Over a procedure's name in its own declaration, hover gives a debug block instead,TB-DEBUG CODEGEN SIZE: [NOT-READY]; over aByValparameter of a class,String,VariantorObjectit adds a wrong note aboutOption Explicit` (BUGS-TO-REPORT.md). -
Go To Definition names the package's own source. Definition (
@846297) returns one file and range. ForMsgBoxthe file istwinbasic:/SymbolsProbe/Packages/tbIDE/Packages/VBA/Sources/Interaction.twinand the range the declaration's lines, and the IDE's file system opens the file. Each package's own references sit under itsPackagesfolder again, so in a project that references tbIDE, VBA is in the tree twice, and definition named tbIDE's copy: the package is the name after the lastPackages/. The file is the module for a function, and for a class's member the file the interface is in (Collection.twin,ToolWindows.twin). Nothing forDebug.Print. -
Signature help and completion say the same. The signatures that the completion request returns (the request the code editor's intellisense sends) carry documentation that starts with the same heading, for package procedures as for the project's own: P2 saw
in AddinHost.Haystackin the expanded signature help. The request for a completion's details gives its declaring file and line, the same as definition's. -
Only page script can ask. The question is a call on the page's own socket to the compiler, answered through a callback. A harness makes it over CDP; an add-in could only through an inline handler in HTML it sets (P4), which Open decisions keeps for probes, except for the help add-in's hover (Stage 4, increment 3).
Host.ShowMessageBoxandHost.ShowNotificationare drawn in the page, not as native dialogs, and a harness reads them and clicks them (checked on Sample 10). A message box is a.modalDialogContainerholding a.modalTitleBar(the title as a text node, then a close button), a.simpleMsgBoxwith the message and a.msgBoxButtonper button; the add-in's call returns once one is clicked. A notification's text is the.msgBoxTextof one of the fixed boxes#msgBox1to#msgBox3.- The IDE calls
alert()at 37 sites, 33 inmain.jsand 4 inmain2.js, and neverconfirm()orprompt(). Analert()blocks the renderer, so the harness records and dismisses every one (Page.javascriptDialogOpening, thenPage.handleJavaScriptDialog), whichattachIdedoes. The ones an add-in test could reach: the rename provider (alert("need to massage workspace edits here...")), Find with an invalid regular expression, and an unknown message on any of the compiler's sockets. A notification's "Copy to clipboard" link also callsalert(), but plainShowNotificationmessages hide that link. An alert that opened before the harness attached cannot be dismissed over CDP: the page then answers nothing, and the harness reports it as the likely cause. The IDE's own candidates are its "IDE startup failure" alert and "Bad command line syntax.", whichlaunchIde's single argument never provokes.
- All IDE settings are in
HKCU\Software\VB and VBA Program Settings\twinBASIC_IDE:IDESettings(13 values ---GENERAL,GENERAL2,LAYOUTv2,WEBPANEL_SETTINGS,targetArchitectureMemory, the licence values, ...),ProjectState(one value per project ever opened),RecentlyOpened(21 values) andWindow(6). Every IDE a user runs shares them, and so does every IDE a harness starts. A harness that opens temp projects fillsProjectStatewith them and pushes the user's own recent projects out ofRecentlyOpened, so every harness tool puts the registry back (Stage 1). - The
.twinprojassociation isHKCU\Software\Classes\.twinproj→twinBASIC.ProjectFile, and it points at the newest install'stwinBASIC.exe. The IDE rewritesDefaultIconandshell\open\commandwhen its path differs (an IDE copy in%TEMP%pointed them into the copy; the real install's IDE left them alone), and does not rewrite them on every launch. So a copy points the user's association at a folder that is about to be deleted, and tb-registry.mjs puts it back (three writes). %APPDATA%\twinBASICholds the user's downloaded packages and emptyaddins\win32,addins\win64,localeandthemesfolders, and it is shared by every install. A compile session writes nothing to it.SaveSettingfrom an add-in writes to the same tree, underVB and VBA Program Settings\<app name>, so it is shared with any installed copy of the same add-in. A test that changes an add-in-wide option changes it for the user too, which is why the add-in runner records and puts back every application a lane names (Stage 1, item 7).- WebView2 profiles:
%LOCALAPPDATA%\twinBASIC\v0for the IDE ---tbbuildreplaces it per port withWEBVIEW2_USER_DATA_FOLDER--- and%LOCALAPPDATA%\twinBASIC_WebPanel\v0for the WEBPAGE panel (reported).
The add-in's source goes in add-in/ at the repository root, as an exported source
tree (Settings plus Sources/*.twin, the shape tbrun takes), so that it diffs as text
and stays outside docs/, where the publish allowlist would refuse it. add-in/ holds the
add-in's tree and nothing else: stageProject copies the whole folder it is given and packs
the copy, so a probe kept inside it would be packed into the add-in's project.
The symbol index is committed there, as Resources/SYMBOLS/symbols.json, which the add-in
reads as a resource, so the tree builds as it is. It is a copy of the docs build's
tB/symbols.json, and a local build rewrites it whenever the index changes (a page added, a
heading reworded), to be committed with the pages
(builder/addin-index.mjs). Built without it, the add-in loads and says no index in its
loaded line.
Built. Everything after this stage is developed against it. WIP.Harness.md has the harness side of each item.
-
One library for starting and driving the IDE: scripts/lib/tb-ide.mjs, shared by
tbbuild,tbrunandcheck_examples. Every CDP call has a time limit, and the connection records and dismissesalert()dialogs. An alert already open before the harness attached is the one case it cannot handle, and it says so.launchIderefuses a DevTools port another IDE holds, since the harness would otherwise operate that IDE. -
A private IDE for every lane: scripts/lib/tb-ide-copy.mjs, described in WIP.Harness.md, A private IDE for every lane. It is a copy, not hardlinks: a session writes nothing into its install (P11), and without
projects\the copy is 57 MB and takes 380 ms. The copy'saddins\win32andaddins\win64are real and empty, so a test add-in never loads into the user's IDE and two lanes never share one. The IDE runs inside a kill-on-close job (The IDE runs inside a job), so a run that dies takes its IDEs with it and no restarted compiler outlivestaskkill /T. -
The registry is left as it was found, for every harness tool: scripts/lib/tb-registry.mjs, described in WIP.Harness.md, What a run leaves in the registry.
- It saves
HKCU\Software\Classes\.twinprojandtwinBASIC.ProjectFilebefore a run and restores them once the last lane has ended, and never puts back an association that points into the temp folder. - It deletes the
ProjectStatevalues the run created and restores the wholeRecentlyOpenedlist as found. Match paths with either separator: some tools store them with forward slashes (C:/Users/.../Temp/tbprobe/...). A full list loses its oldest entry for every project a run opens. - It puts back the build target the IDE remembers for each project path, because a lane
that inherits
win64builds and loads the wrong bitness. - The add-in runner (item 7) records the
SaveSettingkeys a lane names and puts them back. - Every IDE a lane starts, the add-in builds' included, gets an
APPDATAof the lane's own,<work>\appdata, and the lane checks afterwards that the IDE's add-ins folder is under it (checkAddinsRootintb-ide-addins.mjs). So the user's add-ins never load into a test IDE, and the user need not move them out to run the tests. - A recent list shorter than 21 entries gets its empty slots filled with copies of the last entry (an IDE bug, in BUGS-TO-REPORT.md).
The complete answer would be a separate Windows account for test runs, which only the user can create.
- It saves
-
Build, then load.
buildAddinin scripts/lib/tb-addin.mjs builds the add-in with the lane's copy into<work>\out\, for win32 or win64, andaddAddinintb-ide-copy.mjsthen puts the DLL in the copy'saddins\<arch>. Built straight intoaddins, a rebuild would meet the previous build loaded by the very IDE doing the building, and a loaded add-in cannot be overwritten (P8). WIP.Harness.md, Building an add-in and loading it has how the build log is read. The tree staging is scripts/lib/tb-project.mjs, shared withtbrun. One project per IDE, as always: build, end that IDE, then start the same lane IDE on a test project, and its compiler loads the add-in as it starts. -
Operating the IDE and reading it, as library calls over CDP: scripts/lib/tb-operate.mjs, with
readCrashintb-ide.mjs, described in WIP.Harness.md, Operating the IDE and reading it.- open a file:
fs.tree.resolvePath("twinbasic:/<Project>/Sources/<file>"), thenopenEditors.openFile(node,false,false,false,line,col), line and column counted from 1; - move the cursor or select:
window.editoris the one Monaco code editor, given the model of whichever file's tab is selected (setPosition,setSelection,getModel().getValue()). Notmonaco.editor.getEditors(), which also returns editors that add-ins created (reported); - press keys:
Input.dispatchKeyEventkey-down, then key-up, with realkeyandcodevalues, less than 500 ms apart; - click: real
Input.dispatchMouseEventpresses at the element's centre. The IDE's own controls ignoreelement.click().clickwaits up to five seconds for its target, since a list view draws a row a moment after the row is in its data; - ask which add-ins loaded:
loadedAddins(c)intb-ide-addins.mjs, which lists a DLL that failed to load asUnknown Addin; - build the open project:
buildProject(c)intb-ide.mjs; - read a tool window through
toolWindowsById[<guid>].bodyElement; read the DEBUG CONSOLE's backing array, notifications and message boxes; dismiss anyalert(); notice a compiler restart or crash, astbbuild's console check does.
- open a file:
-
No real side effects:
TB_ADDIN_TEST. An add-in's URL opener treats the variable as set when it is not empty, and then printsopen <url>to the DEBUG CONSOLE instead of starting a browser. On a private desktop a real browser would start where nobody can see it and outlive the run.launchIdein tb-ide.mjs sets it to1for every IDE the harness starts,tbbuild's,tbrun's andexamples.bat's included, because each of them loads whatever add-ins the user has installed, on a desktop nobody watches; a caller'senvcan set it otherwise, or leave it out with the valueundefined.openedUrls(c, { since })in tb-operate.mjs reads theopen <url>lines back, andconsoleMark(c)intb-ide-console.mjstakes the mark thatsincenames, so a scenario asks what was opened after the key it pressed. A line counts only when what followsopenhas no white space in it, as a URL has none.PrintTextstores its text escaped (<b>as<b>), so a URL comes back exactly as printed,&included. The rule rests on P10, which is a lane: test/addin/probes/env with env.test.mjs. -
A runner. scripts/addin_test.mjs, with the lanes in test/addin/ and what a scenario gets in scripts/lib/tb-lane.mjs, described in WIP.Harness.md, The add-in test runner. A lane is one scenario file under
node:test, run in a process of its own with its own port and copy of the install; the runner owns the registry, the add-ins' saved settings included, and checks it afterwards. Ctrl+C and a lane timeout both end the lanes and still put the registry back. The wrapper isaddin-test.bat, outside every gate and CI for the reasonexamples.batis: it needs Windows and a twinBASIC install.removeTreeretries a delete that an ending IDE still blocks, since on Node 24rmSync's ownmaxRetriesdoes not.Stage 4's code is tested end to end, through the editor: the help lane puts the cursor or a selection on each case and reads what the add-in opened. Word extraction and lookup have no tests of their own. If a case comes up that the editor cannot set up, a test project would hold those modules and a
[RunAfterBuild]runner printing one line per case, built and run in a lane's own copy rather than bytbrun: atbrunstarted under the runner leaves its registry entries to the runner, which sweeps only the lanes' folders.
Stage 1's acceptance, met: both scenarios pass under addin-test.bat, and the whole
registry, IDESettings included through hashes, is identical around the run.
- Sample 10: toolbar button, then its tool window, then a message box (the three-button
box answered
button2, the follow-up answeredok, then a notification and a DEBUG CONSOLE line). - Sample 15: type a search, see the results, click one, and the right file opens at the
right line (
Haystack.twin, line 4, column 13).
Most probes are a small add-in plus a scenario. P5, P11 and P13 need only CDP and the file system. Record every answer in this file with the build number it was measured on.
All twenty-one questions are answered.
A probe whose answer something else rests on becomes a lane: its add-in in
test/addin/probes/<name>/, its scenario beside the others, listed in lanes.mjs, with each
test asserting what the build did. A later build that behaves differently then fails the
run, and the failure says what to update. The lanes and what rests on each:
- keys.test.mjs (P1, P2): the KeyboardShortcuts page's NOTE and two entries in BUGS-TO-REPORT.md.
- panes.test.mjs (P3, P4, P12, and the shared id
""found with P9): the NOTEs on the HtmlElement, HtmlElementProperties, HtmlElements and ToolWindow pages. - symbols.test.mjs (P5, P15): Stage 4's context. It needs no add-in, only the project in
probes/symbols. - ideserver.test.mjs (P13): the offline route.
- appdata.test.mjs (P6): the lanes' own
APPDATAand the FAQ's answer on where add-ins go. - arch.test.mjs (P7): the Add Ins page's account of which folder
loads when, and the win64 builds
buildAddinmakes. - reload.test.mjs (P8, P9): the account of a compiler restart on the tbIDE package page and the ToolWindows page.
- entry.test.mjs (P14): the entry point's NOTE on the tbIDE package page.
- env.test.mjs (P10): the rule that an add-in under test opens
nothing, which reads
TB_ADDIN_TEST. - widgets.test.mjs (P16, P17): the two routes to hover help increment 6 weighed; the mouse route is the one built. It prints the P16 timings with the tests.
- references.test.mjs (P20): increment 9's route to the packages a project references.
- parent.test.mjs (P21): the WARNING on the FileSystemItem page's Parent, and the reproducer of its entry in BUGS-TO-REPORT.md.
A probe that settles a question once stays in scratch. So do the two P3 checks that need the network or a changed WebView2: the live site in the frame, and the colour scheme with WebView2 preferring light.
The table gives each answer in brief; the sections above have the detail.
| # | Question | What it decides |
|---|---|---|
| P1 | Do {ctrl} and {alt} add-in shortcuts ever fire? Answered, BETA 983 and 995: no. d, {shift}d, f1 and {shift}f1 fire; {ctrl}{shift}d, {ctrl}d and {alt}f fire only when the same key was pressed on its own less than 500 ms before. Queued in BUGS-TO-REPORT.md; the KeyboardShortcuts page has a NOTE. |
the bug report; which key the add-in uses; the NOTE on the KeyboardShortcuts page |
| P2 | Does the add-in's f1 fire with focus in the code editor, and what happens with signature help showing? Answered, BETA 983 and 995: yes. It fires with the focus in the code editor, in the DEBUG CONSOLE and on nothing, and types nothing. With signature help showing, the IDE expands or collapses it as well. |
F1 or another key --- F1 |
| P3 | Does an iframe of a documentation page load and navigate inside a tool window? Answered, BETA 983 and 995: yes. It loads, follows its own links, moves when the add-in sets src, scrolls, and fills the window under a wrapper's flex layout; the live site works too. Keys in the frame never reach the add-in, and the page's colour scheme is Windows', not the IDE's. |
how pages are shown --- in the pane |
| P4 | Does innerHTML render, and do inline handlers in it run page script? Answered, BETA 983 and 995: yes, and yes. Inline handlers run as the IDE page's own script, with its globals in reach. A property whose name starts with on is dropped, and the add-in hears no error. |
how summaries are drawn; whether the page-internals route exists --- it does |
| P5 | What does hover return for MsgBox, Collection.Add, ToolWindows.Add and a symbol declared in the project? What does definition return for a package symbol? Answered, BETA 983 and 995: hover gives the declaration, then a heading naming where it is declared: in VBA.Interaction, in VBA._Collection, in tbIDE.IToolWindowsV1, in SymbolsProbe.Symbols --- a class's members by its default interface, not by the class's name. Definition gives the declaration in the package's own source, which the IDE opens. Nothing for Debug.Print or a statement. Only page script can ask. |
compiler-assisted context is possible; which route is Stage 4, increment 3 |
| P6 | Does the compiler also load add-ins from %APPDATA%\twinBASIC\addins\<arch>? Needs a DLL placed there, which the user's own IDE would load too --- ask before running it against the real %APPDATA%. Answered, BETA 983 and 995: yes, from addins\win32 there and not from addins itself. The compiler loads from the folder the page sends it. The lane points APPDATA at a folder of its own, so no DLL goes in the user's. |
harness isolation --- every lane IDE gets an APPDATA of its own |
| P7 | Which bitness does the compiler start in, and does switching the build target restart it in the other one and load the other addins folder? Answered, BETA 983 and 995: yes, and yes. The target a project opens in picks the compiler, and each compiler loads the folders of its own bitness alone, the install's and %APPDATA%'s. |
building and testing both bitnesses --- buildAddin builds either, and a shipped add-in needs both |
| P8 | Is a loaded add-in DLL locked against being overwritten? Answered, BETA 983 and 995: yes. While its IDE runs, overwriting fails (EBUSY) and deleting fails (EPERM), though renaming works; the hold outlasts the compiler's exit by a few tens of milliseconds. The P9 lane checks all three again. |
the rebuild loop --- the DLL is built outside addins, and copied in once the IDE has ended, or renamed aside first (P9) |
| P9 | Does a compiler restart reload add-ins from disk? Answered, BETA 983 and 995: yes. The restart removes every add-in's buttons and shortcuts, leaves its windows showing (currently unavailable), kills the old compiler so that no Class_Terminate runs, and starts a compiler that loads whatever file is in the folders then. A window comes back by its id. |
a rebuild loop without restarting the IDE --- it works: rename aside, copy in, restart |
| P10 | Does an environment variable set by the harness reach the add-in (Environ$)? Answered, BETA 983 and 995: yes, through the launcher, the IDE and the compiler the IDE starts, and again in a compiler a restart starts. With TB_ADDIN_TEST=1 in launchIde's environment, Environ$ and GetEnvironmentVariableW both returned 1; left out, Environ$ was empty and the Win32 call said unset. WEBVIEW2_USER_DATA_FOLDER, which launchIde always sets, arrived with the lane's port in it. |
the side-effect switch |
| P11 | Does the IDE write into its own install folder during a session? Answered, BETA 983 and 995: no. On 983 a compile, a compiler crash and a tbrun build-and-run left all 233 files byte-identical, mtimes included. On 995 the install (235 files) was byte-identical after exports, a tbbuild and two tbrun runs; %APPDATA% was not re-checked. |
hardlinks or copies --- copies, for safety, at 380 ms |
| P12 | Does raiseEvent from plain tool-window HTML throw? Answered, BETA 983 and 995: yes --- TypeError: Cannot read properties of null (reading 'rootEventHandler'), and the listener is not called. An inline handler that calls the listener AddEventListener stored on its parent, this.parentNode.<name>(event), reaches the add-in. |
how the pane's events are written |
| P13 | Does the compiler's HTTP server serve any file placed under ide\? Answered, BETA 983 and 995: yes, and it is the page server, twinBASIC_win32.exe --ide=<pid>, not the compiler. A frame with a relative src shows the file on the IDE page's own origin. A query string makes a 404, and .html has no Content-Type. |
an offline route --- it exists (Offline) |
| P14 | What do tbCreateCompilerAddin_v2 and _v3 expect? Answered, BETA 983 and 995: what tbCreateCompilerAddin does. The names are version stamps: the linker exports a function named tbCreateCompilerAddin as tbCreateCompilerAddin_v3 alone, and an IDE that knows none of a DLL's names refuses it as compiled for a newer version of the twinBASIC IDE, as a patched _v4 was. |
nothing in the design --- the add-in declares tbCreateCompilerAddin as the package says; the tbIDE page has a NOTE |
| P15 | Does the compiler say anything about a name's [Description], or a COM type library's help string? Answered, BETA 995: hover gives it, after the line naming where the name is declared, for a procedure, a member, a constant, a module variable, an enumeration, a Type, a class and a module, and a type library's member (Scripting.IDictionary.Add: "Add a new key and item to the dictionary."). Classes and modules, the project's and a type library's, are in library <name>. A procedure with none gets the IDE's tip, *no further info available. Tip: use [Description("")] ...*. On a procedure's name where it is declared, hover gives a block of code-generation details (TB-DEBUG CODEGEN SIZE: [NOT-READY]) instead. Signature help gives the same text. A member of an enumeration has none: a [Description(...)] line inside an Enum is not an attribute but a member of its own, named by a bracketed identifier, as VB6 allows (Debug.Print Shade.[Description("The light one.")] prints its value), so hover lists it as a member of the enumeration (twinbasic/twinbasic#2465, closed as not a bug). |
Stage 4, increment 4: a name with no page shows its declaration and description from the hover; no parser of the project's source is needed |
| P16 | What does hover help through the public API cost, and how long does a widget live? Answered, BETA 997: GetSelectionInfo takes 0.5 ms a call, reading Text of a 152-character file 0.3 ms, AddMonacoWidget 2.8 ms and Remove 0.6 ms; a 100 ms AddinTimer ticks every 110 ms. A widget with a column is drawn above its line (below when there is no room), stays when the cursor moves, and is removed by the IDE when another file is shown; Remove on it after that raises no error. An add-in has no cursor or mouse event (only OnProjectLoaded, OnChangedActiveEditor, OnChangedTheme, a tool window's OnClose and a button's OnClick), so it can only poll the cursor, and cannot see the mouse |
Stage 4, increment 6: polling every 250 ms costs nothing worth counting |
| P17 | Can the page's Monaco take a second hover provider for twinbasic, as the add-in would register it through the page? Answered, BETA 997: yes. Its text is shown in the IDE's own hover, above the compiler's, from a synchronous answer and from a promise answered 500 ms later, and goes once the provider is disposed. Registered for "*", which matches less closely than the IDE's "twinbasic", its text is shown below the compiler's. A link in it is drawn as <a data-href="<url>" href="">, and a click listener on the window in the capture phase receives the click, and with preventDefault and stopPropagation nothing is opened. The IDE's hover shows after 1,000 ms (hover:{delay:1e3}) and is off when the user turns off showExtraInformationWhenHovering |
Stage 4, increment 6: the mouse route, the owner's second exception to page internals |
| P18 | What hides a hover once the mouse slides down it past the code editor's bottom? Answered, BETA 997: the IDE's dock. Its resizer along the editor's edge (z-index: 1000) and its drop targets lie over the hover, transparent, and take the mouse; Monaco's _onEditorMouseLeave then hides the hover. Monaco alone keeps a hover the mouse is over: its DOM stays inside the editor's, and the hover is sticky. CDP's mouse reproduces it only in steps small enough to land on the 6 px resizer; the owner's own mouse found it, under a watcher on the IDE's DevTools port that recorded _hideWidgets's stack |
Stage 4, increment 6: the add-in draws every hover above the dock, hover help on or off |
| P19 | Can an add-in show a window of its own, a Form holding the WebView2 package's control? Answered, BETA 997: yes (detach.test.mjs, probes/detach). A minimal hand-written .tbform builds, given _className and _clsid on the form and the control. Show vbModeless gives a visible top-level window, owned by the compiler's process (twinBASIC_win32_noDEP.exe, a child of twinBASIC.exe), with no message loop of the add-in's own; the control is ready about 250 ms after the first Show, 60--80 ms for a later form. A page loaded with NavigateToString posts to the add-in and receives what it posts, and an iframe in it loads an http://localhost page. Hide and Show keep the page; Unload and a new form work; the IDE ending leaves no msedgewebview2.exe of the window's behind. The harness gives the IDE WEBVIEW2_USER_DATA_FOLDER and WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS, the compiler inherits them, and they override the control's own options: with them left in place the control fails, error 8007139F (which of the two, not isolated). With no user data folder set, the control uses bin\twinBASIC_win32_noDEP.exe.WebView2, which the IDE has made already |
Stage 4, increment 7: the detached window, and the variables it removes under the test switch |
| P20 | Can an add-in learn, through the public API, which packages its project references? Answered, BETA 995 and 997: yes (references.test.mjs, probes/references and its three hosts probes/references-*). <Project>/Packages, reached with For Each from Host.FileSystem.RootFolder (twinbasic:/), holds a folder for each package the project references, newest first, then VBRUN and VBA, which are always there; a package's own references are folders inside its own Packages. A folder is named for the package's project, which is the key the symbol index's packages map uses (VB, not the reference's symbol WindowsControlsPackage; Assert, not TwinBasicAssertions); VBComDlg, which only WinNativeCommonCtls references, is the one name the map lacks. IsPackagesFolder is true for every Packages folder and false for a package's. A package is one folder shared by every list it is in, and its Path and Parent name the first one it was found in, so only Name identifies it. The list is complete when Host_OnProjectLoaded runs. Applying a reference added or removed on the References page restarts the compiler with no question asked, and the new add-in instance's list shows the change. A type library is a file in References, not a package. |
Stage 4, increment 9: the packages the add-in offers |
| P21 | What does reading Parent of the file system's root do? Answered, BETA 983, 995 and 997: it crashes the compiler (parent.test.mjs, probes/parent, one IDE per case, the case passed in TB_P21_CASE). The read itself crashes, before the value is used: into an Object or a Folder, in Is Nothing, in TypeName. Each compiler the IDE restarts loads the add-in and crashes again. The project folder's Parent reads, and is the root (twinbasic:/). |
no add-in here reads the root's Parent (P20's tells the root by its path); the WARNING on FileSystemItem's Parent; a BUGS entry |
The index is the symbolIndex task of the build, in builder/symbols.mjs,
and it publishes /tB/symbols.json ---
under /tB/ because the file is part of the same contract as the URLs in it, and its URL is
fixed from here on. Permanent Links documents the format for any reader;
Building and
WIP.Build.md the build side.
It has 5,536 entries at 4,086 distinct URLs; 782 KB, 52 KB gzipped, under 100 ms of the build.
The file is emitted the way assets/js/search-data.json is, so an installed add-in can fetch
a newer index; a copy is also to be built into the add-in, for when the site cannot be reached.
The data model:
{ "name": "Add", "package": "tbIDE", "container": "ToolWindows", "kind": "method",
"url": "/tB/Packages/tbIDE/ToolWindows#add" }name--- as written in source; matched without regard to case.package--- the owning package, ornullfor language keywords and statements.container--- the class or module, ornullfor a top-level symbol.kind---keyword,statement,function,class,module,enum,enumvalue,control,property,methodorevent.url--- the path below the documentation root.
The entries come from the pages, and the packages annotate them. An index built from the
packages would carry page-less names and put the pages' own layout in the wrong place: of the
2,368 public members of VB's documented classes, 173 have no page or heading, and of
WinNativeCommonCtls' 738, 294 --- inherited members that pages name in a sentence
(DTPicker "inherits ... Anchors, Dock, Font ...") or not at all.
- From the documentation: every page's
permalink:exactly as written. Folder-style pages keep their trailing slash (/tB/Packages/VB/CheckBox/), single-file pages do not (/tB/Packages/tbIDE/ToolWindows), so a URL is copied, never assembled. Canonical URLs only, never aredirect_fromalias. A member is either a page or a heading.Collection.Addhas its own page,/tB/Modules/Collection/Add;ToolWindows.Addis a heading on its class page, and its URL uses the id tbdocs gives that heading (### Addgives#add), read from the build and never computed a second time. Two kinds of alias: the$,BandWvariants share the base page (LenB→/tB/Modules/Strings/Len), and attributes are/tB/Core/Attributes#<name in lowercase>. - The reading rules, in builder/symbols.mjs's header: a page's
title and its first heading's comma list name a type or member; a page under a type's page
in the nav is one of its members; a heading on a type's page whose every part is an
identifier and one a member is that member's, as is any heading under Properties, Methods
or Events; an inherited member documented on its declaring type's page has that URL from
every type that inherits it (
CodeEditor.CloseisEditor#close); a page filed under one module and declared in another belongs to the declarer (VBA'sArrayis under Information and declared in_HiddenModule); a$form shares its base's URL; and a Core page's title gives its statement and keywords (Do...Loop:Do, andLoopas a keyword). Every package page gives at least one entry; a build names any that stops. symbols:, an optional frontmatter key, names what a page documents when its title cannot: the (Default) module is_HiddenModule, and the comparison operators page documents=,<>and four more. A page's first heading# \ and \= operatorsmust be written\\=, since\=is a markdown escape.- From the packages: builder/package-api.json, which
scripts/build_package_api.mjs writes from the packages of
an install, through the export of scripts/lib/tb-packages.mjs
(shared with
census_attributes.mjs) and a declaration scanner, scripts/lib/twin-api.mjs. It is committed, likeinter-metrics.json, because CI has no install. 1,379 types in 16 exports, 255 KB. It supplies each symbol's package, container and kind, lists the public symbols that have no page (documentation coverage as a side effect), and supplies each class's default interface, which is what the compiler names a class's members by (P5). Four facts about the packages shaped it:- A package is known by its project name, not its folder's. TwinBasicAssertions is
Assert, all three CEF builds arecefPackage(their declared APIs are identical, and the generator checks that they stay so), and AppGlobalClassObject isAppGlobalClassProject. That is what code writes and hover says, so the index'spackageis the project name, and itspackagesmap gives each project's name on the pages. - Visibility cannot be read off one declaration. CEF's
CefLogSeverityis aPublic Enuminside aPrivate Module, documented and used; CustomControls'Bordersis aPrivate Classa control'sBordersproperty returns;CheckBoxdeclares almost nothing itself, its members being onPrivate Class CheckBoxBaseCtland four classes above it; and VB'sClipboardis a public CoClass whose members are onPrivate Interface _Clipboard. So every declared type is kept and marked, a non-public one keeping its members only when a public type exposes them, by inheritance or as the interface a public CoClass is built on. [Hidden]is not "absent".Module [_HiddenModule]carries it, and its members are globals.- The default-interface map comes from CoClasses:
interfacesmapsVBA._CollectiontoVBA.CollectionandtbIDE.IToolWindowsV1totbIDE.ToolWindows, 37 in all, which is P5's hover answer turned into a lookup. A lookup that takes the compiler's word has to map_CollectiontoCollection.
- A package is known by its project name, not its folder's. TwinBasicAssertions is
- Regenerate
package-api.jsonwithnode scripts/build_package_api.mjswhen the reference is re-indexed against a newer build, and commit it with the pages.
The gates. builder/symbol-baseline.json lists every published URL, and a build that
loses one fails --- the case it exists for is a reworded member heading, which moves an
anchor an installed add-in still holds. A new URL rewrites the baseline. Retiring an entry
follows the rules in Permanent Links.
scripts/check_symbol_index.mjs, a test.bat gate, asserts each derivation rule, each
scanner trap and the guard's refusals on fixtures. Every URL resolves by construction, and
tB/symbols.json is in the online tree's index, so --check-audit-index sees it written.
The build's summary gives the count of public symbols no page documents, 602 today, and
--symbol-gaps <file> lists them: 173 VB and 294 WinNativeCommonCtls members inherited and
named only in prose, 48 WinNativeCommonCtls types --- enumerations and structures declared in
its controls' base classes and its public ...Consts modules --- 39 members of VBA's
_HiddenModule and Interaction, and a tail of a few each.
Names that belong to more than one page, to test lookup against (the generated index has the complete list):
FileSystem--- the VBA module and the tbIDE class.Line--- the VB control, the CustomControls style class, and theLine Inputstatement.App--- the VB page and the AppGlobalClassObject_Apppages.Name--- the statement,AddIn.Name,ToolWindow.Name,HtmlElement.Name, and the controls'Nameproperties.Close--- the statement,ToolWindow.Close,Editor.CloseandProject.Close.Timer--- the VB control (/tB/Packages/VB/Timer/) and the VBA function (/tB/Modules/DateTime/Timer).Add---Collection.Add,ToolWindows.Add,HtmlElements.AddandKeyboardShortcuts.Add.Item---Collection.Item, andItemon six tbIDE classes (Editors,Folder,HtmlElements,HtmlElementProperties,HtmlEventProperties,Toolbars).
Not done, and why:
- The rest of an embedded mode for pages. The theme is done: the site reads a
theme=dark|lightquery parameter (Stage 4, increment 2), since the add-in cannot reach into a cross-site frame. Hiding the header and navigation is a separate, optional question, untested. The IDE's own server answers any URL with a query string with a 404 (P13); the add-in's server (increment 5) ignores the query, so the parameter works offline too. - Keywords with no page of their own ---
ElseIf,Until,Step,To,In,ByVal,ByRef,Optional,As--- are in the index only where a page's title gives them. Adding one is asymbols:line on the page that explains it, which is a content decision per keyword. Deferred. - Data types ---
Long,String,LongPtr--- have their page at/Reference/Data-Types, outside/tB/, so the index cannot carry them without breaking its own rule. Giving that page a/tB/permalink, with the old one inredirect_from:, would. Deferred. - The offline tree has no copy of the index. The add-in embeds its own, and the help archive (increment 5) is the offline tree as built.
Each increment is finished with its scenarios. Worked on by hand, a new build replaces the loaded one without ending the IDE: rename the loaded DLL aside, put the new build in its place, and click the compiler's restart button (P9). The lanes need not: a restart saves a harness about a second against opening a new IDE.
-
F1 to a page. Built, tested by help.test.mjs on BETA 995. F1 takes the selection or the name under the cursor (below) and looks it up in the embedded index. One page is shown in the help pane (increment 2); a miss says
No help for '<name>'throughShowNotification; an empty spot puts the focus in the pane's search box. Several pages fill the pane's results list, labelledContainer.Name, and leave the page as it was.The key is F1: P1 and P2 do not rule it out. It fires wherever the focus is in the IDE's window. The one overlap is signature help: while it shows, F1 also expands or collapses it, and the cursor is then inside a call's parentheses, often on an argument rather than the procedure. If that proves a nuisance, Shift+F1 has no binding of its own. Never a key with
{ctrl}or{alt}while the P1 bug stands.The URL opener honours the test switch. It calls
ShellExecuteW, except whileEnviron$("TB_ADDIN_TEST")is not empty: then it printsopen <url>to the DEBUG CONSOLE and starts nothing (Stage 1, item 6). When the add-in loads it prints whether the switch is on, and every scenario checks that line before it presses anything. An IDE build that stopped passing the variable on to the compiler then fails the run, instead of starting a browser on the private desktop. -
The help pane. Built, tested by the same lane on BETA 995. The tool window
tbDocsHelpPane, titledTWINBASIC HELP, holds a search box, an Open in browser button, a results list and the page in an iframe (P3), laid out by a flex wrapper inside the root, because showing the window resets the root'sdisplay. The toolbar's Help button shows the pane with the focus in the search box.- Search runs on every
inputandkeyup(the box's clear button sends no key): the entries whose name starts with the text, then those that contain it, at most 100; text with a dot is matched againstContainer.Name. Enter shows the first. A row shows the label and the entry'skind. - The results are a list view whose rows carry
raiseEvent("onPick", event, true, <entry number>), as in Sample 15, sinceraiseEventworks nowhere else (P12). Only the number goes into the handler, and the label and kind are HTML-escaped. - The list's height is set by the add-in: the IDE's list view draws only the rows
in view, out of flow, under a host that must have a definite height, and gives that
host none of its own (
createScrollableContainerinmain.js). SoShowResultssets it to the row count times--helpRow, andpane.csscaps it at 35% of the pane. The row height is whole pixels: the list view measures a row withoffsetHeight, which rounds, and rows of 22.75 px overflowed a list of seven by a pixel each, which showed its scrollbar. The vertical scrollbar is left on the list view's default, shown only when there is something to scroll. - Open in browser opens the page the add-in last gave the frame, through
OpenUrl, so it honours the test switch. A link followed inside the frame is not seen: the live site is on another origin, whose location the IDE's page cannot read. - Theme. The pane's stylesheet, the resource
Resources/STYLESHEETS/pane.cssgiven toApplyCss, uses the IDE theme's own custom properties (--themeGeneralPanelBackColor,--themeToolWindowBodyForeColor, ...). They are set on the IDE's document and inherited by the shadow root, so the pane needs no code for a theme change and no separate light and dark stylesheets. The lane checks the background, which the window does not otherwise inherit: the window already has the theme's text colour. - The page's theme is the IDE's: the frame's URL carries
?theme=andThemes.ActiveThemeNameGroup(FrameUrl), before any fragment, and the site applies it before its first paint and keeps it insessionStoragefor the pages reached from it (the head script inrenderHead, andtheme-toggle.js), never in the reader'slocalStoragechoice; a click on the site's theme button replaces it. Without it the page follows WebView2's colour scheme, which is Windows' app mode (P3).Host_OnChangedThemegives the frame its page again with the new theme, so a link followed inside the frame is lost. Open in browser passes no theme. The lane works out the IDE's group from its panel colour, not from the API, checks every page'sdata-theme, and switches the theme with the IDE's own commands (executeIdeCommand("tbTheme_SwitchToLightMode"),...DarkMode,main.jsBETA 995), which save it intwinBASIC_IDE\IDESettings\GENERAL(colorTheme), where the user's own IDE reads it; the registry tidy puts that one entry back (restoreTheme). Fault runs: without the handler only the theme-change case fails; with the site ignoring the parameter every case that shows a page fails. - The page shows without the site's chrome:
FrameUrladdspane=1too, which the head script keeps insessionStorageas it keeps the theme and turns intodata-paneon the root.custom.scssthen hides the sidebar and the header (search box, auxiliary links, theme toggle) and centres the page; the breadcrumbs stay, and the footer, which holds the attribution line. The rules are under:root[data-pane], at (0,2,0), because the dark compilation re-emits the header'sdisplay: flexat (0,4,1). Open in browser passes neither parameter. EveryshowsPagechecks the computeddisplayof the four, and a case follows a link inside the frame and checks that the next page keeps the theme and the layout. Fault runs: the rules underhtml[data-pane]failed every page case in the dark theme, with the sidebar back; the head script not keepingpanefailed only the link case. The lane's pane is narrower than the site'smdbreakpoint, where the header is hidden anyway, so the wide layout was checked with puppeteer over the offline tree at 500, 900 and 1400 px in both themes. - F1 pressed while the focus is in the page goes to the page, not to the add-in, so a lookup from there goes through the search box.
The pane has an id of its own, never none: every window given no id is the same window. And it outlives the add-in. Every compiler restart, which every switch of build target is, ends the add-in and loads a new instance, which gets the same window back, emptied (P9). So the pane is built in
Host_OnProjectLoaded, every time. The page it showed is kept withSaveSetting "tbDocsHelp", "Pane", "Page"(the site path, add-in-wide), which the lane'ssettingsentry restores, and is read back there; the lane checks that a new frame comes back with it.ToolWindow_OnClosebuilds the pane again, as Sample 15 does; nothing tests closing the pane.The lane serves the built site. With the test switch on, the add-in takes its site from
TB_DOCS_HELP_SITE; help.test.mjs servesdocs/_siteon localhost withbuilder/static-files.mjs, the handlertbdocs --serveuses, so each case loads the build's real page, and a 404 fails it. The frame is then same-site, andPage.getFrameTreereads its URL. The offline tree cannot be used: a page served over http cannot frame afile://URL. So the lane needsbuild.batfirst. - Search runs on every
-
Context: which
Add? Built, tested by the same lane on BETA 995. The route is the page's socket (the owner, 2026-10-04): the add-in's own parser would have to be close to a whole twinBASIC parser, so it waits, and the socket stands until upstream gives the API a call for it. For the name under the cursor, F1 asks the compiler's hover about the character the name starts from (Words.NameColumn); a selection is looked up as it is, without asking.- The question is an
<img src='data:,' onerror='...'>set as theinnerHTMLof a hiddendivin the pane (P4). Its handler isResources/SCRIPTS/hover.js, whichCompilerHover.HoverQuestionfills in with a question number, line and character (numbers only) and escapes for the attribute. It askslspSocketfortextDocument/hoverontwinbasic:+ the selected editor'sgetFullPath(), as the IDE's own hover provider does, and answers by calling the function the add-in'sAddEventListener("tbDocsHover", ...)stored on thatdiv(P12's direct call), with a plain object{ seq, status, value }:copyEventkeeps its strings. It answers exactly once:okwith the hover's markdown (possibly empty),unavailablewhen the socket is not open,timeoutafter 3 s,errorwith the exception's text, which the add-in prints. A request sent across a compiler restart never gets its callback, which is why the timeout exists. An answer to an older question is dropped. - The answer is read by
Declared.ReadHover(the forms are in P5 under What is under the cursor) into a name, a package and a container. A package the index does not document is the project's own, an undocumented package's or a type library's, and has no page (increment 4), even though VBA has aBeep. When the hover's name is the name written (case and a trailing$ignored), the entries are those of that package and container, the container mapped through the index'sinterfacesor with its leading_dropped (VB._CheckBoxisCheckBox), else those of that name anywhere in the package (an interface the map lacks, such asVBRUN.PropertyBag_VB5). When it is not ---App.PathisGetAppPath in VB.HiddenModule2,Err.NumberGetErrNumber in VBA._HiddenModule--- the plain lookup's entries are kept to the package, which still picks VB'sAppover AppGlobalClassObject's andErrObject.Numberover every otherNumber. No answer to read (a statement,Debug.Print, a comment, a late-boundo.Add) is the plain lookup. - The fault run (the question made to answer
unavailableevery time) failed the cases that need the compiler:c.Add,.AddinsideWith, a CheckBox'sValue,App.Path,Err.Numberand the project'sBeep. - The socket code is in files of its own,
CompilerHover.twinandhover.js, without comments, so that it can be replaced whole once the API has a call.
- The question is an
-
A name with no page. Built, tested by the same lane on BETA 995, at the owner's request (2026-10-04): a user who documents their own code with
[Description]sees it, and is not sent to a page for another name. When the hover names a package the index does not document --- the project, another package, a type library --- F1 shows, in place of the frame, the declaration, the description and where the name is declared, all from the hover (P15), read byDeclared.ReadSummary; a name with none saysNo description.. Open in browser is disabled meanwhile, and showing a page puts the frame back. The saved page is unchanged, so a restart shows the last page.ReadHoveralso readsin library <name>, a class's or module's: a class of the project named as a documented one no longer finds that one's page.- On a procedure's name where it is declared, the hover is the code-generation block, so
F1 says
No help for 'Beep': it is declared here. - The lane's own race. F1 on the name whose page shows already sets the same
srcagain, which reloads the frame, and until the reload starts the old page looks loaded.f1Showsmarks the page before pressing F1 and waits for a page without the mark. A page showing again after a summary is waited for too, since itssrccan already be right. - The fault run (
ShowPagenot putting the frame back) failed the four cases that show a page after a summary.
-
Offline help. Built, tested by the lane
help-offlineon BETA 995, at the owner's choice (2026-10-05): the add-in's own server, written in twinBASIC, serving one archive embedded in the DLL. The owner's other choices: the archive is a resource; the documentation build makes it (owner, 2026-10-06); the pane uses it when the DLL has it, and the live site otherwise; the symbol index stays a resource of its own.- The archive is
add-in/Resources/HELP/site.zip, gitignored, written fromdocs/_site-offlineby the last step of a local build of the documentation (builder/help-archive-step.mjsoverlib/help-archive.mjs;--no-help-archiveskips it, and CI passes that) (about 21 MB, 1,471 files; the DLL builds with it for win32 and win64). An add-in built without it serves nothing and uses the live site, as before. The offline tree is the one served: its links are relative, so they work on any origin, and its pages hold only their part of the nav tree. - No inflating in twinBASIC. A deflated entry is sent as it is, between a 10-byte
gzip header and the CRC32 and size from the central directory, with
Content-Encoding: gzip; an image or font is stored and sent plain. So the format is fixed by the writer: no zip64, no data descriptors. HelpServerlistens on 127.0.0.1 at a port Windows picks, on the add-in's own thread: Winsock posts each socket's events to a message-only window (WSAAsyncSelect), so there is no second thread. It reads the archive withLoadResDataInternalonce, at project load. A path is found as GitHub Pages finds it (/a/bisa/b.html, or a 301 to/a/b/for a folder, keeping the query), the query is ignored, and every reply closes its connection. It printsserving site.zip, <n> files, at <origin>, which the lane reads, since the port changes with every compiler restart.- The origin is
http://localhost:<port>, not 127.0.0.1. The IDE's page is on localhost, so a frame on another localhost port is same-site and in its frame tree; a frame on 127.0.0.1 is not there at all, and every case that reads the page failed. - Open in browser still opens the live site.
- The lane.
help.test.mjsruns twice:helpbuilds a copy ofadd-in/withoutResources/HELP, whatever the working tree holds, andhelp-offlinebuilds it with an archive made bywriteHelpArchive. A page's status is read from the page itself (PerformanceNavigationTiming.responseStatus), so a 404 fails a case either way. An offline-only case asks the server from Node: a page's gzip body and a stored image byte for byte, a folder's 301, a 404 andHEAD.
- The archive is
-
Hover help. Built, tested by the
helpandhelp-offlinelanes on BETA 997, with the owner's choices (2026-10-06): links in the IDE's hover under the mouse, behind one Hover help checkbox in the settings panel (increment 8), off by default and kept withSaveSetting(Hover,On). It shows a link per page the name has, at most five, labelled with the name, its kind and where it is, as in Help: MsgBox function (VBA.Interaction) (the owner's wording, 2026-10-07;SymbolIndex.Place); a click shows that page in the pane, as F1 does. A name with no page shows nothing, since the IDE's hover already shows its declaration and description.SymbolIndex.Pagesis the lookup F1 and hover help share.HoverHelpraisesPickwith the page's path.- At the cursor there is nothing (owner, 2026-10-07). A widget above the line, added through the public API once the cursor had rested on a name (P16), was built and then removed: it covered the line above whenever typing paused on a name, did not look like the IDE's own hover, and gave nothing the mouse hover and F1 do not. P16 says how to build it again, should the page's provider ever stop working.
- Under the mouse, through the page (P17):
hoverhelp.js, sent through a sink (helpAskMouse) every time the pane is built, disposes the provider and click listener an earlier instance of the add-in left inwindow.tbDocsHoverHelp, and registers new ones while the box is ticked. The provider is for"twinbasic", so the links are the hover's first lines, next to the name, where a long hover shows them without being scrolled (the owner's choice, 2026-10-06; a provider for"*"puts them below the compiler's text). The provider asks the compiler's hover itself, passes the line, the column and the hover to the add-in, and waits up to 4 s for the markdown it answers with (window.tbDocsHoverHelp.answer). The links point at the live site; a click on one whosedata-hrefstarts with it is taken in the capture phase and sent to the add-in, which shows the page in the pane. - The hover stays past the editor's edge (P18). The editor is created with
fixedOverflowWidgets, so a long hover reaches past the code editor's edge, and the IDE's dock resizer along that edge (.dockElementResizer,z-index: 1000) and the dock's drop targets (.dockGroupVerticalDockPointBottom) lie over it: transparent, so the hover shows, but they take the mouse. Monaco takes the mouse reaching them as leaving the editor (_onEditorMouseLeave, itsrelatedTargetnot in the hover) and hides the hover, whether or not hover help is on. Sohoverhelp.js, every time the pane is built, adds a style (#tbDocsHoverStay) that draws.monaco-hoveratz-index: 100000, above them, with the box ticked or not (owner, 2026-10-07: it is for every hover, and goes once the IDE is fixed, twinbasic/twinbasic#2506). Making the resizer take no pointer events is not enough: the drop target under it takes the mouse instead. - In the lanes, the floating pane covers the middle of the code editor, so the cases
move it against the window's right edge before the mouse rests anywhere; and a hover
left open covers the toolbar, so each mouse case moves the mouse away at its end.
test/addin/hover.mjsholds the helpers both lanes use.
-
A detached window. Built, tested by the
helpandhelp-offlinelanes on BETA 997, at the owner's request (2026-10-07): the pane can be moved to another monitor. The owner's choices: the add-in shows a window of its own, a twinBASIC Form holding the WebView2 package's control (P19), rather than awindow.openfrom the page, which would be page internals and which the host most likely sends to the browser (the host DLL namesNewWindowRequestedand importsShellExecuteA; not tried, since it might open a real browser); and the window holds the whole pane, not the page alone.- Detach, a button in the pane's bar, hides the tool window and shows
HelpWindow; Attach in the window's title bar hides the window and shows the pane. The window is made once and then hidden and shown, so its page is kept. It has no owner window and shows in the taskbar. - The title bar (owner, 2026-10-07) is one row drawn by the page, in place of the
Windows caption, which is light even in the dark theme. It is the IDE's tool-window
header:
.sectionHeader, with--themePanelHeaderBackColorand...TextColor, the theme's...Border*,...Paddingand...FontSize, weight 600, the 6-dot grip at the left, 13px, and the pane's title,TWINBASIC HELP. The IDE's row is 20px high (its close button, 16px with 2px margins), so the bar is 28px with the dark theme's padding and 26px with the light theme's; the lane compares the bar's colours, font, padding and height with the pane's own header. The grip and the glyphs are drawn with inline SVG in the text colour, since the IDE's grip is an image, which is not copied. On the right are Attach (a dock icon, "Attach to the IDE"), Minimize, Maximize or Restore and Close, 24px by 20px, in--themePanelCloseButtonForeColor; the light theme's boxed close button is not copied. - Dragging the bar, grip included, moves the window: the page posts
dragonce the left button is down and the mouse has moved 4px, or after the button has been held for 250 ms (the IDE's own title bar holds for 250 ms beforeDragFormStart), and the add-in callsReleaseCaptureand postsWM_NCLBUTTONDOWNwithHTCAPTION, so that Windows' own move loop runs, edge snapping included. A double click on the bar postsmaxrestore, which a drag that waits for movement does not pre-empt. The form's border style isvbSizableNoTitleBar, a sizing frame with no caption, so the edges still resize. Minimize, Maximize and Restore areShowWindow; the add-in tells the pagestate:max,state:normalorstate:minfromForm_Resizeand when the page is ready, and the page swaps the Maximize glyph. Windows maximizes a window with a sizing frame and no title bar over the whole monitor, taskbar included (a page of 1707x1067 against a work area of 1707x1027), so a window procedure put in front of the form's (SetWindowLongPtrW, asHelpServerdoes for its message window) answersWM_GETMINMAXINFOwith the work area exactly. A maximized window with no title bar has no frame taken off its client area (measured at 150%: the client area is the whole window rectangle), so the window outside the work area by the frame's thickness, which an earlier version assumed, was shifted up and left by it and left a strip at the right and bottom; the procedure also answersWM_NCCALCSIZEfor a maximized window whose proposed rectangle is the work area with the rectangle as it came, so that no Windows version takes a frame off it. (A window being restored is still zoomed when itsWM_NCCALCSIZEcomes, with its own smaller rectangle: only the work-area rectangle is answered, or the client area is left the work area's size.) The form's layout does not keep the control at the client area either: with anchors, and without them, it leaves the control overhanging or offset by the frame once the window has been maximized and restored (by 10 and 5 pixels at 150%). So the tbform has no anchors, and the same procedure, which is also put in front with the Windows title bar, sizes the control toGetClientRectafter the window's own handling ofWM_SIZEandWM_WINDOWPOSCHANGED. The lane asks the add-in (the page postsgeometry, under the test switch) for the client area, the control and the work area in screen pixels, and checks that the control is the client area, the page is its size, and, maximized, that the client area is the work area, to the right and bottom edges as well; and the same control check with the Windows title bar. The procedure is taken off inClass_Terminate. - The X hides the window, as the pane's X hides the pane, and the help stays
detached; Attach is the way back to the pane. F1, the Help button and a hover link show
the window again. F1 and a hover link do not activate it, so the editor keeps the
focus: a hidden window is shown with
SW_SHOWNOACTIVATE, and so is a minimized one; the Help button activates it, as it focuses the search box. A window that has to be made is activated, since it is shown withForm.Show. Alt+F4 and the system menu's Close (QueryUnloadwithvbFormControlMenu, cancelled) hide it too. Whether it was shown is kept (Window:Shown), so a hidden window stays hidden after a compiler restart and is not made until the help is asked for. - Show real OS titlebar (the IDE's option,
showRealTitlebarin the JSON object of theGENERALvalue ofHKCU\Software\VB and VBA Program Settings\twinBASIC_IDE\IDESettings, defaultfalse;main.jspasses it toChangeWindowSetting("RealTitlebar", ...)and hides its own title bar's elements) gives the window the Windows caption instead: the border style isvbSizable, the caption texttwinBASIC Help, the page draws no bar and Attach is in the search bar again. The caption is dark withDwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE), 20, falling back to 19, whenThemes.ActiveThemeNameGroupisdark, and light otherwise, andSWP_FRAMECHANGEDdraws it again; a theme change applies it again. The add-in reads the option withRegGetValueWeach time the window is shown, and applies the frame then; the page is toldchrome:customorchrome:native. Under the test switch only,TB_DOCS_HELP_REAL_TITLEBAR, 1 or 0, decides in place of the registry, so a lane never writes the IDE's settings. A form takes a new border style only when something else in its frame is set, which setting the caption does (BaseForm.twin,SyncTitlebarFlag). - Its page is a shell (
Resources/SHELL/) loaded withNavigateToString, with the pane's element ids, sopane.cssapplies unchanged;shell.cssadds what the IDE's document gave the pane, and the results are plain.hitrows rather than the IDE's list view. The page and the add-in exchangeverb:payloadstrings (shell.jslists them); a result the page picks is shown only if it is one of the current results. - One state, two views. The add-in keeps the pane's state either way, and while detached sends every change to the window as well. The pane's frame is not given a page while detached, so no page loads twice; Attach gives it the page it missed.
- The theme: the window's page is not the IDE's, so it has none of the IDE's
--theme*properties.ThemeCssreads the active theme's file as the IDE'sbuildThemeDatadoes (Name: value;to--themeName,/* */comments,inherits:parent first, a theme that inherits nothing and is not Light or Dark inheriting Dark), from the install'sthemesfolder, then%APPDATA%\twinBASIC\themes, and sends it as one:rootrule; the lane checks four properties are the same in the IDE and the window, before and after a change of theme. Which folder the IDE prefers when both hold a file of one name was not found. - Kept with
SaveSetting(Window:Detached,Shown,Left,Top,Width,Height, in pixels): a compiler restart ends the window with the compiler, and the new instance opens it again unless it was hidden. A saved place is used only ifMonitorFromRectfinds a monitor for it, and only a window that is neither minimized nor maximized has a place to save. - Its user data folder is
%LOCALAPPDATA%\tbDocsHelp\WebView2, and under the test switch%TEMP%\tbDocsHelp, which the lanes make private. Under the test switch only, the add-in also removesWEBVIEW2_USER_DATA_FOLDERandWEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTSfrom the compiler's environment (P19), and starts the control with the DevTools port inTB_DOCS_HELP_WINDOW_PORT, through which the lane operates the window's page. The%LOCALAPPDATA%path is not run by any lane. - The lanes read the add-in's test-switch lines:
window shown,window hidden,window state max|normal|min,window dragandwindow chrome custom|native style <hex of GWL_STYLE>. They check the bar's one row and the four buttons, noWS_CAPTIONin the style, Maximize and Restore through the button and a double click, Minimize and F1, a mouse press and move, and a press held, on the bar reaching the add-in, the X, F1 after it, a hidden window staying hidden over a compiler restart, and the Windows caption's style bits withTB_DOCS_HELP_REAL_TITLEBAR=1, for which the lane closes the IDE and starts another, the variable being the IDE's environment. Not tested: the dark caption (no read-back of the DWM attribute), Alt+F4, the registry read, maximizing on a monitor other than the primary one (the lab machine has one), and the move itself, whose loop needs the real mouse, which the harness's desktop has none of.
- Detach, a button in the pane's bar, hides the tool window and shows
-
Settings. Built, tested by the
helpandhelp-offlinelanes on BETA 997, at the owner's choice (2026-10-07): a gear at the right end of the pane's bar, and on the detached window's title bar (in its bar under the Windows caption), opens a drop-down panel of settings, one label row each. A click outside it, Esc, or the gear closes it; changing a setting leaves it open. Each view has its own panel; the settings themselves go through the window's messages as before. In the pane a transparent shade under the panel takes a click outside it. In the window a press over the page's frame never reaches the page, because the frame is cross-site there and runs in a process of its own, so the window'sblurcloses the panel as well. The lanes' pane frame is on the IDE page's site, so one case points it at127.0.0.1to check the shade with a cross-site frame. -
Only the packages the project references. Built, tested by the
helpandhelp-offlinelanes on BETA 997, at the owner's choice (2026-10-07): F1, the hover links and the search offer the language's own entries and the packages the project references; an All packages row in the settings panel, off by default and kept withSaveSetting(Packages,All), offers every package.LoadPackagesreads the references atHost_OnProjectLoadedfrom the virtual file system (P20): the folders of the project'sPackagesfolder and, through each, of its own, keyed onName; a reference change restarts the compiler, so that is enough. If the list cannot be read, every package is offered, and the loaded line says so.SymbolIndex.RestrictlimitsFindandSearch;FindDeclaredis not limited, since the compiler's hover names the declaring package. F1 on a name only an unreferenced package documents says No help for 'X' in the packages this project references. -
Later: how a user gets an add-in with the archive: the owner's plan (2026-10-07) is to build it in CI with everything else, once the compiler runs on Linux there.
Lookup:
- A selection on one line is looked up exactly as selected.
- Otherwise take the dotted name under the cursor. Its last two segments are tried as
Container.Nameand asPackage.Namefirst (VBA.Interaction.MsgBox,VBA.Interaction); a container written with the compiler's leading_matches without it. - Otherwise every entry with that name. For a bare name, the language's own entries
(package
null) win:Closeis the statement. For a qualified name whose qualifier matched nothing (c.Add,Me.Close), only members count, since a statement cannot follow a dot. Several pages left: offer the choice. Entries that share a URL (CodeEditor.CloseandEditor.Close) are one choice.
Word extraction. Given the line and the cursor's column:
- If the character at the cursor is not
A-Z,a-z,0-9or_, take the one before it instead, so that a cursor just after a name, as after typing it, takes that name. If that one is not either, there is no word. - Extend left over those characters and
., to take the whole dotted chain on the left. - Extend right over those characters without
., stopping at the end of the current segment.
With the cursor on Add in Set w = Host.ToolWindows.Add(name, id) this gives
Host.ToolWindows.Add. A chain cut off on its left by ) or by nothing, inside a With,
starts with a dot, which is dropped: .Add. Not handled: string
literals and comments are read like code, so a name in a comment finds its page and a word
in a string usually misses (File.ReadText(CommentsToWhitespace) can blank comments out if
needed); lines joined with _; numbers, which miss the index. GetSelectionInfo counts
lines and columns from 1, as Monaco does (the help lane's cases depend on it).
Settings: per project through Project.SaveMetaData / LoadMetaData, add-in-wide
through SaveSetting / GetSetting --- which is the shared registry tree, so the runner's
registry restore has to include it.
The add-in's own parser, deferred (increment 3 takes the page's socket instead), and the route once the socket is gone, if the API never gains a call for it:
- On
Host_OnProjectLoaded, which runs again after every compiler restart, in a new instance of the add-in (P9), go through the virtual file system withFor Each--- neverCountandItem, because the IDE is multi-threaded (WIP.tbIDE.md) --- read each source file withFile.ReadText, and record declarations only:Module,Class,Interface,EnumandTypeheaders; member headers with theirAstypes;Dim,Private,Public,StaticandConstwith theirAstypes;Implements. - Before a lookup, read again only the files that changed (
File.IsDirty, or a hash). - Resolve
x.Addby finding the declaration ofxin scope and taking its declared type. - Work line by line and skip anything not understood; it is not a compiler. The census
notes in WIP.Harness.md list the
traps a line scanner meets in this language: attributes that span lines, comma-separated
attribute lists, escaped identifiers such as
[_HiddenModule], comments between attribute groups, andType As Longfields. - Scanning on a background thread is possible. Measure the scan first; a second thread inside the IDE's process is the riskiest part of the whole design.
The add-in's code skeletons are written in Stage 4 against the compiler, with tests.
- Build both bitnesses, which
buildAddindoes (P7). The documentation page is docs/IDE/AddIns/Help.md; its pictures are taken byscripts/shoot_help_addin.mjsfromtest/addin/helpdemo, so retake them after a change the pictures show, and update the page's Getting the add-in once increment 10 settles how users get it. - Distribution is upstream's decision: the community add-ins list, or bundled with the IDE.
- Take to upstream, with the probe results as evidence: the shortcut bug; a call to open a URL; a way to ask the compiler about the symbol at a position, whose answer hover already has (P5); tool windows given no id sharing one window. The shortcut bug and the windows are in BUGS-TO-REPORT.md.
Recommended, and not yet confirmed:
- Page internals are for probes only, with two exceptions the owner made: increment 3
asks the compiler's hover through the page's socket, until the API has a call for it
(2026-10-04), and increment 6's mouse route registers a Monaco hover provider through the
page (2026-10-06, P17). Otherwise the shipped add-in uses the public API and
ShellExecuteW, and whatever the API lacks is requested upstream. P4 showed the route exists: any inline handler can reach the page's globals,openEditors, the socket to the compiler andhostAppObjectamong them.raiseEventin a list view's items is another exception, since the IDE's own samples use it that way and it is the only way a list view reports a click. - Isolation starts with restoring the registry (Stage 1, item 3), and a private
APPDATAfor every lane IDE (P6). A separate Windows account for test runs comes only if that proves not to be enough.
Stage 1 is built, so the rules for testing add-ins bind every session and are in
WIP.md, Driving the twinBASIC compiler: no test
add-in in the real install's addins\ or in %APPDATA%\twinBASIC\addins\, no real browser
from a test, every SaveSetting application named in lanes.mjs, IDEs ended by pid, and
one project per IDE.