diff --git a/.github/actions/run-gates/action.yml b/.github/actions/run-gates/action.yml index a781539e2..2cee750fb 100644 --- a/.github/actions/run-gates/action.yml +++ b/.github/actions/run-gates/action.yml @@ -236,6 +236,12 @@ runs: - name: Verify the package test markers and projects (check_package_tests.mjs) shell: bash run: node scripts/check_package_tests.mjs + # Every page's Common Errors section is what scripts/errors.mjs write makes of the + # run-time error data under errors/, and the data is in order. Sources + # only: no twinBASIC, no VB6. + - name: Verify the Common Errors sections against their data (check_errors.mjs) + shell: bash + run: node scripts/check_errors.mjs # The symbol index (tB/symbols.json) is read by the IDE help add-in. A # build that indexes the reference cleanly says nothing about the rules # that did not fire on it, so these probes assert each against the case diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 39d25c9c1..608163ecf 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -6407,3 +6407,532 @@ Severity: medium. A `[RunAfterBuild]` procedure that signs or post-processes the Not tried: a run with F5 (only `[RunAfterBuild]` was observed), a variable of the module's own read with a qualified name (`Other.Limit2`), 64-bit, LLVM. + +--- + +## `ReDim` with an upper bound less than its lower bound raises -2147467259, *Unspecified error*, where VB6 raises error 9 + +**Describe the bug** +`ReDim X(0 To -1)`, `ReDim X(-1)` and `ReDim X(5 To 4, 1)` on a dynamic array (`Dim X() As Long`), all of which give a dimension an upper bound below its lower bound, raise error -2147467259 (`&H80004005`), *Unspecified error*, where VB6 raises error 9, *Subscript out of range*. A handler that tests for 9 does not recognise it. Observed in the compiler's test mode, 32-bit and 64-bit, and in a probe run after a build. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `redim-inverted-bounds-error.twinproj` (attached as `redim-inverted-bounds-error.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass (`ReDim X(0 To 0)`, `ReDim X(-3 To -1)`, and a subscript above a fixed-size array's range, which raises 9) and the four cases on an inverted bound fail with `Actual: -2147467259`; `Err.Description` is *Unspecified error*. + +The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`redim-inverted-bounds-error-vb6.zip`) prints: 9, *Subscript out of range*, for each of the three statements. +```vb +' Regression tests: ReDim with an upper bound less than its lower bound raises error 9, +' "Subscript out of range". Expected numbers are VB6's (the VB6 project beside this one +' prints them). tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: bounds in order, an empty-looking but valid range, and a subscript out of range. + [TestCase] + Public Sub ReDim_OneElement_Succeeds() + Dim X() As Long + ReDim X(0 To 0) + Assert.Strict.AreEqual 0&, UBound(X) + End Sub + + [TestCase] + Public Sub ReDim_NegativeBoundsInOrder_Succeeds() + Dim X() As Long + ReDim X(-3 To -1) + Assert.Strict.AreEqual -3&, LBound(X) + End Sub + + [TestCase] + Public Sub FixedArray_SubscriptAboveRange_Raises9() + Dim A(1 To 3) As Long + On Error Resume Next + A(4) = 1 + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_ZeroToMinusOne_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(0 To -1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_MinusOne_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(-1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_FiveToFour_SecondDimension_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(5 To 4, 1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_ErrorDescription() + Dim X() As Long + On Error Resume Next + ReDim X(0 To -1) + Assert.Strict.AreEqual "Subscript out of range", Err.Description + End Sub + +End Module +``` + +**Expected behavior** +Error 9, *Subscript out of range*, as VB6 raises, and as twinBASIC itself raises for a subscript outside a fixed-size array. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. An error is raised wherever VB6 raises one, so no failure goes unreported, but a handler that tests for 9 does not recognise it, and *Unspecified error* does not say what is wrong. It is the same symptom as "An index out of range on a fixed-size array member of a UDT raises -2147467259, *Unspecified error*, where VB6 raises error 9": a bounds failure that reaches the program as *Unspecified error*, here from the allocation rather than from a subscript. + +What does not reproduce it: bounds in order (`0 To 0`, `-3 To -1`), and a subscript outside a fixed-size array that is not in a UDT. + + + +--- + +## A *compare* of 2 (`vbDatabaseCompare`) raises no error in `InStr`, `StrComp` and `Filter`, where VB6 raises error 5 + +**Describe the bug** +`InStr(1, "abc", "a", vbDatabaseCompare)`, `StrComp("a", "A", vbDatabaseCompare)` and `Filter(F, "a", True, vbDatabaseCompare)` raise no error, where VB6 raises error 5, *Invalid procedure call or argument*. `vbDatabaseCompare` (2) is a setting for Microsoft Access only. twinBASIC runs a textual comparison for it: `InStr(1, "abc", "A", vbDatabaseCompare)` returns 1, as `InStr(1, "abc", "A", 1033)` does, so it seems to take 2 as a locale identifier, as it takes any other value above 1. Observed in the compiler's test mode, 32-bit and 64-bit. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `database-compare-no-error.twinproj` (attached as `database-compare-no-error.zip`). +2. Run its test cases, the module below. On BETA 1010 the five controls pass (`InStr` with 0 and with 1, `InStr` with 3, and `InStrB` and `InStrRev` with 2, none of which VB6 refuses) and the four cases on a compare of 2 in `InStr`, `StrComp` and `Filter` fail with `Actual: 0`; `Err.Description` is empty. + +The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`database-compare-no-error-vb6.zip`) prints: 5, *Invalid procedure call or argument*, for `InStr`, `StrComp` and `Filter` with a compare of 2, and no error for the controls, nor for `Replace` and `Split` with 2. +```vb +' Regression tests: InStr, StrComp and Filter with a compare of 2 (vbDatabaseCompare) raise error 5, +' "Invalid procedure call or argument", outside Microsoft Access. Expected results are +' VB6's (the VB6 project beside this one prints them). tbrun --tests runs each case and +' prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: the two compare settings InStr accepts, and the neighbours VB6 lets + ' through with a compare of 2 or 3. + [TestCase] + Public Sub InStr_BinaryCompare_Finds() + Assert.Strict.AreEqual CVar(1&), InStr(1, "abc", "a", vbBinaryCompare) + End Sub + + [TestCase] + Public Sub InStr_TextCompare_Finds() + Assert.Strict.AreEqual CVar(1&), InStr(1, "abc", "A", vbTextCompare) + End Sub + + [TestCase] + Public Sub InStr_CompareThree_NoError() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", 3) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + [TestCase] + Public Sub InStrB_DatabaseCompare_NoError() + On Error Resume Next + Dim R As Variant + R = InStrB(1, "abc", "a", vbDatabaseCompare) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + [TestCase] + Public Sub InStrRev_DatabaseCompare_NoError() + On Error Resume Next + Dim R As Variant + R = InStrRev("abc", "a", -1, vbDatabaseCompare) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + ' The defect, in InStr, StrComp and Filter. + [TestCase] + Public Sub StrComp_DatabaseCompare_Raises5() + On Error Resume Next + Dim R As Variant + R = StrComp("a", "A", vbDatabaseCompare) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Filter_DatabaseCompare_Raises5() + Dim F(0 To 1) As String + F(0) = "abc": F(1) = "x" + On Error Resume Next + Dim R As Variant + R = Filter(F, "a", True, vbDatabaseCompare) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStr_DatabaseCompare_Raises5() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", vbDatabaseCompare) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStr_DatabaseCompare_ErrorDescription() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", vbDatabaseCompare) + Assert.Strict.AreEqual "Invalid procedure call or argument", Err.Description + End Sub + +End Module +``` + +**Expected behavior** +Error 5, *Invalid procedure call or argument*, from `InStr`, `StrComp` and `Filter` given a compare of 2, as VB6 raises. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. Code brought over from Access VBA that passes `vbDatabaseCompare` gets a textual comparison with no error, where VB6 stops it with error 5. + +What does not reproduce it: a compare of 0 or 1; a compare of 3, which VB6 also accepts; and `InStrB`, `InStrRev`, `Replace` and `Split` with a compare of 2, which VB6 accepts too and twinBASIC accepts likewise. + + + +--- + +## `InStr` raises error 5 for an invalid locale ID when *string1* or *string2* is zero-length, where VB6 raises nothing + +**Describe the bug** +`InStr(1, "", "a", 99999)`, `InStr(1, "abc", "", 99999)` and `InStr(1, "", "", 99999)` raise error 5, *Invalid procedure call or argument*, because 99999 is not a valid locale identifier. VB6 raises no error and returns 0, 1 and 0: its `InStr` returns before it checks *compare* when either string is zero-length. The same holds for a *compare* of -2. twinBASIC's own `InStrB` and `StrComp` already return before the check, as VB6 does. Observed in the compiler's test mode, 32-bit and 64-bit. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `instr-invalid-lcid-empty-string.twinproj` (attached as `instr-invalid-lcid-empty-string.zip`). +2. Run its test cases, the module below. On BETA 1010 the seven controls pass (`InStr` with no zero-length string and with *start* past the end, `InStrRev` and `Replace`, all of which raise 5 in VB6 too, and `InStrB` and `StrComp`, which return as VB6 does) and the five `InStr` cases on a zero-length string fail with `Actual: 5`. + +The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`instr-invalid-lcid-empty-string-vb6.zip`) prints: no error from `InStr` with a zero-length *string1* or *string2*, whatever the *compare*, and the results 0, 1 and 0. +```vb +' Regression tests: InStr given a compare that is not a valid locale ID raises no error +' when string1 or string2 is zero-length, and returns its usual result. Expected results +' are VB6's (the VB6 project beside this one prints them). tbrun --tests runs each case and +' prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: VB6 raises 5 for the invalid locale ID whenever neither string is empty. + [TestCase] + Public Sub InStr_BadLcid_Raises5() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStr_BadLcid_StartPastEnd_Raises5() + On Error Resume Next + Dim R As Variant + R = InStr(5, "abc", "a", 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStrRev_BadLcid_EmptyString1_Raises5() + On Error Resume Next + Dim R As Variant + R = InStrRev("", "a", -1, 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Replace_BadLcid_EmptyExpression_Raises5() + On Error Resume Next + Dim R As Variant + R = Replace("", "a", "x", 1, -1, 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + ' Controls: InStrB and StrComp return before the check, as VB6's InStr does. + [TestCase] + Public Sub InStrB_BadLcid_EmptyString1_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStrB(1, "", "a", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStrB_BadLcid_EmptyString2_ReturnsStart() + On Error Resume Next + Dim R As Variant + R = InStrB(1, "abc", "", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(1&), R + End Sub + + [TestCase] + Public Sub StrComp_BadLcid_EmptyString1_ReturnsMinusOne() + On Error Resume Next + Dim R As Variant + R = StrComp("", "a", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(-1), R + End Sub + + ' The defect: a zero-length string1 or string2 returns before the compare is checked. + [TestCase] + Public Sub InStr_BadLcid_EmptyString1_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStr(1, "", "a", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStr_BadLcid_EmptyString2_ReturnsStart() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(1&), R + End Sub + + [TestCase] + Public Sub InStr_BadLcid_BothEmpty_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStr(1, "", "", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStr_MinusTwo_EmptyString1_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStr(1, "", "a", -2) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStr_MinusTwo_EmptyString2_ReturnsStart() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "", -2) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(1&), R + End Sub + +End Module +``` + +**Expected behavior** +No error, and the result `InStr` gives for a zero-length string with a valid *compare* (0 for a zero-length *string1*, *start* for a zero-length *string2*), as VB6 returns, and as twinBASIC's `InStrB` and `StrComp` return. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. Only a *compare* that VB6 would refuse for any other strings is affected, but a program that ran under VB6 stops with error 5 where a string is empty. + +What does not reproduce it: an invalid *compare* with neither string zero-length, or with *start* past the end of *string1*, which VB6 refuses too; `InStrRev` and `Replace`, which check *compare* first in VB6 as well; and `InStrB` and `StrComp`, which already behave as VB6 does. + + + +--- + +## `String` with a *character* of `Null` raises error 94, where VB6 returns `Null` + +**Describe the bug** +`String(3, Null)`, the **Variant** form of `String`, raises error 94, *Invalid use of Null*. VB6 raises no error and returns **Null** (`VarType` 1), as it does for `String(0, Null)`. `String$(3, Null)` raises 94 in both, and so does a *number* of **Null** in either form, so only the **Variant** form with a **Null** *character* differs. Observed in the compiler's test mode, 32-bit and 64-bit. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `string-null-character.twinproj` (attached as `string-null-character.zip`). +2. Run its test cases, the module below. On BETA 1010 the four controls pass (`String(3, "a")`, a **Null** *number* in `String` and `String$`, and `String$(3, Null)`, which raise 94 in VB6 too) and the three cases on `String` with a **Null** *character* fail: `Actual: 94`, or `runtime error 94: Invalid use of Null`. + +The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`string-null-character-vb6.zip`) prints: no error and `VarType` 1 (**Null**) for `String(3, Null)` and `String(0, Null)`, and 94 for the controls. +```vb +' Regression tests: the Variant form String, given a character of Null, returns Null +' with no error; String$ raises error 94. Expected results are VB6's (the VB6 project +' beside this one prints them). tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: a letter, a Null number in either form, and String$ with a Null character. + [TestCase] + Public Sub String_Letter_Repeats() + Assert.Strict.AreEqual CVar("aaa"), String(3, "a") + End Sub + + [TestCase] + Public Sub String_NullNumber_Raises94() + On Error Resume Next + Dim R As Variant + R = String(Null, "a") + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + [TestCase] + Public Sub StringDollar_NullNumber_Raises94() + On Error Resume Next + Dim R As Variant + R = String$(Null, "a") + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + [TestCase] + Public Sub StringDollar_NullCharacter_Raises94() + On Error Resume Next + Dim R As Variant + R = String$(3, Null) + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + ' The defect: String with a Null character returns Null. + [TestCase] + Public Sub String_NullCharacter_NoError() + On Error Resume Next + Dim R As Variant + R = String(3, Null) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + [TestCase] + Public Sub String_NullCharacter_ReturnsNull() + Dim R As Variant + R = String(3, Null) + Assert.Strict.AreEqual vbNull, VarType(R) + End Sub + + [TestCase] + Public Sub String_ZeroNullCharacter_ReturnsNull() + Dim R As Variant + R = String(0, Null) + Assert.Strict.AreEqual vbNull, VarType(R) + End Sub + +End Module +``` + +**Expected behavior** +`String(number, Null)` returns **Null** with no error, as VB6 does, and as the **Variant** forms of other string functions propagate **Null**; `String$` keeps raising 94. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. Code that relies on **Null** propagating through `String`, as VB6 code reading database fields can, stops with error 94. + +What does not reproduce it: `String$` with a **Null** *character*, and a **Null** *number* in either form, all of which raise 94 in VB6 as well. + + + +--- + +## `StrConv` raises error 5 for `vbWide`, `vbNarrow`, `vbKatakana` and `vbHiragana` with an East Asian *LCID* on a system whose locale is not East Asian, where VB6 converts + +**Describe the bug** +On a system whose locale is 1033 (English, United States), `StrConv("a", vbWide, 1041)`, `StrConv(ChrW(&HFF41&), vbNarrow, 1041)`, `StrConv(ChrW(&H3042), vbKatakana, 1041)` and `StrConv(ChrW(&H30A2), vbHiragana, 1041)` raise error 5, *Invalid procedure call or argument*, although the *LCID* argument, 1041, names Japanese. VB6 converts with the rules of the locale *LCID* names, and returns U+FF41, `a`, U+30A2 and U+3042. The same holds for 2052 (Chinese, PRC) and 1042 (Korean). twinBASIC seems to check the system's locale rather than *LCID*'s. Observed in the compiler's test mode, 32-bit and 64-bit. + +**To Reproduce** +Steps to reproduce the behavior: +1. On a system whose locale is not East Asian, open `strconv-east-asian-lcid.twinproj` (attached as `strconv-east-asian-lcid.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass (`vbUpperCase` with 1041, and `vbWide` with no *LCID* and with 1033, which VB6 refuses too) and the six cases on an East Asian *LCID* fail with `runtime error 5: Invalid procedure call or argument`. + +The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`strconv-east-asian-lcid-vb6.zip`) prints, with the system's locale: no error and the converted characters for 1041, 2052 and 1042, and 5 for no *LCID* and for 1033. +```vb +' Regression tests: StrConv's vbWide, vbNarrow, vbKatakana and vbHiragana convert when +' the LCID argument names an East Asian locale, whatever the system's locale. Expected +' results are VB6's on a system whose locale is 1033, English (United States) (the VB6 +' project beside this one prints them). tbrun --tests runs each case and prints PASS +' or FAIL. The controls assume such a system. +[TestFixture] +Module RegressionTests + + ' Controls: another conversion with an East Asian LCID, and vbWide with no East + ' Asian locale at all, which VB6 refuses too. + [TestCase] + Public Sub UpperCase_Japanese_Converts() + Assert.Strict.AreEqual CVar("A"), StrConv("a", vbUpperCase, 1041) + End Sub + + [TestCase] + Public Sub Wide_NoLcid_Raises5() + On Error Resume Next + Dim R As Variant + R = StrConv("a", vbWide) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Wide_English_Raises5() + On Error Resume Next + Dim R As Variant + R = StrConv("a", vbWide, 1033) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + ' The defect. + [TestCase] + Public Sub Wide_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 1041) + End Sub + + [TestCase] + Public Sub Narrow_Japanese_Converts() + Assert.Strict.AreEqual CVar("a"), StrConv(ChrW(&HFF41&), vbNarrow, 1041) + End Sub + + [TestCase] + Public Sub Katakana_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&H30A2)), StrConv(ChrW(&H3042), vbKatakana, 1041) + End Sub + + [TestCase] + Public Sub Hiragana_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&H3042)), StrConv(ChrW(&H30A2), vbHiragana, 1041) + End Sub + + [TestCase] + Public Sub Wide_Chinese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 2052) + End Sub + + [TestCase] + Public Sub Wide_Korean_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 1042) + End Sub + +End Module +``` + +**Expected behavior** +The conversion, made with the rules of the locale *LCID* names, as VB6 makes it. Error 5 only when neither *LCID* nor, without one, the system's locale is East Asian. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045), system locale 1033 + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. A program that converts Japanese, Chinese or Korean text with an explicit *LCID*, so that it runs on any system, stops with error 5 on a Western one. + +What does not reproduce it: another conversion (`vbUpperCase`) with an East Asian *LCID*; and `vbWide` with no *LCID* or with 1033, which VB6 refuses as well. Not checked on a system whose locale is East Asian. + + diff --git a/WIP.Authoring.md b/WIP.Authoring.md index 644d4b662..26636360e 100644 --- a/WIP.Authoring.md +++ b/WIP.Authoring.md @@ -138,6 +138,16 @@ and a keyword such as `ElseIf` belongs on a page only when the page explains it. Relative links resolve against the **rendered URL** (the page's `permalink:`), not the file path. Pages that share a URL folder can use bare names (`[Y](Y)`); crossing folders needs `../` to climb out. +**A page many pages link to can have a shortcut** (owner, 2026-10-21): `shortcut: ` in +its front matter, and every page links to it as `[x](tb:)` or `[x](tb:#anchor)`, +from any folder, with no `../`. The build (`relativeLinksPlugin` in `builder/render.mjs`) +writes the page's permalink; the fragment is kept as written, and the link check checks it. A +name is a word (letters, digits, `_`, `-`, starting with a letter); an unknown name, a name two +pages claim, and `tb:` as an image's source fail the build. Inside code a `tb:` link is left as +written. The first is `shortcut: Errors` on `Reference/Run-time-Errors.md`, whose anchors are the +error numbers themselves: `[5](tb:Errors#5)`. Any page that wants one may have one; a plain +markdown preview (GitHub, an editor) shows such a link as dead. + The URL prefixes are *not* uniform across packages — VBA pages live one segment shallower than VBRUN pages, so cross-package links are asymmetric: - Core statement → `/tB/Core/` @@ -353,6 +363,13 @@ Add a `> [!NOTE]` callout or rewrite the affected section when source diverges. When in doubt about a tB-specific behavior, check `docs/Features/` and `docs/Reference/index.md` before assuming VBA semantics carry over. +**VBA-Docs content that twinBASIC and VB6 both contradict is commented out, not deleted** +(owner, 2026-10-21): the documentation is meant to become switchable between VB6, VBA and +twinBASIC, and the VBA text should not be lost. Put it in an HTML comment that starts +``, outside any table, so + the VBA text is kept for docs that will one day switch between VB6, VBA and twinBASIC + (WIP.Authoring.md, *twinBASIC deviations from VBA to flag*). The first were the + `vbUseCompareOption` rows of six *compare* tables and two "**Null** is returned" sentences of + String. + +## What a page gets + +A `## Common Errors` section (owner, 2026-10-21, on a reader's request) after the page's main +text and before `## Example`: + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **Mid$** is given a *string* that is **Null** | + +- The number is given in decimal and in VB hexadecimal, as a 32-bit **Long** (owner, + 2026-10-20): -2147467259 (`&H80004005`), 5 (`&H5`), in the table and in the VB6 NOTE alike. +- The number links to its row on the central page as `tb:Errors#` (owner, 2026-10-21): the + central page has `shortcut: Errors` in its front matter and its anchors are the bare numbers. + How shortcuts work: WIP.Authoring.md, *Cross-section linking*. +- The message is `Err.Description` of the raised error (owner, 2026-10-20), in italics. +- *When* is a condition on the arguments or the state, in the reader's terms --- not the + probe's expression. Name the form when only some forms raise (**Mid$**, **LCase$**). +- **What is listed**: what the procedure raises from its own arguments and state, and **any + error a Null argument raises**, in the procedure or in converting the argument (owner, + 2026-10-21: which parameters convert is invisible to the reader; `Filter(Null, "a")`'s 13 is + listed). + **What is not**: errors anything can raise (7 *Out of memory*, 14 *Out of string space*, 28 + *Out of stack space*), errors in converting an argument to the type its page gives for the + parameter (13 for a value of the wrong kind, 6 for a number outside that type's range, such + as 2147483648 for a **Long**), and compile-time diagnostics (TB codes). The central page says + this once. A 6 for a value inside the documented type's range is listed (`ChrB(256)`: the + page says **Long**). +- The heading takes the level of the page's Example heading: `###` on the VBA and Core pages, + which the build promotes to `##`; `##` on a page without an Example or See Also heading. +- Rows of a page that raise the same number are one table row, their *When* cells joined with + `
` in the order of the data. +- A row where twinBASIC raises nothing and VB6 raises (`error: 0`, below) is a NOTE only. +- A procedure that raises nothing gets no section --- unless its absence is surprising enough + to state (`none:`). +- The section is written between markers, so regenerating it replaces only it. + +## The central page + +[`docs/Reference/Run-time-Errors.md`](docs/Reference/Run-time-Errors.md), permalink +`/Reference/Run-time-Errors`, `shortcut: Errors`, under *Reference Section* in the nav and in +the book's *The Core Language* part after Compiler Constants. Hand-written: what the sections +list and leave out, how a number is written, the message rule, and a NOTE on `Err.Raise` of a +number without a message. Generated by `errors.mjs write`, between `` +and ``: + +- a table, one row per number in `messages.yml`: the number, anchored by itself (`#5`, + `#-2147467259`), its hexadecimal, its message (and `Error$`'s where it differs), and + *Listed by*, a link (*22 pages*) to the number's line below, or `---`; +- a section *Pages that list each error*: a line per number some table names (twinBASIC's + number, not the `vb6:` one), anchored `#listed-`, its pages comma-separated and sorted by + title (owner, 2026-10-21). + +VB's numbers come first in order, then the HRESULTs in the order of their hexadecimal. +`check_errors.mjs` checks the index as it checks the sections, and that the page keeps its +shortcut. + +What `errors.mjs messages` found (BETA 1010): + +- **Every number from 1 to 65535 with a message of its own: 88, and twinBASIC 32-bit and VB6 + agree on every one**, for `Error$(n)` and for `Err.Description` after `Err.Raise n`. They + are all in `messages.yml`, whether or not a table names them yet. +- **HRESULTs** are in `messages.yml` only when the data names one: -2147467259 so far. For + these the two messages differ, and the file says `{ raised, error }`. The sections show + `Err.Description` (owner: settled); showing `Error$(n)` instead would be a one-line change in + `renderSection` (`.raised` to `.error`). +- **Differences from VB6 that batch 8 (`ErrObject`) should take up**, none queued yet: + `Err.Raise n` of a number from 1 to 65535 without a message of its own leaves + `Err.Description` empty in twinBASIC below 747, and from 747 up gives the Windows system + message of that number (*Page fault was a transition fault.* for 747); VB6 gives + *Application-defined or object-defined error* (the central page says so in a NOTE). + VB6's `Err.Raise` of a common HRESULT raises its own number instead (`&H80070057` raises 5, + `&H80004001` 445, `&H80004002` 430, `&H80020003` 438, `&H80040154` 429, ...) and describes + the rest as *Automation error* with the HRESULT's text on a second line; twinBASIC keeps the + HRESULT and its text. `Err.Raise 65536` raises 5 in VB6, and `Error$(65536)` raises 6; + twinBASIC raises 65536 as *Automation error*, and `Error$(65536)` is generic. twinBASIC's + `Error$` of a few HRESULTs is VB's message (`Error$(&H80070057)` is *Invalid procedure call + or argument*), VB6's is generic. + +## Tooling + +Their reference is on the Tools pages: +[`errors.mjs`](docs/Documentation/Tools-Defects.md#errorsmjs) and +[`check_errors.mjs`](docs/Documentation/Tools-Toolchain-Tests.md#check_errorsmjs). + +- `errors/` --- the data: one YAML file per area (`VBA.Strings.yml`, `Core.yml`, ...) and + `messages.yml`, each error number's message. +- `node scripts/errors.mjs run [] [--only ]` --- builds one probe from the data + per runtime, runs it, and compares each case with the data. twinBASIC 32-bit and VB6 by + default, win64 for a file that says `win64: true` or with `--runtime win64`. By hand only: + it needs twinBASIC and VB6. A run of the whole Strings file takes a few minutes. A case a + compiler refuses is reported and dropped, and the probe rebuilt. `generate ` writes the + probes without running them; `--keep` keeps the ones `run` built. +- `node scripts/errors.mjs write` --- writes each page's section and the central page's index. +- `node scripts/errors.mjs messages` --- measures `messages.yml` in twinBASIC 32-bit and VB6. +- `scripts/check_errors.mjs` --- the gate in `test.bat` and CI: data in order, every section + what `write` makes of it, no section without data, the index current, the central page's + shortcut. No runtime needed. A data problem makes `run` exit 2 with only "the data has + problems"; `check_errors.mjs` names it. +- `scripts/lib/errors-data.mjs` --- what the two share (`renderSection`, `renderIndex`, + `errorAnchor`, `CENTRAL_SHORTCUT`, ...). + +How a probe runs a case: a `Private Sub C()` of its own, `Dim Result As Variant`, the +case's `decl` lines, `On Error Resume Next`, its `setup` lines (an error there is reported as +a setup failure, not as the case's result), then `Result = ` or its `run` lines, then +`case `. twinBASIC: a tree for `tbrun --exe`, module +`Probe` with `Sub Main` and a `[RunAfterBuild]` that only prints, so the IDE run passes and +the exe runs the cases. VB6: `Probe.vbp` and `Module1.bas` through `runRepro`, appending to +`out.txt` line by line, so a case that kills the exe still leaves the ones before it. + +### The data format + +```yaml +# errors/VBA.Strings.yml +beta: 1010 # the BETA the data was last measured on; titles the VB6 NOTE +win64: false # optional: run measures win64 too (operators, Math) +pages: + Reference/Default/VBA/Strings/Mid.md: # the page, as a path under docs/ + rows: # one table row each, in this order + - error: 5 # the number twinBASIC raises + when: "*start* is less than 1, or *length* is negative" # the When cell + cases: # every case must raise `error` in twinBASIC + - Mid("abc", 0) # a string is an expression: Result = Mid("abc", 0) + - Mid("abc", 1, -1) + - error: 94 + when: "**Mid$** or **MidB$** is given a *string* that is **Null**" + cases: + - Mid$(Null, 1) + - MidB$(Null, 1) + quiet: # cases that must raise nothing; never shown on the page + - Mid(Null, 1) + - Mid("abc", 4) + Reference/Default/VBA/Strings/InStr.md: + rows: + - error: 0 # twinBASIC raises nothing, VB6 raises: a NOTE, no table row + when: "**InStr** is given a *compare* of 2 (**vbDatabaseCompare**)" + vb6: 5 # what VB6 raises instead (0: nothing); writes the NOTE + bug: database-compare-no-error # a bugs/ reproducer, or pending until queued + cases: + - InStr(1, "abc", "a", 2) + Reference/Core/ReDim.md: + rows: + - error: -2147467259 + when: an upper bound is less than its lower bound + vb6: 9 + bug: redim-inverted-bounds-error + cases: + - { decl: Dim X() As Long, run: ReDim X(0 To -1) } + - { decl: Dim X() As Long, run: "ReDim X(5 To 4, 1)" } + quiet: + - { decl: Dim X() As Long, setup: ReDim X(3), run: ReDim Preserve X(5) } + Reference/Default/VBA/Strings/Join.md: + quiet: + - decl: Dim A(1) As Long # block form for a case whose text holds commas + expr: Join(A) + only: tb # not run in VB6 (twinBASIC syntax, or an extension) + - { expr: 'StrConv("a", 256)', only: tb } # single quotes around double quotes +``` + +A case is an expression, or a mapping with `expr` or `run` (statements; a multi-line string +for several) and optionally `decl` (`Dim` lines), `setup` (statements before it) and +`only: tb`. A row's `win64:` is the number win64 raises where it differs. A row of `error: 0` +needs a `vb6:` other than 0 and a `bug:`; its NOTE reads *When ..., VB6 raises error N, ...; +twinBASIC raises none.* A page with no rows gets no section; one with `none: ""` and +no rows gets a section holding that sentence. `messages.yml` maps each number to its message, +or to `{ raised: , error: }` where they differ; `run` fails a +case whose `Err.Description` is not the `raised` text. A number a row names must be in the +file; a new HRESULT is added by hand and then measured with `errors.mjs messages`. A data page +must have `title:` and `permalink:` in its front matter. + +**YAML pitfalls**, each met in batch 2: + +- **A flow list breaks on a comma inside a case**: `cases: [Mid(Null, 1)]` is two items. Use a + block list (`- Mid(Null, 1)`) for any case with a comma; a flow list only for cases with none. +- Quote a case that holds `: `, starts with a quote, or sits in a `{ }` mapping and holds a + comma; in a `{ }` mapping, a case holding double quotes goes in single quotes. + +## How a batch runs + +Read first: [WIP.Harness.md](WIP.Harness.md) (`tbbuild`, `tbrun`), [WIP.Build.md](WIP.Build.md) +(gates), [WIP.Authoring.md](WIP.Authoring.md) (page template, *Cross-section linking*, +*twinBASIC deviations from VBA to flag*), and `scripts/lib/vb6.mjs`'s opening comment +(driving VB6). + +1. **List the batch's pages**, and for each read its syntax, parameters, settings tables and + every sentence that mentions an error, **Null** or a limit. +2. **Explore first.** Write a temporary `errors/Explore.yml` that names the real pages and puts + every candidate case under `quiet:`, then `node scripts/errors.mjs run Explore.yml`: each + case's line shows what both runtimes raised, `[tb 5, vb6 5]`. Try, for every parameter: + **Null**; 0, -1, -2; the upper limit and one past it (strings stop at 1,073,741,823; + 2147483647); a value just past a **Long** (2147483648#, which raises 6 and is not listed); + an empty string; a 2-D array, an unallocated one and a typed one for an array parameter; + **every value a settings table on the page lists**, and values between and beyond them + (a *compare* above 1 is an LCID: 1033 is valid, 99999 is not). Probe a boundary from both + sides before writing it. **Delete `Explore.yml` before committing.** +3. **Write the data** in the area's file, one row per condition, quiet cases for each + boundary's passing side, then `node scripts/errors.mjs run --only "\.md"` until + it says *all agree with the data*. +4. **`node scripts/errors.mjs write`**, then edit the pages: remove each error sentence the + table now carries; comment out, as ``, what both runtimes contradict; move a + VB6 claim that the table's NOTE now makes. Check index pages (a module's `index.md`) for the + same claims. +5. **A difference from VB6**: a row with `vb6:` (or `error: 0`), `bug: pending`, and either + queue it yourself (`node scripts/bug_repro.mjs new `, the entry in + BUGS-TO-REPORT.md, then the slug in the data) or list it for the owner. **Where twinBASIC + accepts what VB6 refuses, ask the owner** whether it is an extension before queuing. +6. **Gates**: `build.bat` (0 broken), `node scripts/check_errors.mjs`, `test.bat`; + `check.bat` once per batch. Commit on staging (the owner's choice for this work), one-line + message, no Co-Authored-By trailer. **When another session shares the checkout, `git add` + by path**, never `-A`, so its files are not swept into the commit. +7. Update *Where things stand* and the batch's row below, and list the questions for the owner. + +What the probes must respect (BETA 1010; `errors.mjs` already does all of it): + +- **Run cases in the built exe** (`tbrun --exe`), and print with `TbRun.Out`, which `tbrun` + adds to the project: `Debug.Print` writes nothing in an exe. A `[RunAfterBuild]` run in the + IDE reads every module-level variable as 0 once one initializer reads another module's + variable (bug `module-init-reads-other-module-ide`), and may differ in other ways. +- **A `tbrun --exe` probe brings its own `Sub Main`**, without the console template's + `tbxMain.twin`; two `Main`s make the build fail with *'Main' is ambiguous*. +- **Never `MsgBox`, `InputBox` or an unhandled error** in a probe: each opens a modal box on a + private desktop. Every case runs under `On Error Resume Next`. +- **Keep the generated code plain.** A module-level or class-field initializer cannot call a + procedure of the project, and a class field that reads another field fails the build (bug + `class-field-init-reads-field`). +- **VB6 cannot compile twinBASIC syntax** (`LongLong`, `Return` in a function, inline `Dim x =`, + generics), and has no counterpart for twinBASIC additions such as StrConv's `vbUTF8` (256). + A twinBASIC-only case says `only: tb`. +- **One case can stop a whole project**: a compile error anywhere fails all of it. `run` + builds a failing project again without the case named in the error. +- **A case that allocates near the string limit raises 14** (`String(1073741823, "a")` in + 32-bit): use it to show that the 5 above the limit is a check of its own, never as a row. + +## Batch 2 findings (VBA `Strings`, BETA 1010) + +Both runtimes agree unless said. The pages and data carry each of these; they are here so a +later batch knows what to look for. + +- **`vbUseCompareOption` (-1) raises 5** in InStr, InStrRev, Filter, Replace, Split and StrComp, + though VBA-Docs lists it: their rows are `` comments, and the module's index no + longer offers it. Omitting *compare* still follows **Option Compare** where the page says so. +- **A *compare* above 1 is an LCID**: 3, 4, 1024, 1033 pass; 255, 65536, 99999 raise 5. + `InStrB` ignores *compare*. +- **`vbDatabaseCompare` (2)**: InStr, Filter and StrComp take it as an LCID where VB6 raises 5 + (`database-compare-no-error`); Replace and Split take it in both. +- **The LCID check and empty strings**: twinBASIC's InStr checks it when a string is + zero-length, VB6's does not (`instr-invalid-lcid-empty-string`); InStrRev, Replace and Split + check it in both; StrComp checks it in neither when a string is **Null** or zero-length. +- **Limits**: Mid, Left, Right (not the B forms), String and Space raise 5 above 1,073,741,823 + characters; InStr raises 5 for a *start* above 1,073,741,824, InStrRev and InStrB do not; + FormatCurrency raises 5 for *numDigitsAfterDecimal* above 255, FormatNumber and + FormatPercent take 2147483647. Chr raises 5 outside 0--255 (on a code page that is not + double-byte), ChrW outside -32,768--65,535, ChrB 6 outside 0--255. +- **Settings ranges** that raise 5: MonthName's *month* outside 1--12; WeekdayName's *weekday* + outside 1--7 and *firstdayofweek* outside 0--7; Format's *firstDayOfWeek* outside 0--7 and + *firstWeekOfYear* outside 0--3; FormatDateTime's *namedFormat* outside 0--4; the Format... + functions' tristates other than -2, -1, 0 and *numDigitsAfterDecimal* below -1; Split's + *limit* and Replace's *count* below -1; StrConv's unlisted values and any sum that combines + vbUnicode, vbFromUnicode, vbUTF8 or vbFromUTF8 with another value. +- **Null**: every $ form raises 94 for a **Null** string where the Variant form returns + **Null**; `String(3, Null)` raises 94 in twinBASIC only (`string-null-character`); the + Format... functions return for a **Null** *expression*. +- **StrConv's East Asian conversions** (vbWide, vbNarrow, vbKatakana, vbHiragana) raise 5 on a + system whose locale is not East Asian; given an East Asian *LCID* VB6 converts and + twinBASIC still raises (`strconv-east-asian-lcid`). +- **Extensions** (owner): `Join` and `Filter` take a `Long` or `Double` array; VB6 raises 5 and + 13 for a `Long` one. + +## Batches + +Each batch is one session. They are independent after batch 0, so any order works, and +each can be run when there is budget. Status is kept here. + +| # | Batch | Pages (about) | Status | +|---|---|---|---| +| 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | done; reviewed 2026-10-20 | +| 1 | The central Run-time errors page | 1 | done; reviewed 2026-10-20; list of pages per number and `tb:Errors` shortcut 2026-10-21 | +| 2 | **Pilot: VBA `Strings`** --- settled the section's style with the owner | 30 | done: 28 sections, 339 cases; reviewed 2026-10-21 | +| 3 | VBA `Conversion` | 28 | not started | +| 4 | VBA `Math` (also win64) | 17 | not started | +| 5 | VBA `DateTime` | 20 | not started | +| 6 | VBA `Information` and `Interaction` | 54 | not started | +| 7 | VBA `FileSystem` | 23 | not started | +| 8 | VBA `Collection` and `ErrObject` (and the `Err.Raise` differences under *The central page*) | 22 | not started | +| 9 | VBA `Financial` | 14 | not started | +| 10 | VBA `HiddenModule`, then `Compilation` and `TbExpressionService` (`Constants` has nothing to raise) | 68 | not started | +| 11 | Core file I/O statements (`Open`, `Get`, `Put`, `Input`, ...: errors 52--75) | 15 | not started | +| 12 | Core arrays and control flow (`ReDim`, `Erase`, `Resume`, `Return`, ...); ReDim has data already, in `errors/Core.yml` | 15 | not started | +| 13 | Operators: the matrix generator and its pages (also win64) | 25 | not started | +| 14 | VB package (37 pages) and the VBRUN pages that document members rather than constants: properties and methods that raise 380, 383, 387 and the like; needs forms | 37 + part of 134 | not started | +| 15 | Built-In packages | 195 | not started | + +Page counts are the folders' `.md` files on 2026-10-20, index pages included; a batch's first +step is to list the pages in it that document something that can raise. diff --git a/WIP.Harness.md b/WIP.Harness.md index f14829655..3f12fdcc6 100644 --- a/WIP.Harness.md +++ b/WIP.Harness.md @@ -1182,6 +1182,34 @@ options and the remembered targets exactly as they are. It writes only its scrat a gate: it needs Windows and a real registry, and the CI runners are Ubuntu. Run it after changing `tb-registry.mjs`. +## Default settings, whatever the user saved + +`IDE_FLAGS` stop an IDE writing its settings, and nothing stops it reading them: the IDE reads its +options, panel layouts, keyboard groups and a few more keys through `hostAppObject.GetIDESetting(key)` +from the user's registry key, so a font size the owner set in their own IDE showed in every IDE the +tools started (a help-lane check failed on 14 px against 13 px). `scripts/lib/tb-defaults.mjs` makes +every IDE `launchIde` starts run with the defaults, in the page only and with no write to the +registry; `try-help-addin` passes `userSettings: true` and keeps the user's. + +The script has to run before `main.js` reads the keys, and `main.htm` commits 0.2 to 0.8 s after the +IDE's browser process writes `DevToolsActivePort`. So `installDefaultsHook` starts before the IDE does, +polls that file every 5 ms (`launchIde`'s own poll is at 100 ms and answers too late), connects to the +browser target, auto-attaches to every target with `waitForDebuggerOnStart`, adds the script to each page +with `Page.addScriptToEvaluateOnNewDocument` and resumes it. The page is `about:blank` when attached, and +never navigates twice. The script puts a setter on `window.hostAppObject`, which `main2.js` assigns, and +wraps the host object: `GetIDESetting` answers `""` for the blanked keys and `"{}"` for `GENERAL` and +`GENERAL2`, every other call goes through with `this` bound to the real object. The keys it left +unanswered are the licence's, `LICENCEAGREEMENTV3`, `ALLOWERRORREPORTINGV3`, `PUBLISHER` and `NEWYEAR_*`. +The recent lists are left alone: `test/ide/recent-projects-copies.test.mjs` needs the IDE's own, and +`shoot_docs.mjs`'s `PAGE_DEFAULTS` empties them where a picture shows them. + +A probe of 50 launches hooked 49. The one miss was a page whose document began before the script was +added. `attachIde` therefore checks `window.__tbDefaults`, the marker the script sets (it lists the keys +answered, never their values), and reloads the page once when it is missing: the hook's session still holds +the script, the reload takes about 40 ms, and the UI and the command-line project come back. A marker still +missing after that is a harness error, exit 2, since the run would have the user's settings. The hook's +connection lives as long as the process, so a `tbbuild --keep` IDE is hooked until `tbbuild` ends. + ## A private IDE for every lane **The compiler loads every DLL in `\addins\win32` or `\win64` as it starts.** A diff --git a/WIP.HelpAddin.md b/WIP.HelpAddin.md index e00f4adfb..1fe061ca2 100644 --- a/WIP.HelpAddin.md +++ b/WIP.HelpAddin.md @@ -1198,6 +1198,12 @@ not: a restart saves a harness about a second against opening a new IDE. 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. + **The window's text is in the IDE's font size** (owner, 2026-10-20): `fontsize.js`, + put into the page by `BuildPane`, sends `--ide-fontsize` to the hidden `helpFont` + element now and whenever a `MutationObserver` sees the root's style change, and the + add-in puts `:root{--ideFontSize:...}` before the theme's rule in the `theme:` message, + which `shell.css` sets the body in. The help lane checks the window's size against the + IDE page's. 8. **Settings**, tested by the `help` and `help-offline` lanes on BETA 1007 (owner's choice): 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 @@ -1233,6 +1239,49 @@ not: a restart saves a harness about a second against opening a new IDE. IDE. **Not done:** the DLLs themselves, built in CI --- the owner's plan is to build them there with everything else, once the compiler runs on Linux there. Why the pack is made the way it is: [WIP.Build.md](WIP.Build.md#the-help-add-ins-project-file). +11. **Every add-in's `{ctrl}` and `{alt}` shortcuts fire** (owner, 2026-10-20), tested by the + lane `keys-help` on BETA 1010: [keys.test.mjs](test/addin/keys.test.mjs) with the help + add-in loaded beside the keys probe. `BuildPane` puts `Resources/SCRIPTS/addinkeys.js` + into the page through a hidden element, as hover help does. It puts in capture-phase + `keydown` and `keyup` listeners on `window`, which run before the IDE's own: they record a + press made with Ctrl or Alt (not AltGr), and on its key-up, within 500 ms, run + `addinKeys` for it with the modifiers held at the key-down. They also delete the IDE's + `realKeyPresses` record of the key, which is from an earlier plain press, so the key-up + does not also run as the plain key (the bug's second half, and the IDE's own key-up + bindings, which it never runs for a modified key). The script goes in only while + `globalKeyDown`'s text still has the guard (see [Keyboard shortcuts](#keyboard-shortcuts)), + so a build that fixes the bug is left alone; a second run replaces the first one's + listeners. The IDE's key-down bindings still run beside an add-in's shortcut: Ctrl+- + is `tbEditor_DecreaseFontSize` as well. A settings-panel row, on by default and kept with + `SaveSetting` (`Fixes`, `AddinKeys`), turns it off: the script has a `$ON$` switch as + `hoverhelp.js` has and runs again at each change, taking the listeners out when off. The + window's panel has its own box, in step through `addinkeys:<1|0>` both ways. +12. **Ctrl+F in the configuration editors** (owner, 2026-10-21). Manage Keyboard Shortcuts + and Manage Panel Layouts (`showConfigEditor`) edit each entry in a plain + `textarea.genericMonospaceTextArea`, made again whenever another entry is picked, not + in Monaco, so there is no find widget to switch on; and `globalKeyDown` returns before + the keymap while a modal dialog is open. `Resources/SCRIPTS/textfind.js`, put in by + `BuildPane`, adds a capture-phase `keydown` listener on `window`: Ctrl+F in such a text + area puts a find box over it (positioned in the text area's parent), Enter and F3 select + the next match, Shift with either the one before, round past the end and in any case, + and Esc closes the box (the Visual Studio keys: F3 and Shift+F3 work with the box closed, + on the last query, and open the box when there is none; Ctrl+F3 and Ctrl+Shift+F3 take + the one-line selection, else the word at the caret (letters, digits, `_`), as the query + and search without the box). A match is scrolled to by measuring it in an off-screen + mirror of the text area (`matchRect`), a third down and clear of the box; + `window.tbDocsTextFind.visible(area)` tells the test whether the selection shows. Each handled key is stopped at `window`, so the dialog's own + Escape handler (which would close it unless the focus is in a text field) never sees it. + Focusing the text area does not reliably scroll it to the caret. A second + settings-panel row (`Fixes`, `TextFind`, on by default) turns it off, by the same + `$ON$` switch, and the window's box follows through `textfind:<1|0>`. + + **Zoom, not done** (owner, 2026-10-20: the IDE's own Ctrl+/Ctrl- and Ctrl+Alt+/Ctrl+Alt- + font commands are enough for now). WebView2's own zoom is off in the IDE and no host call + reaches the main WebView2's zoom; CSS `zoom` on the page leaves Monaco at its old width + and puts context menus and hovers at the pointer's position times the zoom; the font + options scale text but not icons or controls. Emulated device scaling (what a WebView2 + `ZoomFactor` does) was right throughout, so whole-IDE zoom needs that host call + upstream. The probe and pictures are in `.claude/tooling-review-scratch/zoom/`. **Lookup:** @@ -1325,3 +1374,9 @@ The rules for testing add-ins bind every session and are in 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. + +**A change a user of the add-in would notice gets an entry in `add-in/CHANGELOG.md` and a +new version** (owner, 2026-10-20), in the same commit: `project.versionMajor`, `Minor`, +`Build` and `Revision` in `add-in/Settings`, with `versionAutoIncrement` left `None`, so +that a lane's or a reader's build changes nothing. The changelog lists the add-in's own +behaviour, not the documentation or the symbol index it carries. diff --git a/WIP.md b/WIP.md index ff49f7c8b..1f1ba134d 100644 --- a/WIP.md +++ b/WIP.md @@ -26,6 +26,7 @@ one covering what you are about to change. | retake the documentation's IDE screenshots, or change the tool that takes them | [WIP.Screenshots.md](WIP.Screenshots.md) --- `shoot_docs.mjs`: the setups, cut-outs, the annotation overlay and the owner's decisions | | work on the IDE help add-in, or on testing IDE add-ins by machine | [WIP.HelpAddin.md](WIP.HelpAddin.md) --- the plan, the IDE facts it rests on, and the probes still open | | find the documentation work a new twinBASIC build leaves, or change the package indexer (`indexer/`) | [WIP.Indexer.md](WIP.Indexer.md) --- the store and the snapshots, the one shared scanner, `api.json` format 3, the owner's decisions and the open items | +| add or check the run-time errors a procedure, statement or operator raises | [WIP.Errors.md](WIP.Errors.md) --- the owner's decisions, the data-driven design, and the batches with their status | | look up what a `TBnnnn` diagnostic code is, or document a compiler warning | [upstream/twinbasic-diagnostic-ids.h](upstream/twinbasic-diagnostic-ids.h) --- the compiler's full list of diagnostic codes and their internal names, from Wayne Phillips (current as of BETA 1007); `docs/Features/Compiler-IDE/Compiler-Warnings.md` is the public page for the warnings | | run a use-case evaluation round | [eval/README.md](eval/README.md) --- start every evaluator with `eval/run_case.mjs`, **never as a subagent**: a subagent inherits this session's `CLAUDE.md`, and with it this file, which is the answer key the corpus withholds | @@ -131,7 +132,7 @@ node scripts/tbbuild.mjs C:/probe/Thing.twinproj # does it compile node scripts/tbrun.mjs # what does it print ``` -- **Every IDE the tools start is given `--no-save-ide-settings` and `--no-update-recent-files`, and BETA 1007 is the oldest build any tool runs** (owner: "we never run anything before that"). The two flags are `IDE_FLAGS` in `scripts/lib/tb-ide.mjs`, passed by `launchIde` on both its branches; any new code that starts `twinBASIC.exe` passes them too. They keep the IDE from writing the user's options, panel layouts, recent list, remembered build targets and licence answers to the registry; what they leave is `ProjectState`, the `.twinproj` association, the `Window` key and `RemoveRecentsList`, which `scripts/lib/tb-registry.mjs` puts back or a test blocks in its page. **Never pass `--reset-ide-settings`** (it writes empty options to the real registry even after `--no-save-ide-settings`); an unknown `--` flag is ignored without a word; a second positional argument opens a modal box nobody can answer. An older install is passed over by `findIde`, and an older one named by `--ide` or `TB_IDE` is refused with exit 2 before an IDE starts (`MIN_BUILD` and `unsupportedIde` in `scripts/lib/tb-install.mjs`): there is no fallback and no switch to allow it, and the control for a probe is BETA 1007 itself. The reasons and the registry's details are in [WIP.Harness.md](WIP.Harness.md#what-a-run-leaves-in-the-registry-and-putting-it-back). +- **Every IDE the tools start is given `--no-save-ide-settings` and `--no-update-recent-files`, and BETA 1007 is the oldest build any tool runs** (owner: "we never run anything before that"). The two flags are `IDE_FLAGS` in `scripts/lib/tb-ide.mjs`, passed by `launchIde` on both its branches; any new code that starts `twinBASIC.exe` passes them too. They keep the IDE from writing the user's options, panel layouts, recent list, remembered build targets and licence answers to the registry; what they leave is `ProjectState`, the `.twinproj` association, the `Window` key and `RemoveRecentsList`, which `scripts/lib/tb-registry.mjs` puts back or a test blocks in its page. **Never pass `--reset-ide-settings`** (it writes empty options to the real registry even after `--no-save-ide-settings`); an unknown `--` flag is ignored without a word; a second positional argument opens a modal box nobody can answer. An older install is passed over by `findIde`, and an older one named by `--ide` or `TB_IDE` is refused with exit 2 before an IDE starts (`MIN_BUILD` and `unsupportedIde` in `scripts/lib/tb-install.mjs`): there is no fallback and no switch to allow it, and the control for a probe is BETA 1007 itself. **The flags stop the IDE writing its settings, and a hook stops it reading them**: `launchIde` answers every IDE's `GetIDESetting` with "no saved value" for its options, panel layouts and keyboard groups, in the page only (`scripts/lib/tb-defaults.mjs`; the answer for `GENERAL` and `GENERAL2` must be `"{}"`, never `""`, which the IDE reads as a first run and opens its Options dialog), so every tool's IDE runs with the defaults; `attachIde` fails the run when the page did not run the hook, and `userSettings: true` is the opt-out, passed by `try-help-addin` only. The reasons and the registry's details are in [WIP.Harness.md](WIP.Harness.md#what-a-run-leaves-in-the-registry-and-putting-it-back). - **Pack and unpack projects with `impexp`, never with the compiler executable's `import` or `export` verbs.** The tooling does both in process, through `packTree` and `unpackProject` in `scripts/lib/tb-project.mjs`; by hand it is `node scripts/impexp.mjs`. The compiler's `import` refuses a project that embeds a package (exit 999, twinbasic/twinbasic#841), and both verbs exit 0 after the failures they report. The reasons, and the path and stdin rules for anything that does run the executable (`bug_repro`'s `cli` mode), are in [WIP.Harness.md](WIP.Harness.md#getting-at-the-twin-sources). - **`tbbuild` exit codes:** 0 clean, 1 the project has errors, 2 the harness failed or the project file cannot be read (empty, cut short, not a project: it is read before an IDE starts), 3 the compile never settled or met its limit, 4 the project crashes the compiler, 5 the build failed after a clean compile (an LLVM refusal in its log included, an exe linked or not, and the build's limit). `--json` returns one object, `--keep` leaves the IDE running. **`--build` and `--llvm` (which implies it) build the project after a clean compile**, because LLVM code generation runs in a build and a compile never asks for it. They first stage a copy with an explicit build path (`%TEMP%\tbbuild\\`), so the given `.twinproj` is untouched. A plain `--build` is the control for an `--llvm` run. `--llvm` refuses a Community or Personal licence. - **It runs the IDE on a private Windows desktop**, so it cannot seize focus. Set `TBBUILD_SHOW=1` while working interactively and leave it unset for unattended runs: a wedged IDE nobody can see is expensive to diagnose. @@ -587,6 +588,7 @@ Why the report separates the wedged task from the merely blocked ones, and why - **The help archive is the `helpArchive` task of the build's graph.** `builder/help-archive-step.mjs` writes `add-in/Resources/HELP/site.zip` from `docs/_site-offline` through `lib/help-archive.mjs`, once every task that writes into the offline tree has finished (`flushJoin`, `scss`, `writeOffline`; the archive task lists exactly those) and **before** the Gantt injection. **The archive leaves out `HELP_ARCHIVE_SKIP`** (`builder/help-archive-step.mjs`): the Documentation Development section (`BuildInfo.html` and `assets/images/gantt.svg` with it, so the chart never reaches the zip), Videos and their poster frames, Challenges, and **every download, which stays online** (owner): a file readers download goes in a `downloads` folder beside its page, and the step fails the build when the Downloads page offers a file the archive would hold. The add-in's server redirects any file the archive lacks to the website, because pages link to all of these; a new entry needs nothing else. It reads the file back to verify it. A mismatch fails the build (exit 1). After the Gantt injection `recheckHelpArchive` checks that the offline tree on disk lists the same files as the archive and differs from it only in `BuildInfo.html` and `gantt.svg`; a task that wrote into the offline tree late fails the build there. The help add-in embeds the zip as a resource and serves the pages from it with its own server; a build of the add-in without it uses the live site. The file is gitignored (about 26 MB), so a build of the add-in from a clean clone is the online one until `build.bat` has run. The download carries the same zip (previous bullet). **`--no-help-archive` turns the step off, and neither CI workflow may pass it** (`check_ci_workflows.mjs` refuses it, in `ALLOWED.forbiddenBuildFlags`), because CI's published download would then hold no archive. It runs only for the guarded source tree (`docs`) built into the default `docs/_site-offline` (not another `--dest`, so `compare_trees` and `check_links_diff` leave it alone), never under `--serve` or `--dry-run`, never without an offline tree, and not keyed on `$CI`. There is no command-line tool for the archive: the `help-offline` lane makes its own with `writeHelpArchive`. The zip's format is fixed by the add-in's reader, which does no inflating ([WIP.HelpAddin.md](WIP.HelpAddin.md), Stage 4, increment 5). - `node scripts/bug_repro.mjs` --- the reproducer projects under `bugs//` for the entries of [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md): `new`, `pack` (impexp, then the zip, written in Node), `compile`, `build` and `run` through `tbbuild` and `tbrun`, `vb6` (builds the optional `vb6/` VB6 project beside the twinBASIC one in a temp copy, runs its exe on a private desktop, and prints its `out.txt`; a project with a form or a user control is built without Unattended Execution, which VB6 refuses for it; `pack` zips its sources into `-vb6.zip`; VB6 is started only by `scripts/lib/vb6.mjs`, never from a shell; a graphical defect's `images` key makes `run` and `vb6` keep what the `PngDump` modules of `test/repro-templates/png/` draw as `images/-tb.png` and `-vb6.png`, with a `-compare.png` of both, tested by `test/png.test.mjs`), `file` (moves a filed entry and its reproducer to `bugs/filed//`; `file --marked` does every marked entry), and `verify`, which reads each `bugs/*/repro.json` and `bugs/filed/*/repro.json` and says whether the bug still reproduces on the newest beta; a `lane` reproducer names the `ide-test` or `addin-test` lane and tests that assert its bug, which `verify` runs last through that runner, a `probe` one a script under `scripts/`, and a `test` one its own `[TestCase]` Subs and compile cases through `tbrun --tests`, judged by the `PASS` lines of what is fixed and the `FAIL` lines of the bug (`new --template tests` starts one). **No reproducer should need a person at the IDE** (owner): a bug the IDE has to be operated for gets a lane test, and `manual` is left for what cannot be automated at all. It has no wrapper. **`verify` is run by a person only**, never by a gate or CI: it needs a twinBASIC install, like `examples.bat`. Each `tbbuild` and `tbrun` it starts takes a slot of its own from `tbbroker`; `--jobs N` runs N at once. Its command-line cases are in `scripts/lib/cli-cases.mjs`, and need no IDE. - `node scripts/vb6run.mjs ` and `node scripts/vb6run.mjs --docs [--only ]` --- build and run VB6 code, so that what a sample prints in twinBASIC can be compared with VB6: a stand-alone file of statements or a `.bas` module, or `--docs`, which builds the documentation's `check_run` fences in VB6 and says for each whether it prints what its page says (`same`, `differs`, `not VB6`, `error`, `refused`; a `projname=` group is built as a project of its own, its `slot=file` fences translated into VB6 `.cls` and `.bas` components). It has no wrapper and is outside every gate and outside CI: it needs VB6 (`--vb6`, `VB6_EXE`, or the standard install folder). **Never start `VB6.EXE` from a shell**: in Git Bash `/make` is rewritten as a path and VB6 answers with a modal box on the desktop; the tool spawns it from Node with an argument array, and a sample that calls `MsgBox` or `InputBox` or contains `End` is refused. Its `Debug.Print` rewrite and its file translation have probes in `test/example-batches.test.mjs`, and its command-line cases are in `scripts/lib/cli-cases.mjs`; both need no VB6. What it relies on is in [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md). +- `node scripts/errors.mjs run|generate|write` --- the run-time errors of the documented procedures, as data under `errors/` ([WIP.Errors.md](WIP.Errors.md), [Tools-Defects](docs/Documentation/Tools-Defects.md#errorsmjs)): `run` builds and runs a probe of the cases in twinBASIC (`tbrun --exe`; win64 on request) and VB6 and compares each with the data, `write` writes each page's Errors section. Its gate is `check_errors.mjs`; `run` is by hand, never CI. - `node scripts/ide_js.mjs ` --- reads, names and patches the IDE's minified `ide/main.js` ([Tools-Defects](docs/Documentation/Tools-Defects.md#ide_jsmjs)): `survey`, `units`, `list`, `name`, `seal`, `check`, `view`, `patch` and `shape`. It reads files and starts nothing; the rules for it are under [Driving the twinBASIC compiler](#driving-the-twinbasic-compiler) (*Fixing the IDE itself*). It has no wrapper and is outside every gate and CI, since it needs an install; its tests are `test/ide-js.test.mjs`, in `test.bat`, and its command-line cases are in `scripts/lib/cli-cases.mjs`. **After a new BETA, run `check` on it**, and `seal` the map once each `review` and `lost` name has been looked at. Three generators sit outside that loop and produce committed artifacts rather than build output — none runs during a build, and none is needed for one. `python scripts/build_fonts.py` rebuilds the subset webfaces under `docs/assets/fonts/` and needs a network connection; `node scripts/build_dot_metrics.mjs` regenerates `builder/inter-metrics.json` from those webfaces and needs only a browser. See [Typography](#typography). `node indexer/twin-index.mjs derive` regenerates `builder/package-api.json`, the packages' declared API that the build's symbol index (`tB/symbols.json`, for the IDE help add-in) is annotated from; it needs a twinBASIC install (or `--release`), so **run it when the reference is re-indexed against a newer build** and commit it with the pages and the snapshots. See [WIP.HelpAddin.md, Stage 3](WIP.HelpAddin.md#stage-3-the-symbol-index-generated-by-the-docs-build). @@ -601,7 +603,7 @@ build.bat && check.bat On the dev box that is ~4 s of build against ~37 s of check, of which the axe scan is ~20 s. [builder/PLAN-checks.md](builder/PLAN-checks.md) records how the link checker sits in the build's task graph and its cost; the axe follow-ons are designed there but not implemented. -**If the change touched `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `indexer/`, `test/`, `add-in/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~23 s. Twenty-four of its twenty-eight gates cannot be affected by an edit under `docs/` at all. **Four can.** `check_package_tests.mjs` reads the callouts of the contributed packages' sections. `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `check_gate_lists.mjs` reads `README.md` and every page under `docs/Documentation/`, so an edit to any developer page that states a gate count can fail it. **`check_code_regions.mjs` has two halves, and a content edit reaches only one.** Its corpus sweep reads `DOCS_DIR`, which is `/docs`, and tokenises every markdown file in it, so a page that provokes a rewrite into *altering* a code region fails it --- that half is content-dependent. Its fixed probes are not: they run against their own sources whatever the tree holds, and they cover the **mirror** fault, where a rewrite silently stops firing. The sweep cannot see that fault, because text the rewrite skipped is masked and restored unchanged and every region still matches. So run `test.bat` after adding an unusual code construct --- a fence whose contents include a fence marker, a 4-space indented block, an admonition wrapping a fence --- and read the built page as well, because for the mirror fault the gate is asserting that the mask and the admonition scan still work rather than checking your page: +**If the change touched `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `indexer/`, `test/`, `add-in/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~23 s. Twenty-four of its twenty-nine gates cannot be affected by an edit under `docs/` at all. **Five can.** `check_errors.mjs` compares the reference pages' Common Errors sections with the data under `errors/`, so edit the data and run `node scripts/errors.mjs write`, never the section. `check_package_tests.mjs` reads the callouts of the contributed packages' sections. `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `check_gate_lists.mjs` reads `README.md` and every page under `docs/Documentation/`, so an edit to any developer page that states a gate count can fail it. **`check_code_regions.mjs` has two halves, and a content edit reaches only one.** Its corpus sweep reads `DOCS_DIR`, which is `/docs`, and tokenises every markdown file in it, so a page that provokes a rewrite into *altering* a code region fails it --- that half is content-dependent. Its fixed probes are not: they run against their own sources whatever the tree holds, and they cover the **mirror** fault, where a rewrite silently stops firing. The sweep cannot see that fault, because text the rewrite skipped is masked and restored unchanged and every region still matches. So run `test.bat` after adding an unusual code construct --- a fence whose contents include a fence marker, a 4-space indented block, an admonition wrapping a fence --- and read the built page as well, because for the mirror fault the gate is asserting that the mask and the admonition scan still work rather than checking your page: ```sh build.bat && check.bat && test.bat diff --git a/add-in/CHANGELOG.md b/add-in/CHANGELOG.md new file mode 100644 index 000000000..a296174aa --- /dev/null +++ b/add-in/CHANGELOG.md @@ -0,0 +1,78 @@ +# Changelog + +What changed in the twinBASIC Documentation Help add-in, newest first. The version is the +DLL's (its file properties show it). The documentation the add-in shows and its index of +symbols change with every build of the documentation and are not listed here. + +## 1.1.0 (2026-10-21) + +- Ctrl+F finds text in the editors of **Window > Keyboard Shortcuts > Manage Keyboard + Shortcuts** and of **Manage Panel Layouts**. Enter or F3 goes to the next match, + Shift+Enter or Shift+F3 to the one before (F3 also with the find box closed), Ctrl+F3 and + Ctrl+Shift+F3 search for the selection or the word at the caret without opening the box, + and Esc closes the find box and leaves the dialog open. The text scrolls to each match. +- The settings panel has two new boxes, both ticked by default: **Ctrl and Alt shortcuts of + add-ins** (the 1.0.0 fix) and **Find in the shortcut editors** (the Ctrl+F above). Unticking + one turns that feature off at once. + +## 1.0.0 (2026-10-20) + +- Other add-ins' keyboard shortcuts that include Ctrl or Alt now work while this add-in is + loaded. The IDE on its own does not run them. +- The help window's text follows the IDE's font size (**Tools > IDE Options**, *IDE Font + Size*), as the IDE's own panels do, also when the size is changed while the window is open. + +## Before version numbers + +### 2026-10-18 + +- F1 on a data type in a declaration, such as `As String`, opens the data type; on + `String(` it opens the **String** function. + +### 2026-10-16 + +- The help pane, the first time it floats, fits a small IDE window: at most 480 by 640 + pixels, and never more than 85% of the IDE's width or height. + +### 2026-10-15 + +- Offline help: a page or file that the add-in's own copy of the documentation leaves out + (videos, downloads, the documentation's development pages) opens from the website. + +### 2026-10-09 + +- A line between the search results and the page, in the pane and in the help window. + +### 2026-10-07 + +- **Detach**: the help pane moves into a window of its own, which can go to another monitor. + **Attach** brings it back; closing the window hides it until the next F1. With the IDE's + **Show real OS titlebar** option, the window has an ordinary Windows title bar. +- A settings panel, opened with the gear: **Hover help** and **All packages**. +- The add-in offers only the pages of the packages the project references, unless **All + packages** is ticked. +- Hover help shows the links to a name's pages as the first line of the IDE's own mouse + hover, such as *Help: MsgBox function (VBA.Interaction)*. + +### 2026-10-06 + +- **Hover help**: links to the pages of the name under the mouse, in the IDE's hover. + Off by default. +- The search results list is as tall as its rows, up to about a third of the pane, and + clearing the search box hides it. + +### 2026-10-05 + +- Offline help: when the add-in is built with the documentation's archive, it shows the + pages from that copy, with no internet connection. + +### 2026-10-04 + +- First release. F1 opens the documentation page for the name under the cursor, in the + **TWINBASIC HELP** tool window, with a search box over the documentation's index. +- F1 asks the compiler where a name is declared, so it tells apart names spelled the same + (`c.Add` on a **Collection** opens **Collection.Add**). +- F1 on a name with no page, such as your own procedure, shows its declaration and its + `[Description]`. +- The pages follow the IDE's dark or light theme, and show without the website's + navigation. diff --git a/add-in/Resources/SCRIPTS/addinkeys.js b/add-in/Resources/SCRIPTS/addinkeys.js new file mode 100644 index 000000000..c22ce77f1 --- /dev/null +++ b/add-in/Resources/SCRIPTS/addinkeys.js @@ -0,0 +1,59 @@ +// Makes add-in shortcuts with {ctrl} or {alt} fire, for every add-in. The +// IDE's key-down handler (globalKeyDown) records a press for its key-up +// dispatcher only when neither Ctrl nor Alt is held, so such a shortcut never +// fires, or fires as the plain key when that was pressed less than 500 ms +// before. This records those presses itself and runs their shortcuts on +// key-up as the IDE runs the others. It is put in only while the IDE has that +// guard, so a build that fixes it is left alone. Running it again replaces +// what an earlier run put in, and puts in nothing when it is turned off. +var on = $ON$; +var old = window.tbDocsAddinKeys; +if (old) { + window.removeEventListener("keydown", old.down, true); + window.removeEventListener("keyup", old.up, true); + window.tbDocsAddinKeys = null; +} +var faulty = + on && + typeof globalKeyDown === "function" && + typeof addinKeys === "object" && + typeof realKeyPresses === "object" && + /\(!\w+\.ctrlKey\|\|\w+\.key==="Control"\)&&\(!\w+\.altKey\|\|\w+\.key==="Alt"\)/.test(String(globalKeyDown)); +if (faulty) { + var presses = {}; + // The key's name as the IDE's key-up handler gives it. + var keyName = function (e) { + return e.code.startsWith("Key") ? e.code.substr(3) : e.key; + }; + var down = function (e) { + if (e.key === "Control" || e.key === "Alt" || !(e.ctrlKey || e.altKey)) return; + // AltGr types a character, as a plain key does. + if (e.getModifierState && e.getModifierState("AltGraph")) return; + presses[keyName(e)] = { at: performance.now(), ctrl: e.ctrlKey, shift: e.shiftKey, alt: e.altKey }; + }; + var up = function (e) { + var o = keyName(e); + var p = presses[o]; + if (!p) return; + delete presses[o]; + // The IDE's record of this key is from an earlier press without Ctrl or + // Alt; left, it would run this key-up as that key. + delete realKeyPresses[o]; + if (modalDialogs.length || isRenameWidgetActive) return; + if (performance.now() - p.at >= 500) return; + var shortcut = (p.ctrl ? "{ctrl}" : "") + (p.shift ? "{shift}" : "") + (p.alt ? "{alt}" : "") + o.toLowerCase(); + var handlers = addinKeys[shortcut]; + if (handlers) { + handlers.forEach(function (h) { + try { + h(); + } catch (err) { + console.error(err); + } + }); + } + }; + window.addEventListener("keydown", down, true); + window.addEventListener("keyup", up, true); + window.tbDocsAddinKeys = { down: down, up: up }; +} diff --git a/add-in/Resources/SCRIPTS/fontsize.js b/add-in/Resources/SCRIPTS/fontsize.js new file mode 100644 index 000000000..1fae05920 --- /dev/null +++ b/add-in/Resources/SCRIPTS/fontsize.js @@ -0,0 +1,26 @@ +// Tells the add-in the IDE's font size, now and whenever the IDE changes it, +// so that the detached window can be set in the same size. The IDE keeps it +// in --ide-fontsize on the page's root, and sets its text from that. Running +// this again replaces the observer an earlier run put in. +var sink = this.parentNode; +var old = window.tbDocsFontSize; +if (old) { + old.observer.disconnect(); + window.tbDocsFontSize = null; +} +var last = null; +var send = function () { + var size = getComputedStyle(document.documentElement).getPropertyValue("--ide-fontsize").trim(); + if (!size) size = getComputedStyle(document.body).fontSize; + if (size === last) return; + last = size; + try { + sink.tbDocsFontSize({ value: size }); + } catch (err) { + console.error(err); + } +}; +var observer = new MutationObserver(send); +observer.observe(document.documentElement, { attributes: true, attributeFilter: ["style"] }); +window.tbDocsFontSize = { observer: observer }; +send(); diff --git a/add-in/Resources/SCRIPTS/textfind.js b/add-in/Resources/SCRIPTS/textfind.js new file mode 100644 index 000000000..6c1920c4a --- /dev/null +++ b/add-in/Resources/SCRIPTS/textfind.js @@ -0,0 +1,228 @@ +// Find in the text boxes of the IDE's configuration editors (Window > Keyboard +// Shortcuts > Manage Keyboard Shortcuts, and Manage Panel Layouts), which are +// plain text areas with no find of their own, inside a dialog where the IDE's +// key handling does nothing. The keys are those of Visual Studio and VS Code: +// Ctrl+F opens a find box over the text area; +// Enter, F3 select the next match of the query, Shift+Enter, Shift+F3 +// the one before, from the selection and round past the end; +// F3, Shift+F3 also work with the box closed, on the last query, and with +// no query yet F3 opens the box; +// Ctrl+F3, Ctrl+Shift+F3 +// take the selection (when on one line), or else the word at +// the caret, as the query, and find the next or previous +// match without opening the box; +// Esc closes the box only: the dialog stays open. +// A match is found in any case. Running this again replaces what an earlier run +// put in, and puts in nothing when it is turned off. +var on = $ON$; +var old = window.tbDocsTextFind; +if (old) { + window.removeEventListener("keydown", old.down, true); + if (old.bar) old.bar.remove(); + window.tbDocsTextFind = null; +} +if (on) { +var EDITOR = "textarea.genericMonospaceTextArea"; +var state = { bar: null, input: null, area: null, query: "" }; + +var barOpen = function () { + return state.bar !== null && state.bar.isConnected; +}; + +var close = function (focusArea) { + if (state.bar) state.bar.remove(); + var area = state.area; + state.bar = state.input = state.area = null; + if (focusArea && area && area.isConnected) area.focus(); +}; + +// Where the text from start to end is, in the text area's scrolling space (the +// origin is its padding box's top left, before any scrolling): measured in an +// off-screen copy of the text area's text, set the same way and as wide, with +// the match in a span. A match that wraps has the first line's top and left and +// the last line's bottom. +var matchRect = function (area, start, end) { + var cs = getComputedStyle(area); + var mirror = document.createElement("div"); + var props = [ + "fontFamily", "fontSize", "fontWeight", "fontStyle", "fontVariant", "letterSpacing", + "lineHeight", "textTransform", "textIndent", "wordSpacing", "tabSize", + "paddingTop", "paddingRight", "paddingBottom", "paddingLeft", + "whiteSpace", "overflowWrap", "wordBreak", "direction" + ]; + for (var i = 0; i < props.length; i++) mirror.style[props[i]] = cs[props[i]]; + mirror.style.cssText += + ";position:absolute;visibility:hidden;left:-99999px;top:0;box-sizing:border-box;border:0;margin:0;" + + "overflow:visible;width:" + area.clientWidth + "px"; + var span = document.createElement("span"); + span.textContent = area.value.substring(start, end) || "​"; + mirror.appendChild(document.createTextNode(area.value.substring(0, start))); + mirror.appendChild(span); + mirror.appendChild(document.createTextNode(area.value.substring(end) + "​")); + document.body.appendChild(mirror); + var origin = mirror.getBoundingClientRect(); + var rects = span.getClientRects(); + var first = rects[0]; + var last = rects[rects.length - 1]; + var right = -Infinity; + for (var j = 0; j < rects.length; j++) right = Math.max(right, rects[j].right); + var r = { + top: first.top - origin.top, + bottom: last.bottom - origin.top, + left: first.left - origin.left, + right: right - origin.left, + wraps: cs.whiteSpace.indexOf("pre-wrap") >= 0 || cs.whiteSpace === "normal" || cs.whiteSpace === "pre-line" + }; + mirror.remove(); + return r; +}; + +// Whether the selection is in view, and not under the find box, with the rect +// matchRect gives for it. +var visibleState = function (area) { + var r = matchRect(area, area.selectionStart, area.selectionEnd); + var box = area.getBoundingClientRect(); + var left = box.left + area.clientLeft + r.left - area.scrollLeft; + var right = box.left + area.clientLeft + r.right - area.scrollLeft; + var top = box.top + area.clientTop + r.top - area.scrollTop; + var bottom = box.top + area.clientTop + r.bottom - area.scrollTop; + var inView = + r.top >= area.scrollTop && + r.bottom <= area.scrollTop + area.clientHeight && + (r.wraps || (r.left >= area.scrollLeft && r.right <= area.scrollLeft + area.clientWidth)); + var covered = false; + if (barOpen()) { + var b = state.bar.getBoundingClientRect(); + covered = left < b.right && right > b.left && top < b.bottom && bottom > b.top; + } + return { visible: inView && !covered, covered: covered, rect: r, cover: barOpen() ? state.bar.offsetHeight + 8 : 0 }; +}; + +// Selects the match and scrolls so that it shows, about a third down the text +// area and clear of the find box. +var select = function (area, at, length) { + area.focus(); + area.setSelectionRange(at, at + length); + area.blur(); + area.focus(); + area.setSelectionRange(at, at + length); + var v = visibleState(area); + if (v.visible) return; + var r = v.rect; + area.scrollTop = Math.max(0, r.top - Math.max(v.cover, area.clientHeight / 3)); + if (!r.wraps) area.scrollLeft = Math.max(0, r.left - area.clientWidth / 3); +}; + +var find = function (area, backwards) { + if (barOpen()) state.query = state.input.value; + var query = state.query.toLowerCase(); + if (!area || !area.isConnected || !query) return; + var text = area.value.toLowerCase(); + var at; + if (backwards) { + at = area.selectionStart > 0 ? text.lastIndexOf(query, area.selectionStart - 1) : -1; + if (at < 0) at = text.lastIndexOf(query); + } else { + at = text.indexOf(query, area.selectionEnd); + if (at < 0) at = text.indexOf(query); + } + if (barOpen()) { + state.input.classList.toggle("tbDocsFindMissing", at < 0); + state.input.title = at < 0 ? "No match" : ""; + } + if (at >= 0) select(area, at, query.length); +}; + +var open = function (area) { + if (state.area !== area || !barOpen()) { + close(false); + var host = area.parentNode; + if (getComputedStyle(host).position === "static") host.style.position = "relative"; + var bar = document.createElement("div"); + bar.className = "tbDocsFindBar"; + bar.style.cssText = + "position:absolute;top:4px;right:20px;z-index:10;display:flex;gap:4px;padding:3px;" + + "background:var(--themeGeneralPanelBackColor);border:var(--themePropertySheetValueBorderColor);" + + "box-shadow:0 2px 6px rgba(0,0,0,0.35)"; + var input = document.createElement("input"); + input.type = "text"; + input.placeholder = "Find"; + input.setAttribute("aria-label", "Find"); + input.value = state.query; + input.style.cssText = + "width:180px;font:inherit;color:var(--themeToolWindowBodyForeColor);" + + "background:var(--themeGeneralPanelBackColor);border:var(--themePropertySheetValueBorderColor);outline:none"; + bar.appendChild(input); + host.appendChild(bar); + state.bar = bar; + state.input = input; + state.area = area; + window.tbDocsTextFind.bar = bar; + } + var picked = area.value.substring(area.selectionStart, area.selectionEnd); + if (picked && picked.indexOf("\n") < 0) state.input.value = picked; + state.input.focus(); + state.input.select(); +}; + +// The query for Ctrl+F3: the selection when it is on one line, else the word at +// the caret (letters, digits and _), which is then selected, so that a search +// backwards goes before it. +var takeQuery = function (area) { + var picked = area.value.substring(area.selectionStart, area.selectionEnd); + if (picked && picked.indexOf("\n") < 0) return picked; + var text = area.value; + var start = area.selectionStart; + var end = start; + var isWord = function (c) { + return /[A-Za-z0-9_]/.test(c); + }; + while (start > 0 && isWord(text.charAt(start - 1))) start--; + while (end < text.length && isWord(text.charAt(end))) end++; + if (start === end) return ""; + area.setSelectionRange(start, end); + return text.substring(start, end); +}; + +var down = function (e) { + var target = e.target; + var inArea = target instanceof HTMLTextAreaElement && target.matches(EDITOR); + var inBox = state.input !== null && target === state.input; + if (!inArea && !inBox) return; + var area = inArea ? target : state.area; + var handled = true; + if (e.ctrlKey && !e.altKey && e.code === "KeyF") { + open(area); + } else if (e.key === "F3" && e.ctrlKey && !e.altKey) { + var word = takeQuery(area); + if (word) { + state.query = word; + if (barOpen()) state.input.value = word; + find(area, e.shiftKey); + } + } else if (e.key === "F3" && !e.altKey) { + if (barOpen() || state.query) find(area, e.shiftKey); + else open(area); + } else if (inBox && e.key === "Enter") { + find(area, e.shiftKey); + } else if (barOpen() && e.key === "Escape") { + close(true); + } else { + handled = false; + } + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } +}; + +window.addEventListener("keydown", down, true); +window.tbDocsTextFind = { down: down, bar: null, visible: visibleState }; +var style = document.getElementById("tbDocsFindStyle"); +if (!style) { + style = document.createElement("style"); + style.id = "tbDocsFindStyle"; + style.textContent = ".tbDocsFindBar input.tbDocsFindMissing{border-color:#c03030 !important}"; + document.head.appendChild(style); +} +} diff --git a/add-in/Resources/SHELL/shell.css b/add-in/Resources/SHELL/shell.css index ef183f022..ae4d94846 100644 --- a/add-in/Resources/SHELL/shell.css +++ b/add-in/Resources/SHELL/shell.css @@ -37,7 +37,7 @@ body { body { overflow: hidden; - font: 13px "Segoe UI", system-ui, sans-serif; + font: var(--ideFontSize, 13px) "Segoe UI", system-ui, sans-serif; } #helpResults { @@ -142,7 +142,7 @@ html[data-chrome="native"] #helpTitleBar { #helpTitleBar #helpSettingsPanel { color: var(--themeToolWindowBodyForeColor); - font: 13px "Segoe UI", system-ui, sans-serif; + font: var(--ideFontSize, 13px) "Segoe UI", system-ui, sans-serif; } /* The Maximize button shows Restore while the window is maximized. */ diff --git a/add-in/Resources/SHELL/shell.html b/add-in/Resources/SHELL/shell.html index c0e9eda8c..4f704859b 100644 --- a/add-in/Resources/SHELL/shell.html +++ b/add-in/Resources/SHELL/shell.html @@ -18,6 +18,8 @@ diff --git a/add-in/Resources/SHELL/shell.js b/add-in/Resources/SHELL/shell.js index fb626c1c3..9523d7662 100644 --- a/add-in/Resources/SHELL/shell.js +++ b/add-in/Resources/SHELL/shell.js @@ -3,12 +3,12 @@ // way is "verb:payload", split at the first colon. // // Page to add-in: ready, search:, enter, pick:, hover:<1|0>, -// allpackages:<1|0>, attach, browser, frame: (the frame has loaded src), +// allpackages:<1|0>, addinkeys:<1|0>, textfind:<1|0>, attach, browser, frame: (the frame has loaded src), // and from the title bar drag (start moving the window), minimize, maxrestore, // close. // Add-in to page: theme: a line break and , search:, // results:, page:, summary:, hover:<1|0>, hoverdisabled:<1|0>, -// allpackages:<1|0>, browser:<1|0 enabled>, focus:, chrome: +// allpackages:<1|0>, addinkeys:<1|0>, textfind:<1|0>, browser:<1|0 enabled>, focus:, chrome: // (whether the page draws the title bar), state: (the window's). (function () { var host = window.chrome.webview; @@ -21,6 +21,8 @@ var browser = byId("helpBrowser"); var hover = byId("helpHover"); var allPackages = byId("helpAllPackages"); + var keysOn = byId("helpKeysOn"); + var findOn = byId("helpFindOn"); var settings = byId("helpSettings"); var settingsWrap = byId("helpSettingsWrap"); var settingsPanel = byId("helpSettingsPanel"); @@ -82,6 +84,8 @@ allPackages.addEventListener("change", function () { post("allpackages:" + (allPackages.checked ? "1" : "0")); }); + keysOn.addEventListener("change", function () { post("addinkeys:" + (keysOn.checked ? "1" : "0")); }); + findOn.addEventListener("change", function () { post("textfind:" + (findOn.checked ? "1" : "0")); }); // The settings panel opens under the gear, with the shade over the rest of // the window so that a click anywhere else closes it. A press over the @@ -172,6 +176,8 @@ case "hover": hover.checked = payload === "1"; break; case "hoverdisabled": hover.disabled = payload === "1"; break; case "allpackages": allPackages.checked = payload === "1"; break; + case "addinkeys": keysOn.checked = payload === "1"; break; + case "textfind": findOn.checked = payload === "1"; break; case "browser": browser.disabled = payload !== "1"; break; case "focus": search.focus(); break; case "chrome": setChrome(payload); break; diff --git a/add-in/Resources/STYLESHEETS/pane.css b/add-in/Resources/STYLESHEETS/pane.css index 6d6291180..caee3c02a 100644 --- a/add-in/Resources/STYLESHEETS/pane.css +++ b/add-in/Resources/STYLESHEETS/pane.css @@ -63,7 +63,9 @@ } #helpHoverLabel, -#helpAllPackagesLabel { +#helpAllPackagesLabel, +#helpKeysLabel, +#helpFindLabel { display: flex; align-items: center; gap: 4px; diff --git a/add-in/Settings b/add-in/Settings index 934e873a4..b46e86408 100644 --- a/add-in/Settings +++ b/add-in/Settings @@ -65,6 +65,11 @@ } ], "project.settingsVersion": 1, + "project.versionAutoIncrement": "None", + "project.versionBuild": 0, + "project.versionMajor": 1, + "project.versionMinor": 1, + "project.versionRevision": 0, "project.warnings": { "errors": [], "ignored": [] diff --git a/add-in/Sources/HelpAddIn.twin b/add-in/Sources/HelpAddIn.twin index 45e68da95..01474bb7d 100644 --- a/add-in/Sources/HelpAddIn.twin +++ b/add-in/Sources/HelpAddIn.twin @@ -42,12 +42,17 @@ Private Class HelpAddIn Private LastSearch As String Private PageUrl As String ' the site path the frame was given Private HoverSink As HtmlElement ' where the compiler's answers come + Private IdeFontSize As String ' the IDE's font size, "" until fontsize.js tells it Private AskSeq As Long ' the number of the last question Private Asked As String ' its word, "" once answered Private Server As HelpServer ' Nothing unless the add-in has the archive Private WithEvents Hovering As HoverHelp ' Nothing without an index Private HoverBox As HtmlElement ' the pane's Hover help checkbox Private AllBox As HtmlElement ' the pane's All packages checkbox + Private KeysBox As HtmlElement ' the pane's Ctrl or Alt shortcuts checkbox + Private FindBox As HtmlElement ' the pane's Find in the shortcut editors checkbox + Private KeysHost As HtmlElement ' where addinkeys.js is put in + Private FindHost As HtmlElement ' where textfind.js is put in Private Referenced As Collection ' the project's packages, keyed by name; Nothing if unknown Private DetachButton As HtmlElement Private SettingsButton As HtmlElement ' the gear at the bar's right end @@ -63,6 +68,8 @@ Private Class HelpAddIn Private SearchText As String ' what the search box holds, in either view Private HoverOn As Boolean ' what the Hover help box holds, in either view Private AllPackages As Boolean ' what the All packages box holds, in either view + Private KeysOn As Boolean ' what the Ctrl or Alt shortcuts box holds, in either view + Private FindOn As Boolean ' what the Find in the shortcut editors box holds, in either view Private SummaryHtml As String ' what ShowSummary shows Private ShowingSummary As Boolean ' the summary shows in place of the page @@ -82,6 +89,8 @@ Private Class HelpAddIn ' The window's settings: whether it is detached, whether it is shown, and ' where it is, in pixels. Private Const WINDOW_SECTION As String = "Window" + ' The settings of the two page-side fixes: addinkeys.js and textfind.js. + Private Const FIXES_SECTION As String = "Fixes" Public Sub New(ByVal Host As Host) Set Me.Host = Host @@ -266,7 +275,9 @@ Private Class HelpAddIn ' What the window is told of the theme: the group, for the plain palette it ' falls back on, a line break, and the rule with the theme's properties. Private Function ThemeMessage() As String - Return Host.Themes.ActiveThemeNameGroup & vbLf & ThemeRule(Host.Themes.ActiveThemeName) + Dim rule As String = ThemeRule(Host.Themes.ActiveThemeName) + If Len(IdeFontSize) > 0 Then rule = ":root{--ideFontSize:" & IdeFontSize & "}" & rule + Return Host.Themes.ActiveThemeNameGroup & vbLf & rule End Function ' The pane's elements. Showing a tool window sets its root's display to @@ -275,6 +286,8 @@ Private Class HelpAddIn ' Kept apart from the box: a property read straight after it is set ' can still give the old value. HoverOn = GetSetting(SETTINGS_APP, "Hover", "On", "0") = "1" + KeysOn = GetSetting(SETTINGS_APP, FIXES_SECTION, "AddinKeys", "1") = "1" + FindOn = GetSetting(SETTINGS_APP, FIXES_SECTION, "TextFind", "1") = "1" Set Pane = Host.ToolWindows.Add("tbDocsHelp", PANE_ID) Pane.Title = "TWINBASIC HELP" Pane.ApplyCss StrConv(LoadResDataInternal("pane.css", "STYLESHEETS"), VbStrConv.vbFromUTF8) @@ -327,6 +340,20 @@ Private Class HelpAddIn AllBox.AddEventListener("change", AddressOf AllPackagesToggled) .ChildDomElements.Add("helpAllPackagesText", "span").Properties.innerText = "All packages" End With + With SettingsPanel.ChildDomElements.Add("helpKeysLabel", "label") + Set KeysBox = .ChildDomElements.Add("helpKeysOn", "input") + KeysBox.Properties.type = "checkbox" + KeysBox.Properties.checked = KeysOn + KeysBox.AddEventListener("change", AddressOf KeysToggled) + .ChildDomElements.Add("helpKeysText", "span").Properties.innerText = "Ctrl and Alt shortcuts of add-ins" + End With + With SettingsPanel.ChildDomElements.Add("helpFindLabel", "label") + Set FindBox = .ChildDomElements.Add("helpFindOn", "input") + FindBox.Properties.type = "checkbox" + FindBox.Properties.checked = FindOn + FindBox.AddEventListener("change", AddressOf FindToggled) + .ChildDomElements.Add("helpFindText", "span").Properties.innerText = "Find in the shortcut editors" + End With End With End With Set SettingsShade = .ChildDomElements.Add("helpSettingsShade", "div") @@ -350,6 +377,22 @@ Private Class HelpAddIn Hovering.Attach mouseSink Hovering.Enabled = HoverOn End If + ' The IDE's font size, for the window (fontsize.js). + With .ChildDomElements.Add("helpFont", "div") + .Properties.style.display = "none" + .AddEventListener("tbDocsFontSize", AddressOf FontSizeChanged) + .Properties.innerHTML = PageScript(StrConv(LoadResDataInternal("fontsize.js", "SCRIPTS"), VbStrConv.vbFromUTF8)) + End With + ' Makes every add-in's {ctrl} and {alt} shortcuts fire, which + ' the IDE's own key handling does not (addinkeys.js). + Set KeysHost = .ChildDomElements.Add("helpKeys", "div") + KeysHost.Properties.style.display = "none" + ApplyFix KeysHost, "addinkeys.js", KeysOn + ' Ctrl+F in the text of the Manage Keyboard Shortcuts and + ' Manage Panel Layouts dialogs (textfind.js). + Set FindHost = .ChildDomElements.Add("helpTextFind", "div") + FindHost.Properties.style.display = "none" + ApplyFix FindHost, "textfind.js", FindOn End With End With Set ResultIds = New Collection @@ -383,6 +426,49 @@ Private Class HelpAddIn If Detached Then PostToWindow "hover:" & IIf(isOn, "1", "0") End Sub + ' Runs a page-side fix's script in the page: it puts the fix in when isOn, + ' and takes out what an earlier run or instance put in. + Private Sub ApplyFix(ByVal Target As HtmlElement, ByVal script As String, ByVal isOn As Boolean) + Dim js As String = StrConv(LoadResDataInternal(script, "SCRIPTS"), VbStrConv.vbFromUTF8) + Target.Properties.innerHTML = PageScript(Replace(js, "$ON$", IIf(isOn, "true", "false"))) + End Sub + + Private Sub KeysToggled(ByVal eventInfo As HtmlEventProperties) + On Error GoTo Failed + SetKeys KeysBox.Properties.checked + Exit Sub + Failed: + Host.DebugConsole.PrintText TAG & "error " & Err.Number & ": " & Err.Description + End Sub + + ' Turns the Ctrl and Alt shortcuts of add-ins on or off, from the pane's box + ' or the window's, and shows it in both. + Private Sub SetKeys(ByVal isOn As Boolean) + KeysOn = isOn + SaveSetting SETTINGS_APP, FIXES_SECTION, "AddinKeys", IIf(isOn, "1", "0") + ApplyFix KeysHost, "addinkeys.js", isOn + KeysBox.Properties.checked = isOn + If Detached Then PostToWindow "addinkeys:" & IIf(isOn, "1", "0") + End Sub + + Private Sub FindToggled(ByVal eventInfo As HtmlEventProperties) + On Error GoTo Failed + SetFind FindBox.Properties.checked + Exit Sub + Failed: + Host.DebugConsole.PrintText TAG & "error " & Err.Number & ": " & Err.Description + End Sub + + ' Turns Ctrl+F in the shortcut editors on or off, from the pane's box or + ' the window's, and shows it in both. + Private Sub SetFind(ByVal isOn As Boolean) + FindOn = isOn + SaveSetting SETTINGS_APP, FIXES_SECTION, "TextFind", IIf(isOn, "1", "0") + ApplyFix FindHost, "textfind.js", isOn + FindBox.Properties.checked = isOn + If Detached Then PostToWindow "textfind:" & IIf(isOn, "1", "0") + End Sub + ' Only the packages the project references are offered until the box is ' ticked, and it stays as it was left. Private Sub AllPackagesToggled(ByVal eventInfo As HtmlEventProperties) @@ -516,6 +602,19 @@ Private Class HelpAddIn Host.DebugConsole.PrintText TAG & "error " & Err.Number & ": " & Err.Description End Sub + ' The window is set in the IDE's font size, as it is when it opens and when + ' the size is changed while the window is open. + Private Sub FontSizeChanged(ByVal eventInfo As HtmlEventProperties) + On Error GoTo Failed + Dim size As String = eventInfo.value + If size = IdeFontSize Then Exit Sub + IdeFontSize = size + If Detached Then PostToWindow "theme:" & ThemeMessage() + Exit Sub + Failed: + Host.DebugConsole.PrintText TAG & "error " & Err.Number & ": " & Err.Description + End Sub + Private Sub HoverAnswered(ByVal eventInfo As HtmlEventProperties) On Error GoTo Failed If CLng(eventInfo.seq) <> AskSeq Or Len(Asked) = 0 Then Exit Sub @@ -882,6 +981,10 @@ Private Class HelpAddIn SetHover payload = "1" Case "allpackages" SetAllPackages payload = "1" + Case "addinkeys" + SetKeys payload = "1" + Case "textfind" + SetFind payload = "1" Case "attach" Attach Case "close" @@ -917,6 +1020,8 @@ Private Class HelpAddIn HelpWin.Post "hoverdisabled:" & IIf(Hovering Is Nothing, "1", "0") HelpWin.Post "hover:" & IIf(HoverOn, "1", "0") HelpWin.Post "allpackages:" & IIf(AllPackages, "1", "0") + HelpWin.Post "addinkeys:" & IIf(KeysOn, "1", "0") + HelpWin.Post "textfind:" & IIf(FindOn, "1", "0") HelpWin.Post "search:" & SearchText PostResults If ShowingSummary Then diff --git a/bugs/database-compare-no-error/database-compare-no-error.twinproj b/bugs/database-compare-no-error/database-compare-no-error.twinproj new file mode 100644 index 000000000..40db1ac83 Binary files /dev/null and b/bugs/database-compare-no-error/database-compare-no-error.twinproj differ diff --git a/bugs/database-compare-no-error/repro.json b/bugs/database-compare-no-error/repro.json new file mode 100644 index 000000000..3f202edf5 --- /dev/null +++ b/bugs/database-compare-no-error/repro.json @@ -0,0 +1,17 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.InStr_BinaryCompare_Finds$", + "^PASS RegressionTests.InStr_TextCompare_Finds$", + "^PASS RegressionTests.InStr_CompareThree_NoError$", + "^PASS RegressionTests.InStrB_DatabaseCompare_NoError$", + "^PASS RegressionTests.InStrRev_DatabaseCompare_NoError$", + "^FAIL RegressionTests.StrComp_DatabaseCompare_Raises5: .*Actual: 0 ", + "^FAIL RegressionTests.Filter_DatabaseCompare_Raises5: .*Actual: 0 ", + "^FAIL RegressionTests.InStr_DatabaseCompare_Raises5: .*Actual: 0 ", + "^FAIL RegressionTests.InStr_DatabaseCompare_ErrorDescription: .*Actual: vbNullString" + ] + } +} diff --git a/bugs/database-compare-no-error/src/Settings b/bugs/database-compare-no-error/src/Settings new file mode 100644 index 000000000..cc4739402 --- /dev/null +++ b/bugs/database-compare-no-error/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "DatabaseCompareNoError", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: A *compare* of 2 (`vbDatabaseCompare`) raises no error in `InStr`, `StrComp` and `Filter`, where VB6 raises error 5", + "project.exportPathIsV2": true, + "project.id": "{75DF680A-3923-4340-A340-34DABB960D4E}", + "project.name": "DatabaseCompareNoError", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/database-compare-no-error/src/Sources/RegressionTests.twin b/bugs/database-compare-no-error/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..a536d7b4d --- /dev/null +++ b/bugs/database-compare-no-error/src/Sources/RegressionTests.twin @@ -0,0 +1,79 @@ +' Regression tests: InStr, StrComp and Filter with a compare of 2 (vbDatabaseCompare) raise error 5, +' "Invalid procedure call or argument", outside Microsoft Access. Expected results are +' VB6's (the VB6 project beside this one prints them). tbrun --tests runs each case and +' prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: the two compare settings InStr accepts, and the neighbours VB6 lets + ' through with a compare of 2 or 3. + [TestCase] + Public Sub InStr_BinaryCompare_Finds() + Assert.Strict.AreEqual CVar(1&), InStr(1, "abc", "a", vbBinaryCompare) + End Sub + + [TestCase] + Public Sub InStr_TextCompare_Finds() + Assert.Strict.AreEqual CVar(1&), InStr(1, "abc", "A", vbTextCompare) + End Sub + + [TestCase] + Public Sub InStr_CompareThree_NoError() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", 3) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + [TestCase] + Public Sub InStrB_DatabaseCompare_NoError() + On Error Resume Next + Dim R As Variant + R = InStrB(1, "abc", "a", vbDatabaseCompare) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + [TestCase] + Public Sub InStrRev_DatabaseCompare_NoError() + On Error Resume Next + Dim R As Variant + R = InStrRev("abc", "a", -1, vbDatabaseCompare) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + ' The defect, in InStr, StrComp and Filter. + [TestCase] + Public Sub StrComp_DatabaseCompare_Raises5() + On Error Resume Next + Dim R As Variant + R = StrComp("a", "A", vbDatabaseCompare) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Filter_DatabaseCompare_Raises5() + Dim F(0 To 1) As String + F(0) = "abc": F(1) = "x" + On Error Resume Next + Dim R As Variant + R = Filter(F, "a", True, vbDatabaseCompare) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStr_DatabaseCompare_Raises5() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", vbDatabaseCompare) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStr_DatabaseCompare_ErrorDescription() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", vbDatabaseCompare) + Assert.Strict.AreEqual "Invalid procedure call or argument", Err.Description + End Sub + +End Module diff --git a/bugs/database-compare-no-error/src/Sources/Startup.twin b/bugs/database-compare-no-error/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/database-compare-no-error/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/database-compare-no-error/vb6/Module1.bas b/bugs/database-compare-no-error/vb6/Module1.bas new file mode 100644 index 000000000..5c4786614 --- /dev/null +++ b/bugs/database-compare-no-error/vb6/Module1.bas @@ -0,0 +1,30 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' What VB6 does with a compare argument of 2 (vbDatabaseCompare) outside Access. +' Build and run it with: node scripts/bug_repro.mjs vb6 instr-database-compare-no-error + +Private Sub Report(ByVal Label As String, ByVal Result As Variant) + Print #9, Label & ": " & Err.Number & " " & Err.Description & " (result " & CStr(Result) & ")" + Err.Clear +End Sub + +Sub Main() + Dim R As Variant + Open App.Path & "\out.txt" For Output As #9 + On Error Resume Next + R = Empty: R = InStr(1, "abc", "a", 0): Report "InStr_Binary", R + R = Empty: R = InStr(1, "abc", "A", 1): Report "InStr_Text", R + R = Empty: R = InStr(1, "abc", "a", 2): Report "InStr_Database", R + R = Empty: R = InStr(1, "abc", "A", 2): Report "InStr_Database_Upper", R + R = Empty: R = InStr(1, "abc", "a", 3): Report "InStr_Three", R + R = Empty: R = InStrB(1, "abc", "a", 2): Report "InStrB_Database", R + R = Empty: R = InStrRev("abc", "a", -1, 2): Report "InStrRev_Database", R + R = Empty: R = StrComp("a", "A", 2): Report "StrComp_Database", R + R = Empty: R = Replace("abc", "a", "x", 1, -1, 2): Report "Replace_Database", R + R = Empty: R = UBound(Split("a,b", ",", -1, 2)): Report "Split_Database", R + Dim F(0 To 1) As String + F(0) = "abc": F(1) = "x" + R = Empty: R = UBound(Filter(F, "a", True, 2)): Report "Filter_Database", R + Close #9 +End Sub diff --git a/bugs/database-compare-no-error/vb6/Probe.vbp b/bugs/database-compare-no-error/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/database-compare-no-error/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/instr-invalid-lcid-empty-string/instr-invalid-lcid-empty-string.twinproj b/bugs/instr-invalid-lcid-empty-string/instr-invalid-lcid-empty-string.twinproj new file mode 100644 index 000000000..364fe6eef Binary files /dev/null and b/bugs/instr-invalid-lcid-empty-string/instr-invalid-lcid-empty-string.twinproj differ diff --git a/bugs/instr-invalid-lcid-empty-string/repro.json b/bugs/instr-invalid-lcid-empty-string/repro.json new file mode 100644 index 000000000..45898c5de --- /dev/null +++ b/bugs/instr-invalid-lcid-empty-string/repro.json @@ -0,0 +1,20 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.InStr_BadLcid_Raises5$", + "^PASS RegressionTests.InStr_BadLcid_StartPastEnd_Raises5$", + "^PASS RegressionTests.InStrRev_BadLcid_EmptyString1_Raises5$", + "^PASS RegressionTests.Replace_BadLcid_EmptyExpression_Raises5$", + "^PASS RegressionTests.InStrB_BadLcid_EmptyString1_ReturnsZero$", + "^PASS RegressionTests.InStrB_BadLcid_EmptyString2_ReturnsStart$", + "^PASS RegressionTests.StrComp_BadLcid_EmptyString1_ReturnsMinusOne$", + "^FAIL RegressionTests.InStr_BadLcid_EmptyString1_ReturnsZero: .*Actual: 5 ", + "^FAIL RegressionTests.InStr_BadLcid_EmptyString2_ReturnsStart: .*Actual: 5 ", + "^FAIL RegressionTests.InStr_BadLcid_BothEmpty_ReturnsZero: .*Actual: 5 ", + "^FAIL RegressionTests.InStr_MinusTwo_EmptyString1_ReturnsZero: .*Actual: 5 ", + "^FAIL RegressionTests.InStr_MinusTwo_EmptyString2_ReturnsStart: .*Actual: 5 " + ] + } +} diff --git a/bugs/instr-invalid-lcid-empty-string/src/Settings b/bugs/instr-invalid-lcid-empty-string/src/Settings new file mode 100644 index 000000000..90b465c3a --- /dev/null +++ b/bugs/instr-invalid-lcid-empty-string/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "InstrInvalidLcidEmptyString", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `InStr` raises error 5 for an invalid locale ID when *string1* or *string2* is zero-length, where VB6 raises nothing", + "project.exportPathIsV2": true, + "project.id": "{649D7E29-EA5F-4A72-9F67-02AA9FCDDA93}", + "project.name": "InstrInvalidLcidEmptyString", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/instr-invalid-lcid-empty-string/src/Sources/RegressionTests.twin b/bugs/instr-invalid-lcid-empty-string/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..bc132d033 --- /dev/null +++ b/bugs/instr-invalid-lcid-empty-string/src/Sources/RegressionTests.twin @@ -0,0 +1,115 @@ +' Regression tests: InStr given a compare that is not a valid locale ID raises no error +' when string1 or string2 is zero-length, and returns its usual result. Expected results +' are VB6's (the VB6 project beside this one prints them). tbrun --tests runs each case and +' prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: VB6 raises 5 for the invalid locale ID whenever neither string is empty. + [TestCase] + Public Sub InStr_BadLcid_Raises5() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStr_BadLcid_StartPastEnd_Raises5() + On Error Resume Next + Dim R As Variant + R = InStr(5, "abc", "a", 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStrRev_BadLcid_EmptyString1_Raises5() + On Error Resume Next + Dim R As Variant + R = InStrRev("", "a", -1, 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Replace_BadLcid_EmptyExpression_Raises5() + On Error Resume Next + Dim R As Variant + R = Replace("", "a", "x", 1, -1, 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + ' Controls: InStrB and StrComp return before the check, as VB6's InStr does. + [TestCase] + Public Sub InStrB_BadLcid_EmptyString1_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStrB(1, "", "a", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStrB_BadLcid_EmptyString2_ReturnsStart() + On Error Resume Next + Dim R As Variant + R = InStrB(1, "abc", "", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(1&), R + End Sub + + [TestCase] + Public Sub StrComp_BadLcid_EmptyString1_ReturnsMinusOne() + On Error Resume Next + Dim R As Variant + R = StrComp("", "a", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(-1), R + End Sub + + ' The defect: a zero-length string1 or string2 returns before the compare is checked. + [TestCase] + Public Sub InStr_BadLcid_EmptyString1_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStr(1, "", "a", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStr_BadLcid_EmptyString2_ReturnsStart() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(1&), R + End Sub + + [TestCase] + Public Sub InStr_BadLcid_BothEmpty_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStr(1, "", "", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStr_MinusTwo_EmptyString1_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStr(1, "", "a", -2) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStr_MinusTwo_EmptyString2_ReturnsStart() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "", -2) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(1&), R + End Sub + +End Module diff --git a/bugs/instr-invalid-lcid-empty-string/src/Sources/Startup.twin b/bugs/instr-invalid-lcid-empty-string/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/instr-invalid-lcid-empty-string/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/instr-invalid-lcid-empty-string/vb6/Module1.bas b/bugs/instr-invalid-lcid-empty-string/vb6/Module1.bas new file mode 100644 index 000000000..e4ec93a85 --- /dev/null +++ b/bugs/instr-invalid-lcid-empty-string/vb6/Module1.bas @@ -0,0 +1,33 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' What VB6 does with an invalid locale ID as compare when a string is zero-length. +' Build and run it with: node scripts/bug_repro.mjs vb6 instr-invalid-lcid-empty-string + +Private Sub Report(ByVal Label As String, ByVal Result As Variant) + Print #9, Label & ": " & Err.Number & " " & Err.Description & " (result " & CStr(Result) & ")" + Err.Clear +End Sub + +Sub Main() + Dim R As Variant + Open App.Path & "\out.txt" For Output As #9 + On Error Resume Next + R = Empty: R = InStr(1, "abc", "a", 99999): Report "InStr_BadLcid", R + R = Empty: R = InStr(1, "", "a", 99999): Report "InStr_BadLcid_EmptyString1", R + R = Empty: R = InStr(1, "abc", "", 99999): Report "InStr_BadLcid_EmptyString2", R + R = Empty: R = InStr(1, "", "", 99999): Report "InStr_BadLcid_BothEmpty", R + R = Empty: R = InStr(5, "abc", "a", 99999): Report "InStr_BadLcid_StartPastEnd", R + R = Empty: R = InStr(1, "ab", "abc", 99999): Report "InStr_BadLcid_String2Longer", R + R = Empty: R = InStr(1, "", "a", -2): Report "InStr_MinusTwo_EmptyString1", R + R = Empty: R = InStr(1, "abc", "", -2): Report "InStr_MinusTwo_EmptyString2", R + R = Empty: R = InStrB(1, "", "a", 99999): Report "InStrB_BadLcid_EmptyString1", R + R = Empty: R = InStrB(1, "abc", "", 99999): Report "InStrB_BadLcid_EmptyString2", R + R = Empty: R = InStrRev("", "a", -1, 99999): Report "InStrRev_BadLcid_EmptyString1", R + R = Empty: R = InStrRev("abc", "", -1, 99999): Report "InStrRev_BadLcid_EmptyString2", R + R = Empty: R = StrComp("", "a", 99999): Report "StrComp_BadLcid_Empty", R + R = Empty: R = Replace("", "a", "x", 1, -1, 99999): Report "Replace_BadLcid_Empty", R + R = Empty: R = Replace("abc", "a", "x", 1, -1, 99999): Report "Replace_BadLcid", R + R = Empty: R = UBound(Split("", ",", -1, 99999)): Report "Split_BadLcid_Empty", R + Close #9 +End Sub diff --git a/bugs/instr-invalid-lcid-empty-string/vb6/Probe.vbp b/bugs/instr-invalid-lcid-empty-string/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/instr-invalid-lcid-empty-string/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/redim-inverted-bounds-error/redim-inverted-bounds-error.twinproj b/bugs/redim-inverted-bounds-error/redim-inverted-bounds-error.twinproj new file mode 100644 index 000000000..49756c5ad Binary files /dev/null and b/bugs/redim-inverted-bounds-error/redim-inverted-bounds-error.twinproj differ diff --git a/bugs/redim-inverted-bounds-error/repro.json b/bugs/redim-inverted-bounds-error/repro.json new file mode 100644 index 000000000..5f4a12400 --- /dev/null +++ b/bugs/redim-inverted-bounds-error/repro.json @@ -0,0 +1,15 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.ReDim_OneElement_Succeeds$", + "^PASS RegressionTests.ReDim_NegativeBoundsInOrder_Succeeds$", + "^PASS RegressionTests.FixedArray_SubscriptAboveRange_Raises9$", + "^FAIL RegressionTests.ReDim_ZeroToMinusOne_Raises9: .*Actual: -2147467259", + "^FAIL RegressionTests.ReDim_MinusOne_Raises9: .*Actual: -2147467259", + "^FAIL RegressionTests.ReDim_FiveToFour_SecondDimension_Raises9: .*Actual: -2147467259", + "^FAIL RegressionTests.ReDim_ErrorDescription: .*Actual: \"Unspecified error\"" + ] + } +} diff --git a/bugs/redim-inverted-bounds-error/src/Settings b/bugs/redim-inverted-bounds-error/src/Settings new file mode 100644 index 000000000..526080587 --- /dev/null +++ b/bugs/redim-inverted-bounds-error/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "RedimInvertedBoundsError", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: ReDim with an upper bound below its lower bound raises -2147467259, *Unspecified error*, where VB6 raises error 9", + "project.exportPathIsV2": true, + "project.id": "{E8A737D9-E99E-4617-B39B-915A53D222A4}", + "project.name": "RedimInvertedBoundsError", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/redim-inverted-bounds-error/src/Sources/RegressionTests.twin b/bugs/redim-inverted-bounds-error/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..de377aac3 --- /dev/null +++ b/bugs/redim-inverted-bounds-error/src/Sources/RegressionTests.twin @@ -0,0 +1,62 @@ +' Regression tests: ReDim with an upper bound less than its lower bound raises error 9, +' "Subscript out of range". Expected numbers are VB6's (the VB6 project beside this one +' prints them). tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: bounds in order, an empty-looking but valid range, and a subscript out of range. + [TestCase] + Public Sub ReDim_OneElement_Succeeds() + Dim X() As Long + ReDim X(0 To 0) + Assert.Strict.AreEqual 0&, UBound(X) + End Sub + + [TestCase] + Public Sub ReDim_NegativeBoundsInOrder_Succeeds() + Dim X() As Long + ReDim X(-3 To -1) + Assert.Strict.AreEqual -3&, LBound(X) + End Sub + + [TestCase] + Public Sub FixedArray_SubscriptAboveRange_Raises9() + Dim A(1 To 3) As Long + On Error Resume Next + A(4) = 1 + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_ZeroToMinusOne_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(0 To -1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_MinusOne_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(-1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_FiveToFour_SecondDimension_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(5 To 4, 1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_ErrorDescription() + Dim X() As Long + On Error Resume Next + ReDim X(0 To -1) + Assert.Strict.AreEqual "Subscript out of range", Err.Description + End Sub + +End Module diff --git a/bugs/redim-inverted-bounds-error/src/Sources/Startup.twin b/bugs/redim-inverted-bounds-error/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/redim-inverted-bounds-error/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/redim-inverted-bounds-error/vb6/Module1.bas b/bugs/redim-inverted-bounds-error/vb6/Module1.bas new file mode 100644 index 000000000..9cff43dae --- /dev/null +++ b/bugs/redim-inverted-bounds-error/vb6/Module1.bas @@ -0,0 +1,30 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' What VB6 raises for ReDim with an upper bound less than its lower bound: error 9. +' Build and run it with: node scripts/bug_repro.mjs vb6 redim-inverted-bounds-error + +Private Sub Report(ByVal Label As String) + Print #9, Label & ": " & Err.Number & " " & Err.Description + Err.Clear +End Sub + +Sub Main() + Open App.Path & "\out.txt" For Output As #9 + On Error Resume Next + Dim X() As Long + Dim A(1 To 3) As Long + ReDim X(0 To 0) + Report "ReDim_OneElement" + ReDim X(-3 To -1) + Report "ReDim_NegativeBoundsInOrder" + A(4) = 1 + Report "FixedArray_SubscriptAboveRange" + ReDim X(0 To -1) + Report "ReDim_ZeroToMinusOne" + ReDim X(-1) + Report "ReDim_MinusOne" + ReDim X(5 To 4, 1) + Report "ReDim_FiveToFour_SecondDimension" + Close #9 +End Sub diff --git a/bugs/redim-inverted-bounds-error/vb6/Probe.vbp b/bugs/redim-inverted-bounds-error/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/redim-inverted-bounds-error/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/strconv-east-asian-lcid/repro.json b/bugs/strconv-east-asian-lcid/repro.json new file mode 100644 index 000000000..dd8170dbb --- /dev/null +++ b/bugs/strconv-east-asian-lcid/repro.json @@ -0,0 +1,17 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.UpperCase_Japanese_Converts$", + "^PASS RegressionTests.Wide_NoLcid_Raises5$", + "^PASS RegressionTests.Wide_English_Raises5$", + "^FAIL RegressionTests.Wide_Japanese_Converts: [(]runtime error 5: ", + "^FAIL RegressionTests.Narrow_Japanese_Converts: [(]runtime error 5: ", + "^FAIL RegressionTests.Katakana_Japanese_Converts: [(]runtime error 5: ", + "^FAIL RegressionTests.Hiragana_Japanese_Converts: [(]runtime error 5: ", + "^FAIL RegressionTests.Wide_Chinese_Converts: [(]runtime error 5: ", + "^FAIL RegressionTests.Wide_Korean_Converts: [(]runtime error 5: " + ] + } +} diff --git a/bugs/strconv-east-asian-lcid/src/Settings b/bugs/strconv-east-asian-lcid/src/Settings new file mode 100644 index 000000000..93cb6022f --- /dev/null +++ b/bugs/strconv-east-asian-lcid/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "StrconvEastAsianLcid", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `StrConv` raises error 5 for `vbWide`, `vbNarrow`, `vbKatakana` and `vbHiragana` with an East Asian *LCID* on a system whose locale is not East Asian, where VB6 converts", + "project.exportPathIsV2": true, + "project.id": "{36B180F9-66F1-4AD3-94A0-8E2BDAEF20FB}", + "project.name": "StrconvEastAsianLcid", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/strconv-east-asian-lcid/src/Sources/RegressionTests.twin b/bugs/strconv-east-asian-lcid/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..6785d5493 --- /dev/null +++ b/bugs/strconv-east-asian-lcid/src/Sources/RegressionTests.twin @@ -0,0 +1,63 @@ +' Regression tests: StrConv's vbWide, vbNarrow, vbKatakana and vbHiragana convert when +' the LCID argument names an East Asian locale, whatever the system's locale. Expected +' results are VB6's on a system whose locale is 1033, English (United States) (the VB6 +' project beside this one prints them). tbrun --tests runs each case and prints PASS +' or FAIL. The controls assume such a system. +[TestFixture] +Module RegressionTests + + ' Controls: another conversion with an East Asian LCID, and vbWide with no East + ' Asian locale at all, which VB6 refuses too. + [TestCase] + Public Sub UpperCase_Japanese_Converts() + Assert.Strict.AreEqual CVar("A"), StrConv("a", vbUpperCase, 1041) + End Sub + + [TestCase] + Public Sub Wide_NoLcid_Raises5() + On Error Resume Next + Dim R As Variant + R = StrConv("a", vbWide) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Wide_English_Raises5() + On Error Resume Next + Dim R As Variant + R = StrConv("a", vbWide, 1033) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + ' The defect. + [TestCase] + Public Sub Wide_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 1041) + End Sub + + [TestCase] + Public Sub Narrow_Japanese_Converts() + Assert.Strict.AreEqual CVar("a"), StrConv(ChrW(&HFF41&), vbNarrow, 1041) + End Sub + + [TestCase] + Public Sub Katakana_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&H30A2)), StrConv(ChrW(&H3042), vbKatakana, 1041) + End Sub + + [TestCase] + Public Sub Hiragana_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&H3042)), StrConv(ChrW(&H30A2), vbHiragana, 1041) + End Sub + + [TestCase] + Public Sub Wide_Chinese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 2052) + End Sub + + [TestCase] + Public Sub Wide_Korean_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 1042) + End Sub + +End Module diff --git a/bugs/strconv-east-asian-lcid/src/Sources/Startup.twin b/bugs/strconv-east-asian-lcid/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/strconv-east-asian-lcid/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/strconv-east-asian-lcid/strconv-east-asian-lcid.twinproj b/bugs/strconv-east-asian-lcid/strconv-east-asian-lcid.twinproj new file mode 100644 index 000000000..c7409b319 Binary files /dev/null and b/bugs/strconv-east-asian-lcid/strconv-east-asian-lcid.twinproj differ diff --git a/bugs/strconv-east-asian-lcid/vb6/Module1.bas b/bugs/strconv-east-asian-lcid/vb6/Module1.bas new file mode 100644 index 000000000..95bde77bf --- /dev/null +++ b/bugs/strconv-east-asian-lcid/vb6/Module1.bas @@ -0,0 +1,38 @@ +Attribute VB_Name = "Module1" +Option Explicit + +Private Declare Function GetSystemDefaultLCID Lib "kernel32" () As Long + +' What VB6 does with StrConv's East Asian conversions given an East Asian LCID, on a +' system whose locale is not East Asian. +' Build and run it with: node scripts/bug_repro.mjs vb6 strconv-east-asian-lcid + +Private Function Codes(ByVal S As Variant) As String + Dim I As Long + If VarType(S) <> vbString Then Codes = "-": Exit Function + For I = 1 To Len(S) + Codes = Codes & Hex$(AscW(Mid$(S, I, 1))) & " " + Next +End Function + +Private Sub Report(ByVal Label As String, ByVal Result As Variant) + Print #9, Label & ": " & Err.Number & " " & Err.Description & " (" & Codes(Result) & ")" + Err.Clear +End Sub + +Sub Main() + Dim R As Variant + Open App.Path & "\out.txt" For Output As #9 + On Error Resume Next + Print #9, "System locale: " & Hex$(GetSystemDefaultLCID()) + R = Empty: R = StrConv("a", vbUpperCase, 1041): Report "UpperCase_1041", R + R = Empty: R = StrConv("a", vbWide): Report "Wide_NoLcid", R + R = Empty: R = StrConv("a", vbWide, 1033): Report "Wide_1033", R + R = Empty: R = StrConv("a", vbWide, 1041): Report "Wide_1041", R + R = Empty: R = StrConv(ChrW(&HFF41), vbNarrow, 1041): Report "Narrow_1041", R + R = Empty: R = StrConv(ChrW(&H3042), vbKatakana, 1041): Report "Katakana_1041", R + R = Empty: R = StrConv(ChrW(&H30A2), vbHiragana, 1041): Report "Hiragana_1041", R + R = Empty: R = StrConv("a", vbWide, 2052): Report "Wide_2052", R + R = Empty: R = StrConv("a", vbWide, 1042): Report "Wide_1042", R + Close #9 +End Sub diff --git a/bugs/strconv-east-asian-lcid/vb6/Probe.vbp b/bugs/strconv-east-asian-lcid/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/strconv-east-asian-lcid/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/string-null-character/repro.json b/bugs/string-null-character/repro.json new file mode 100644 index 000000000..9064e3703 --- /dev/null +++ b/bugs/string-null-character/repro.json @@ -0,0 +1,15 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.String_Letter_Repeats$", + "^PASS RegressionTests.String_NullNumber_Raises94$", + "^PASS RegressionTests.StringDollar_NullNumber_Raises94$", + "^PASS RegressionTests.StringDollar_NullCharacter_Raises94$", + "^FAIL RegressionTests.String_NullCharacter_NoError: .*Actual: 94 ", + "^FAIL RegressionTests.String_NullCharacter_ReturnsNull: [(]runtime error 94: ", + "^FAIL RegressionTests.String_ZeroNullCharacter_ReturnsNull: [(]runtime error 94: " + ] + } +} diff --git a/bugs/string-null-character/src/Settings b/bugs/string-null-character/src/Settings new file mode 100644 index 000000000..99b8c6187 --- /dev/null +++ b/bugs/string-null-character/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "StringNullCharacter", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `String` with a *character* of `Null` raises error 94, where VB6 returns `Null`", + "project.exportPathIsV2": true, + "project.id": "{5B37188A-FBCA-47E0-8FA0-6F64E51CE76F}", + "project.name": "StringNullCharacter", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/string-null-character/src/Sources/RegressionTests.twin b/bugs/string-null-character/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..54474e3ad --- /dev/null +++ b/bugs/string-null-character/src/Sources/RegressionTests.twin @@ -0,0 +1,60 @@ +' Regression tests: the Variant form String, given a character of Null, returns Null +' with no error; String$ raises error 94. Expected results are VB6's (the VB6 project +' beside this one prints them). tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: a letter, a Null number in either form, and String$ with a Null character. + [TestCase] + Public Sub String_Letter_Repeats() + Assert.Strict.AreEqual CVar("aaa"), String(3, "a") + End Sub + + [TestCase] + Public Sub String_NullNumber_Raises94() + On Error Resume Next + Dim R As Variant + R = String(Null, "a") + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + [TestCase] + Public Sub StringDollar_NullNumber_Raises94() + On Error Resume Next + Dim R As Variant + R = String$(Null, "a") + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + [TestCase] + Public Sub StringDollar_NullCharacter_Raises94() + On Error Resume Next + Dim R As Variant + R = String$(3, Null) + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + ' The defect: String with a Null character returns Null. + [TestCase] + Public Sub String_NullCharacter_NoError() + On Error Resume Next + Dim R As Variant + R = String(3, Null) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + [TestCase] + Public Sub String_NullCharacter_ReturnsNull() + Dim R As Variant + R = String(3, Null) + Assert.Strict.AreEqual vbNull, VarType(R) + End Sub + + [TestCase] + Public Sub String_ZeroNullCharacter_ReturnsNull() + Dim R As Variant + R = String(0, Null) + Assert.Strict.AreEqual vbNull, VarType(R) + End Sub + +End Module diff --git a/bugs/string-null-character/src/Sources/Startup.twin b/bugs/string-null-character/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/string-null-character/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/string-null-character/string-null-character.twinproj b/bugs/string-null-character/string-null-character.twinproj new file mode 100644 index 000000000..4d8919c87 Binary files /dev/null and b/bugs/string-null-character/string-null-character.twinproj differ diff --git a/bugs/string-null-character/vb6/Module1.bas b/bugs/string-null-character/vb6/Module1.bas new file mode 100644 index 000000000..20a03b855 --- /dev/null +++ b/bugs/string-null-character/vb6/Module1.bas @@ -0,0 +1,23 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' What VB6 does with String given a character of Null: String returns Null, String$ raises 94. +' Build and run it with: node scripts/bug_repro.mjs vb6 string-null-character + +Private Sub Report(ByVal Label As String, ByVal Result As Variant) + Print #9, Label & ": " & Err.Number & " " & Err.Description & " (VarType " & VarType(Result) & ")" + Err.Clear +End Sub + +Sub Main() + Dim R As Variant + Open App.Path & "\out.txt" For Output As #9 + On Error Resume Next + R = Empty: R = String(3, "a"): Report "String_Letter", R + R = Empty: R = String(Null, "a"): Report "String_NullNumber", R + R = Empty: R = String$(Null, "a"): Report "StringDollar_NullNumber", R + R = Empty: R = String$(3, Null): Report "StringDollar_NullCharacter", R + R = Empty: R = String(3, Null): Report "String_NullCharacter", R + R = Empty: R = String(0, Null): Report "String_Zero_NullCharacter", R + Close #9 +End Sub diff --git a/bugs/string-null-character/vb6/Probe.vbp b/bugs/string-null-character/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/string-null-character/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/builder/cpu-worker.mjs b/builder/cpu-worker.mjs index 47c623ea2..767755d75 100644 --- a/builder/cpu-worker.mjs +++ b/builder/cpu-worker.mjs @@ -631,7 +631,7 @@ async function pullLoop() { } } -function reconstructLinkTables({ byPath, byUrl, byRedirect }) { +function reconstructLinkTables({ byPath, byUrl, byRedirect, byShortcut }) { const make = (pairs) => new Map(pairs.map(([k, pl]) => [k, { permalink: pl }])); - return { byPath: make(byPath), byUrl: make(byUrl), byRedirect: make(byRedirect) }; + return { byPath: make(byPath), byUrl: make(byUrl), byRedirect: make(byRedirect), byShortcut: make(byShortcut) }; } diff --git a/builder/page-baseline.json b/builder/page-baseline.json index 0a44df948..80ca1509d 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 1502, + "pages": 1503, "staticFiles": 1124 } diff --git a/builder/render.mjs b/builder/render.mjs index fffa57ed2..91755c0ce 100644 --- a/builder/render.mjs +++ b/builder/render.mjs @@ -1590,14 +1590,29 @@ function renderTocItems(items, indent, out) { // ---------- §5.3 relative-links plugin -------------------------------------- +// A page's `shortcut:` front-matter key names it for `tb:` links, which +// resolve to its permalink wherever it moves. A name is a word, and is unique. +export const SHORTCUT_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/; + export function buildLinkTables(pages) { const byPath = new Map(); const byUrl = new Map(); const byRedirect = new Map(); + const byShortcut = new Map(); for (const p of pages) { putOnce(byPath, p.srcRel, p); + const shortcut = p.frontmatter.shortcut; + if (shortcut !== undefined) { + if (typeof shortcut !== "string" || !SHORTCUT_NAME.test(shortcut)) { + throw new Error(`${p.srcRel}: shortcut: must be a name of letters, digits, _ and -, starting with a letter`); + } + const other = byShortcut.get(shortcut); + if (other) throw new Error(`${p.srcRel}: shortcut ${shortcut} is already ${other.srcRel}'s`); + byShortcut.set(shortcut, p); + } + const url = p.permalink.replace(/^\//, ""); if (url !== "") { putOnce(byUrl, url, p); @@ -1611,15 +1626,20 @@ export function buildLinkTables(pages) { } } - return { byPath, byUrl, byRedirect }; + return { byPath, byUrl, byRedirect, byShortcut }; } // Serialize linkTables for cross-thread transfer. resolveLink() only reads // .permalink from each page, so shipping [key, permalink] pairs is sufficient. // Workers reconstruct minimal { permalink } stubs via reconstructLinkTables. export function serializeLinkTables(lt) { - const pairs = (m) => [...m.entries()].map(([k, p]) => [k, p.permalink]); - return { byPath: pairs(lt.byPath), byUrl: pairs(lt.byUrl), byRedirect: pairs(lt.byRedirect) }; + const pairs = (m) => [...(m ?? new Map()).entries()].map(([k, p]) => [k, p.permalink]); + return { + byPath: pairs(lt.byPath), + byUrl: pairs(lt.byUrl), + byRedirect: pairs(lt.byRedirect), + byShortcut: pairs(lt.byShortcut), + }; } function putOnce(map, key, value) { @@ -1651,6 +1671,23 @@ function relativeLinksPlugin(md, ctx) { const idx = token.attrIndex(attrName); if (idx < 0) return; const value = token.attrs[idx][1]; + // `tb:[#fragment]`: the page whose front matter says `shortcut: `. + // Before the scheme test, which would pass it through as a URL. + if (/^tb:/i.test(value)) { + if (!ctx.linkTables) return; // a tool rendering without the site's pages + const [name, frag] = splitFragment(value.slice(3)); + const target = ctx.linkTables.byShortcut?.get(name); + if (attrName !== "href") throw new Error(`${fromPage.srcRel}: ${value} is a page shortcut, not an image`); + if (!target) { + throw new Error( + `${fromPage.srcRel}: no page has the shortcut in ${value}; ` + + "a page names its shortcut with `shortcut: ` in its front matter", + ); + } + const url = `${ctx.baseurl}${target.permalink}`; + token.attrs[idx][1] = frag ? `${url}#${frag}` : url; + return; + } if (/^([a-z][a-z0-9+.\-]*:|#|\/\/)/i.test(value)) return; const [pathPart, fragPart] = splitFragment(value); diff --git a/docs/Documentation/Authoring.md b/docs/Documentation/Authoring.md index 440a5d1ab..6171ba4e1 100644 --- a/docs/Documentation/Authoring.md +++ b/docs/Documentation/Authoring.md @@ -944,6 +944,14 @@ Relative links resolve against a page's **rendered URL** (its `permalink`), not When the right number of `../` steps is not obvious, copy a working link from a neighbouring page that points where you want to go and change the final segment. [`build.bat`](Building#checking-link-integrity) catches any link that resolves to nothing. +### Page shortcuts + +A page that many pages link to can be given a short name, its **shortcut**, with `shortcut: ` in its frontmatter. Any page then links to it as `[text](tb:)`, or to an anchor in it as `[text](tb:#anchor)`, from any folder and with no `../`. The build writes the page's real URL in its place, so the link keeps working when the page's file moves. + +The [Run-time Errors](../../Reference/Run-time-Errors) page has the shortcut `Errors`, and each error number is an anchor of its own, so `[5](tb:Errors#5)` links to error 5. + +A shortcut is a single word of letters, digits, `_` and `-`, starting with a letter, and no two pages may share one. The build fails on a `tb:` link whose name no page has, and the link check reports an anchor the page does not have. A `tb:` link inside code is left as written. A plain markdown preview, such as GitHub's, shows a `tb:` link as broken; the built site does not. + ## Crossing between VBA and a package: the depth asymmetry **VBA pages sit one URL segment shallower than every package page.** The VBA diff --git a/docs/Documentation/Builder.md b/docs/Documentation/Builder.md index 04148dc95..d8dc612db 100644 --- a/docs/Documentation/Builder.md +++ b/docs/Documentation/Builder.md @@ -633,6 +633,7 @@ The build aborts or flips the exit code in these cases: - **Nav integrity.** An orphan or ambiguous `parent:` throws inside `nav.execute()`, which aborts the build through `Scheduler._abort()`. - **Unpublishable file type.** Every non-page under `docs/` is copied into the output verbatim, so `publish-policy.mjs` holds an allowlist of publishable types and throws on anything else. It runs in `discover` over the static-file inventory, naming the source path before a byte is written, and again in `dispatch` over each tree's derived inventory, the only sweep that sees redirect stubs, vendored theme assets and generated auxiliaries. Neither depends on `--check`. This failure aborts instead of setting an exit code: a broken link leaves a tree worth inspecting, a tree carrying a private key does not. `SOURCE_EXTENSIONS` and `BUILD_EXTENSIONS` are separate sets so the build can emit `sitemap.xml` and `search-data.json` without allowing a stray `docs/secrets.json`; [`scripts/check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy) asserts they stay separate. - **Redirect collision.** A `redirect_from:` entry becomes a stub page at that URL, so `deriveRedirectStubs()` refuses an entry pointing at a URL some page already publishes, and two pages declaring the same entry. Both name the source file on each side. +- **Page shortcut.** `buildLinkTables()` throws when two pages claim the same `shortcut:` or one is not a name, and `relativeLinksPlugin` throws, naming the page, on a `tb:` link whose name no page has, and on `tb:` as an image's source. Markdown-it would otherwise write the `tb:` URL into the page as it stands. See [Page shortcuts](Authoring#page-shortcuts). - **Destination collision.** `assertNoDestinationCollisions()` runs before the write phase and throws if a static file's destination path equals a page's. The static-file copy and the page write run in parallel, so the survivor would otherwise depend on I/O ordering. - **Missing PDF input.** Phase 8 aborts when the book cannot be assembled: no page (or more than one) has `layout: book-combined`, a font `print.css` names is absent from the source tree (the message names `scripts/build_fonts.py`), or an image `book.html` references is not under the source tree. The last is the one that fires in practice; `--tolerate-missing-images` downgrades only that one to a warning. - **A page the book manifest does not mention.** Not a failure. `bookCoverage()` compares every page with `_book.yml`'s entries and its `left_out:` list, and the summary prints a `book:` warning for a page in neither, a page in both, an entry that matches no page, and a landing or foreword URL that names no page. It runs only when the book is built, and [`check_book_coverage.mjs`](Tools-Toolchain-Tests#check-book-coverage) holds each finding to a probe. See [Book Configuration](Book-Configuration#pages-left-out-of-the-book). diff --git a/docs/Documentation/Building.md b/docs/Documentation/Building.md index 523da7058..7e1652725 100644 --- a/docs/Documentation/Building.md +++ b/docs/Documentation/Building.md @@ -89,6 +89,7 @@ Do not drop `--check-audit-index` when typing the underlying build command: with && node scripts/check_page_baseline.mjs \ && node scripts/check_book_coverage.mjs \ && node scripts/check_package_tests.mjs \ + && node scripts/check_errors.mjs \ && node scripts/check_symbol_index.mjs \ && node scripts/check_twin_parsers.mjs \ && node scripts/check_attribute_sweep.mjs \ diff --git a/docs/Documentation/Tools-Defects.md b/docs/Documentation/Tools-Defects.md index ca31c8ca1..199209201 100644 --- a/docs/Documentation/Tools-Defects.md +++ b/docs/Documentation/Tools-Defects.md @@ -149,6 +149,27 @@ Other options: `--timeout S` is the time limit, in seconds, for each run of the Exit codes: **0** the sample ran, or, with `--docs`, no fence differs and none raised an error; **1** a VB6 compile error, a run-time error, a sample during which the exe ended (a VB6 exe that dies of an access violation can exit with code 0, so the exe ending before the time limit in the middle of a sample is what shows it; the fault the Application event log records is named when there is one, and so are a box the exe opened and what the VB runtime logged) or a sample that did not return, or, with `--docs`, at least one fence that differs or raised an error; **2** the harness could not run --- a refused command line, a file that is missing, a sample that is refused, no VB6, VB6 failing to build, or a crash. +## errors.mjs +{: #errors } + + node scripts/errors.mjs run [...] [--only ] [--runtime tb|vb6|win64]... [--ide ] [--vb6 ] [--keep] + node scripts/errors.mjs generate [...] [--only ] + node scripts/errors.mjs write [...] + node scripts/errors.mjs messages [--runtime tb|vb6]... [--ide ] [--vb6 ] [--keep] + +Measures the run-time errors each documented procedure raises, and writes each reference page's Common Errors section from them. The errors are data, not prose: a YAML file per area under `errors/` lists, for each page, the cases that raise each error and the cases that must raise nothing, and `errors/messages.yml` gives each number's message: the one `Err.Description` gives once the error is raised, and the one `Error$(number)` gives, where the two differ. The format is in `WIP.Errors.md`. A file is named as it is in `errors/`; with none named, every file there is used. `--only ` keeps the pages whose path under `docs/` matches. + +| Command | Effect | +|---|---| +| `run` | Builds one probe of the cases for each runtime and runs it: 32-bit twinBASIC (`tb`) and VB6 by default, and 64-bit twinBASIC (`win64`) as well for a data file that says `win64: true`; `--runtime` names them instead, and may be repeated. Each case is a procedure of its own that runs under `On Error Resume Next` and reports `Err.Number` and `Err.Description`. The twinBASIC probe is a tree run by [`tbrun --exe`](Tools-Compiler#tbrun), so the cases run in the built exe and not in the IDE, and the VB6 probe is a `Probe.vbp` built and run by the code [`bug_repro.mjs vb6`](#bug-repro) uses, on a private desktop. A case a compiler refuses is reported with the compiler's message and left out, and the probe is built again without it, so one bad case costs a rebuild and not the run. Each case is printed with what each runtime raised, and a case is `FAIL` when a runtime raised another number than the data says, refused it, never reported, or raised a number whose message is not the one in `messages.yml`. `--keep` keeps the generated probes and prints where they are. | +| `generate` | Writes the probes to `/tb`, a tree for `tbrun --exe`, and `/vb6`, a `Probe.vbp` project, without running them. | +| `write` | Writes each page's section from the data, between `` and ``: before the page's first Example heading, else before See Also, else at the end, at the level the page gives those headings. Rows of the data that raise the same number make one row of the table, their conditions one line each, in the order of the data. A page whose data lists only cases that raise nothing gets no section, and loses one it had. Each number in a section links to its row on the central page, [Run-time Errors](../../Reference/Run-time-Errors), and `write` also writes that page's index between `` and ``: a row for every number in `messages.yml`, with its anchor, its message and links to the pages whose sections list it. Writing again changes nothing. [`check_errors.mjs`](Tools-Toolchain-Tests#check-errors) is its gate. | +| `messages` | Builds one probe of the messages for each runtime, 32-bit twinBASIC and VB6 unless `--runtime` names one, runs it as `run` runs its probe, and compares it with `messages.yml`. The probe gives `Error$(n)` and, after `Err.Raise n`, `Err.Number` and `Err.Description`, for every number from 1 to 65535 whose `Error$` is not *Application-defined or object-defined error*, and for every other number the file lists. Every such number from 1 to 65535 must be in the file, and every number in it must give its messages: both in both runtimes for a number from 1 to 65535; for another number, both in twinBASIC and `Error$` in VB6, since VB6's `Err.Raise` turns many HRESULTs into its own numbers (`Err.Raise &H80070057` raises 5). Each disagreement is printed. | + +`run` and `messages` need twinBASIC and VB6, found as `tbrun` and [`vb6run.mjs`](#vb6run) find them, so they are outside every gate and outside CI, and are run by a person when a new BETA is installed. `generate` and `write` need neither. + +Exit codes: **0** `run`, `messages`: every runtime agrees with the data; `generate`, `write`: done; **1** `run`, `messages`: a runtime disagrees with the data; **2** a refused command line, data with a problem, a runtime that could not be run or a probe that ended early, or a crash. + ## ide_js.mjs {: #ide-js } diff --git a/docs/Documentation/Tools-Toolchain-Tests.md b/docs/Documentation/Tools-Toolchain-Tests.md index eb2be997a..d7b7ab907 100644 --- a/docs/Documentation/Tools-Toolchain-Tests.md +++ b/docs/Documentation/Tools-Toolchain-Tests.md @@ -395,6 +395,17 @@ Thirteen probes run first, on pages, sources and an `expect.json` built in memor Exit codes: **0** every probe passed and every test project is in order, **1** a probe failed, or a section or test project is not in order, **2** the gate could not run: a refused command line, or a crash. +## check_errors.mjs +{: #check-errors } + + node scripts/check_errors.mjs + +Verifies that every reference page's Common Errors section is what [`errors.mjs write`](Tools-Defects#errors) makes of the run-time error data under `errors/`, and that the data is in order: every file reads, with no key it does not know; every error number it names has its message in `errors/messages.yml`; a row that differs from VB6 names its reproducer under `bugs/`, or says `pending`; every page it names exists, has a title and a permalink, and is named by one file only; no page under `docs/` has a section that no data file names; and the index of the numbers on the central page, [Run-time Errors](../../Reference/Run-time-Errors), is what `write` makes of the data and `errors/messages.yml`. It reads only the sources, so it needs no twinBASIC, no VB6 and no built tree. Measuring the data is `errors.mjs run`, and measuring the messages `errors.mjs messages`; neither is a gate. + +Twenty-eight probes run first, on data and pages built in memory: how the data and the messages are read and refused, the section with its numbers linked to the central page, its rows for one number merged into one, and its VB6 note, where a new section goes and that a marker inside a fence is not one, that writing twice changes nothing and removing a section gives the page back, the links between permalinks, the central page's index and its markers, how the probe modules and their output are read, and how the measured messages are compared with the list. + +Exit codes: **0** every probe passed and every section matches its data, **1** a probe failed, or the data or a section is not in order, **2** the gate could not run: a refused command line, or a crash. + ## check_symbol_index.mjs {: #check-symbol-index } diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md index 887d4bde4..0b02cc696 100644 --- a/docs/Documentation/Tools.md +++ b/docs/Documentation/Tools.md @@ -113,7 +113,7 @@ A local pass of [`check_a11y.mjs`](Tools-Site-Checks#check-a11y) does not predic test.bat -The tests the toolchain has to pass. Twenty-eight steps, each stopping the run if it fails: +The tests the toolchain has to pass. Twenty-nine steps, each stopping the run if it fails: 1. [`scripts/check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy) --- verifies the publish allowlist still refuses the types it should. Needs neither a browser nor a built tree. 2. [`scripts/check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists) --- verifies the two gate lists on this page still match the wrappers that run them. @@ -136,13 +136,14 @@ The tests the toolchain has to pass. Twenty-eight steps, each stopping the run i 19. [`scripts/check_page_baseline.mjs`](Tools-Toolchain-Tests#check-page-baseline) --- verifies the page-count drift guard still refuses a fall. 20. [`scripts/check_book_coverage.mjs`](Tools-Toolchain-Tests#check-book-coverage) --- verifies the build still warns about a page `docs/_book.yml` does not mention. 21. [`scripts/check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests) --- verifies every NOTE and WARNING of a contributed package's section names the test cases behind it, and that each package test project is in order. -22. [`scripts/check_symbol_index.mjs`](Tools-Toolchain-Tests#check-symbol-index) --- verifies the symbol index still places each kind of symbol, and its drift guard still refuses a lost URL. -23. [`scripts/check_twin_parsers.mjs`](Tools-Toolchain-Tests#check-twin-parsers) --- verifies the scanners of twinBASIC source and of the attribute reference still read the shapes each once misread. -24. [`scripts/check_attribute_sweep.mjs`](Tools-Toolchain-Tests#check-attribute-sweep) --- verifies the logic of the attribute sweep: its site skeletons, how it reads a probe's diagnostics, how it batches probes, and how it compares the answers with `Attributes.md`. -25. [`scripts/check_cli.mjs`](Tools-Toolchain-Tests#check-cli) --- verifies `lib/cli.mjs`, the command-line parser, and each tool's recorded command-line errors. -26. [`scripts/check_pdf_shims_equiv.mjs`](Tools-Toolchain-Tests#check-pdf-shims-equiv) --- verifies the book's pdf-lib shims write what stock pdf-lib writes, patch the members of pdf-lib it lists, have each of them in their shim's table of targets, and run. -27. [`scripts/check_impexp_parity.mjs`](Tools-Toolchain-Tests#check-impexp-parity) --- verifies the editions of the impexp tool pass the same built-in tests, and exit, print and write the same for one sequence of commands. Without Python it reports itself skipped and passes, except in CI. It compares the twinBASIC edition too, when `IMPEXP_EXE` names its built exe. -28. [`scripts/check_axe_patch_equiv.mjs`](Tools-Toolchain-Tests#check-axe-patch-equiv) --- verifies the vendored axe source patch still produces identical colour values. +22. [`scripts/check_errors.mjs`](Tools-Toolchain-Tests#check-errors) --- verifies every page's Common Errors section is what the run-time error data under `errors/` writes, and that the data is in order. +23. [`scripts/check_symbol_index.mjs`](Tools-Toolchain-Tests#check-symbol-index) --- verifies the symbol index still places each kind of symbol, and its drift guard still refuses a lost URL. +24. [`scripts/check_twin_parsers.mjs`](Tools-Toolchain-Tests#check-twin-parsers) --- verifies the scanners of twinBASIC source and of the attribute reference still read the shapes each once misread. +25. [`scripts/check_attribute_sweep.mjs`](Tools-Toolchain-Tests#check-attribute-sweep) --- verifies the logic of the attribute sweep: its site skeletons, how it reads a probe's diagnostics, how it batches probes, and how it compares the answers with `Attributes.md`. +26. [`scripts/check_cli.mjs`](Tools-Toolchain-Tests#check-cli) --- verifies `lib/cli.mjs`, the command-line parser, and each tool's recorded command-line errors. +27. [`scripts/check_pdf_shims_equiv.mjs`](Tools-Toolchain-Tests#check-pdf-shims-equiv) --- verifies the book's pdf-lib shims write what stock pdf-lib writes, patch the members of pdf-lib it lists, have each of them in their shim's table of targets, and run. +28. [`scripts/check_impexp_parity.mjs`](Tools-Toolchain-Tests#check-impexp-parity) --- verifies the editions of the impexp tool pass the same built-in tests, and exit, print and write the same for one sequence of commands. Without Python it reports itself skipped and passes, except in CI. It compares the twinBASIC edition too, when `IMPEXP_EXE` names its built exe. +29. [`scripts/check_axe_patch_equiv.mjs`](Tools-Toolchain-Tests#check-axe-patch-equiv) --- verifies the vendored axe source patch still produces identical colour values. POSIX: @@ -167,6 +168,7 @@ POSIX: && node scripts/check_page_baseline.mjs \ && node scripts/check_book_coverage.mjs \ && node scripts/check_package_tests.mjs \ + && node scripts/check_errors.mjs \ && node scripts/check_symbol_index.mjs \ && node scripts/check_twin_parsers.mjs \ && node scripts/check_attribute_sweep.mjs \ @@ -177,9 +179,9 @@ POSIX: Exit codes: **0** every step passed; otherwise the code of the step that stopped the run, as that step's entry gives it. -**All but four cannot be affected by an edit confined to `docs/`**, which is why they are separate from `check.bat`. Run this one when the change touches `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `indexer/` or `test/`, the site's scripts in `docs/assets/js/`, the help add-in's source in `add-in/`, a contributed package's section, a wrapper, or a workflow. Both CI workflows run every step unconditionally. +**All but five cannot be affected by an edit confined to `docs/`**, which is why they are separate from `check.bat`. Run this one when the change touches `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `indexer/` or `test/`, the site's scripts in `docs/assets/js/`, the help add-in's source in `add-in/`, a contributed package's section, a wrapper, or a workflow. Both CI workflows run every step unconditionally. -The four exceptions are [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions), [`check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists), which reads this page, [`check_lint.mjs`](Tools-Toolchain-Tests#check-lint), which lints the site's scripts in `docs/assets/js/`, and [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests), which reads the callouts of the contributed packages' sections. +The five exceptions are [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions), [`check_errors.mjs`](Tools-Toolchain-Tests#check-errors), which compares the reference pages' Common Errors sections with their data, [`check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists), which reads this page, [`check_lint.mjs`](Tools-Toolchain-Tests#check-lint), which lints the site's scripts in `docs/assets/js/`, and [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests), which reads the callouts of the contributed packages' sections. `check_code_regions.mjs` has two halves. Its corpus sweep tokenises every markdown file under `docs/`, so a page that provokes a rewrite into altering a code region fails it. Its fixed probes run against their own sources whatever the tree holds, and cover the *mirror* fault, where a rewrite silently stops firing; the sweep cannot see that one. After adding a page with an unusual code construct, run `test.bat` and read the built page too. diff --git a/docs/IDE/AddIns/Help.md b/docs/IDE/AddIns/Help.md index df2576d43..cc0cc4b30 100644 --- a/docs/IDE/AddIns/Help.md +++ b/docs/IDE/AddIns/Help.md @@ -80,7 +80,7 @@ Without a **Description** attribute, the summary says *No description*. **Open i The gear opens the settings. A click anywhere else, or Esc, closes them. -![The top of the help pane with the settings open under the gear: the Hover help and All packages boxes, both unticked.](Images/Help_Settings.png){:width="480" height="150"} +![The top of the help pane with the settings open under the gear: four boxes. Hover help and All packages are unticked; Ctrl and Alt shortcuts of add-ins and Find in the shortcut editors are ticked.](Images/Help_Settings.png){:width="480" height="200"} **Hover help** : Adds the links described under [Hover help](#hover-help). Off by default. @@ -88,6 +88,12 @@ The gear opens the settings. A click anywhere else, or Esc, closes them. **All packages** : Offers the pages of every package this documentation covers. Off by default: F1, the hover links and the search then offer only the language itself and the packages the project references, so that a name from a package the project does not use never hides the one it does. With this off, F1 on a name that only an unreferenced package documents says *No help for 'Name' in the packages this project references*. +**Ctrl and Alt shortcuts of add-ins** +: Runs the keyboard shortcuts that other add-ins define with Ctrl or Alt, which the IDE on its own does not run: see [Other add-ins' shortcuts](#other-add-ins-shortcuts). On by default. + +**Find in the shortcut editors** +: Ctrl+F and F3 in the text of **Manage Keyboard Shortcuts** and **Manage Panel Layouts**: see [Finding text in the shortcut editors](#finding-text-in-the-shortcut-editors). On by default. + The settings, the last page shown and the help window's place are kept in the registry, under `HKEY_CURRENT_USER\Software\VB and VBA Program Settings\tbDocsHelp`. After the IDE restarts, the pane shows the page it showed last. ## The help window @@ -109,6 +115,28 @@ The gear The window is open again the next time the IDE starts, if it was open when the IDE closed. With **Show real OS titlebar** turned on in the IDE's options, the window has an ordinary Windows title bar instead, in the IDE's dark or light colours, and **Attach** and the gear move next to the search box. +## Other add-ins' shortcuts + +While this add-in is loaded, the keyboard shortcuts that other add-ins register with Ctrl or Alt work. Without it, the IDE does not run an add-in's shortcut that includes Ctrl or Alt: see [KeyboardShortcuts](../../Packages/tbIDE/KeyboardShortcuts). The IDE's own command on the same keys still runs as well. The setting **Ctrl and Alt shortcuts of add-ins** turns this off. + +## Finding text in the shortcut editors + +The text of **Window > Keyboard Shortcuts > Manage Keyboard Shortcuts**, and of **Manage Panel Layouts**, has no find of its own. While this add-in is loaded, these keys work there, as in Visual Studio: + +Ctrl+F +: Opens a find box over the text, holding the selected text if it is on one line. + +Enter, F3 +: Selects the next match. **Shift+Enter** and **Shift+F3** select the one before. F3 and Shift+F3 also work after the find box is closed, with the last text searched for. + +Ctrl+F3 +: Searches for the selected text, or for the word at the caret, without opening the find box. **Ctrl+Shift+F3** searches backwards. + +Esc +: Closes the find box. The dialog stays open. + +The search goes round past the end, and ignores case. The text scrolls to show each match. The setting **Find in the shortcut editors** turns this off. + ## Getting the add-in The add-in is a twinBASIC project, which the IDE builds into two DLLs, one for each bitness. @@ -119,6 +147,8 @@ The add-in is a twinBASIC project, which the IDE builds into two DLLs, one for e An add-in built this way serves the pages itself, from the copy of the documentation in its DLL, so the pane needs no internet connection. **Open in browser** still opens the page on the website, [docs.twinbasic.com](https://docs.twinbasic.com). +The add-in's version is in the DLL's file properties. `CHANGELOG.md`, in the project and in the source folder, lists what changed in each version. + ### From the source code The add-in's source code is in the `add-in` folder of the [documentation's repository](https://github.com/twinbasic/documentation). Build from it to change the add-in. The folder holds the project's files, not a project file, so pack it into one first with the [Import/Export Tool](../../../Features/Packages/Import-Export-Tool), from the repository's root folder: diff --git a/docs/IDE/AddIns/Images/Help_Settings.light.png b/docs/IDE/AddIns/Images/Help_Settings.light.png index 816af5bac..f95337e0a 100644 Binary files a/docs/IDE/AddIns/Images/Help_Settings.light.png and b/docs/IDE/AddIns/Images/Help_Settings.light.png differ diff --git a/docs/IDE/AddIns/Images/Help_Settings.light.svg b/docs/IDE/AddIns/Images/Help_Settings.light.svg index 3476fc998..a33f90952 100644 --- a/docs/IDE/AddIns/Images/Help_Settings.light.svg +++ b/docs/IDE/AddIns/Images/Help_Settings.light.svg @@ -1 +1 @@ -MainMiscellaneousPackagesReferencesResourcesSourcesInventory.twinTWINBASIC HELPOpen in browserDetachSearch the documentation/Packages/Default Packages/VBA Package/Interaction ModuleMsgBoxDisplays a message in a modal dialog with a chosen set ofMsgBoxHover helpAll packages +MainMiscellaneousPackagesReferencesResourcesSourcesInventory.twinTWINBASIC HELPOpen in browserDetachSearch the documentation/Packages/Default Packages/VBA Package/Interaction ModuleMsgBoxDisplays a message in a modal dialog with a chosen set ofbuttons, waits for the user to click a button, and returns aVbMsgBoxResult value identifying that button.MsgBoxHover helpAll packagesCtrl and Alt shortcuts of add-insFind in the shortcut editors diff --git a/docs/IDE/AddIns/Images/Help_Settings.png b/docs/IDE/AddIns/Images/Help_Settings.png index cdf34650b..6163b4fd3 100644 Binary files a/docs/IDE/AddIns/Images/Help_Settings.png and b/docs/IDE/AddIns/Images/Help_Settings.png differ diff --git a/docs/IDE/AddIns/Images/Help_Settings.svg b/docs/IDE/AddIns/Images/Help_Settings.svg index 4ab0dc108..eea9a2afb 100644 --- a/docs/IDE/AddIns/Images/Help_Settings.svg +++ b/docs/IDE/AddIns/Images/Help_Settings.svg @@ -1 +1 @@ -MainMiscellaneousPackagesReferencesResourcesSourcesInventory.twinTWINBASIC HELPOpen in browserDetachSearch the documentation/Packages/Default Packages/VBA Package/Interaction ModuleMsgBoxMsgBoxHover helpAll packages +MainMiscellaneousPackagesReferencesResourcesSourcesInventory.twinTWINBASIC HELPOpen in browserDetachSearch the documentation/Packages/Default Packages/VBA Package/Interaction ModuleMsgBoxDisplays a message in a modal dialog with a chosen set ofbuttons, waits for the user to click a button, and returns aVbMsgBoxResult value identifying that button.MsgBoxHover helpAll packagesCtrl and Alt shortcuts of add-insFind in the shortcut editors diff --git a/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md b/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md index 4b191a493..28e0cfee3 100644 --- a/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md +++ b/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md @@ -30,7 +30,7 @@ End Sub The shortcut fires when the key is released, wherever the focus is in the IDE's window --- in the code editor, in the DEBUG CONSOLE, or on nothing at all --- except while a dialog or the rename box is open. If the IDE has a command of its own on the same key, that command runs as well: F1, for one, also expands or collapses the signature help while it is showing. Shift+F12 has no command in the IDE's default keymap. > [!NOTE] BETA 1010 -> A shortcut that includes `{CTRL}` or `{ALT}` does not fire when the user presses it. The IDE matches an add-in's shortcut when the key is released, against a press of the same key less than half a second earlier, and it does not record a key pressed while Ctrl or Alt is held down. Such a shortcut fires only if the same key was also pressed on its own within that half second. Use a function key, alone or with `{SHIFT}`. +> A shortcut that includes `{CTRL}` or `{ALT}` does not fire when the user presses it. The IDE matches an add-in's shortcut when the key is released, against a press of the same key less than half a second earlier, and it does not record a key pressed while Ctrl or Alt is held down. Such a shortcut fires only if the same key was also pressed on its own within that half second. Use a function key, alone or with `{SHIFT}`. While the [Help Add-In](../../IDE/AddIns/Help) is loaded, shortcuts with `{CTRL}` and `{ALT}` fire as the others do, for every add-in. ## Methods diff --git a/docs/Reference/Core/ReDim.md b/docs/Reference/Core/ReDim.md index d5e8e9170..9bc714c02 100644 --- a/docs/Reference/Core/ReDim.md +++ b/docs/Reference/Core/ReDim.md @@ -29,7 +29,7 @@ The **ReDim** statement is used to size or resize a dynamic array that has alrea Use the **ReDim** statement repeatedly to change the number of elements and dimensions in an array. However, an array declared with one data type cannot later be changed to another data type by **ReDim**, unless the array is contained in a **Variant**. If the array is contained in a **Variant**, the type of the elements can be changed by using an **As** *type* clause, unless the **Preserve** keyword is used, in which case no changes of data type are permitted. -**ReDim** also works on a **Variant** that holds a value other than an array, or nothing at all: the value is discarded and the **Variant** then holds the new array. **ReDim Preserve** on a **Variant** that holds a value other than an array raises error 13 (*Type mismatch*). +**ReDim** also works on a **Variant** that holds a value other than an array, or nothing at all: the value is discarded and the **Variant** then holds the new array. With the **Preserve** keyword, only the last array dimension can be resized, and the number of dimensions cannot change. For example, when the array has only one dimension, that dimension can be resized because it is the last and only dimension. However, when the array has two or more dimensions, only the size of the last dimension can change while still preserving the contents of the array. @@ -42,13 +42,10 @@ ReDim X(10, 10, 10) ReDim Preserve X(10, 10, 15) ``` -Similarly, with **Preserve**, the size of the array can only change by changing the upper bound; changing the lower bound causes an error. +Similarly, with **Preserve**, the size of the array can only change by changing the upper bound. When an array becomes smaller than it was, data in the eliminated elements will be lost. -> [!NOTE] BETA 1010 -> An upper bound below the lower bound, as in `ReDim X(0 To -1)`, raises error -2147467259, *Unspecified error*. VB6 raises error 9, *Subscript out of range*. - When variables are initialized, a numeric variable is initialized to 0, a variable-length string is initialized to a zero-length string (""), and a fixed-length string is filled with zeros. **Variant** variables are initialized to **Empty**. Each element of a user-defined type variable is initialized as if it were a separate variable. A variable that refers to an object must be assigned an existing object by using the [**Set**](Set) statement before it can be used. Until it is assigned an object, the declared object variable has the special value **Nothing**, which indicates that it doesn't refer to any particular instance of an object. @@ -58,6 +55,20 @@ The **ReDim** statement acts as a declarative statement if the variable it decla > [!NOTE] > To resize an array contained in a **Variant**, the **Variant** variable must be explicitly declared before attempting to resize its array. + +### Common Errors + +| Error | When | +|---|---| +| [-2147467259 (`&H80004005`)](tb:Errors#-2147467259), *Unspecified error* | an upper bound is less than its lower bound | +| [9 (`&H9`)](tb:Errors#9), *Subscript out of range* | **Preserve** changes a lower bound, or the bounds of any dimension but the last | +| [13 (`&HD`)](tb:Errors#13), *Type mismatch* | **Preserve** is used on a **Variant** that does not hold an array | + +> [!NOTE] BETA 1010 +> When an upper bound is less than its lower bound, VB6 raises error [9 (`&H9`)](tb:Errors#9), *Subscript out of range*, instead. + + + ### Example This example uses the **ReDim** statement to allocate and reallocate storage space for dynamic-array variables. It assumes the **Option Base** is **1**. diff --git a/docs/Reference/Default/VBA/Strings/Asc.md b/docs/Reference/Default/VBA/Strings/Asc.md index ed9592c63..cb6cdb747 100644 --- a/docs/Reference/Default/VBA/Strings/Asc.md +++ b/docs/Reference/Default/VBA/Strings/Asc.md @@ -16,7 +16,7 @@ Syntax: - **AscW(** *string* **)** *string* -: *required* Any valid string expression. If *string* contains no characters, a run-time error occurs. +: *required* Any valid string expression. The range for returns from **Asc** is 0--255 on non-DBCS systems, but -32768--32767 on DBCS systems. @@ -25,6 +25,16 @@ The range for returns from **Asc** is 0--255 on non-DBCS systems, but -32768--32 The functions [**Chr**, **ChrB**, and **ChrW**](Chr) are the opposite of **Asc**, **AscB**, and **AscW**. The **Chr** functions convert an integer to a character string. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *string* is zero-length | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *string* is **Null** | + + + ### Example This example uses the **Asc** function to return a character code corresponding to the first letter in the string. diff --git a/docs/Reference/Default/VBA/Strings/Chr.md b/docs/Reference/Default/VBA/Strings/Chr.md index 26b17f13f..38b8a918c 100644 --- a/docs/Reference/Default/VBA/Strings/Chr.md +++ b/docs/Reference/Default/VBA/Strings/Chr.md @@ -29,6 +29,17 @@ Numbers from 0--31 are the same as standard, nonprintable ASCII codes. For examp The functions [**Asc**, **AscB**, and **AscW**](Asc) are the opposite of **Chr**, **ChrB**, and **ChrW**. The **Asc** functions convert a string to an integer. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | **Chr** or **Chr$** is given a *charcode* less than 0 or greater than 255
**ChrW** or **ChrW$** is given a *charcode* less than -32,768 or greater than 65,535 | +| [6 (`&H6`)](tb:Errors#6), *Overflow* | **ChrB** or **ChrB$** is given a *charcode* less than 0 or greater than 255 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *charcode* is **Null** | + + + ### Example This example uses the **Chr** function to return the character associated with the specified character code. diff --git a/docs/Reference/Default/VBA/Strings/Filter.md b/docs/Reference/Default/VBA/Strings/Filter.md index e9d0003e5..d37994c8b 100644 --- a/docs/Reference/Default/VBA/Strings/Filter.md +++ b/docs/Reference/Default/VBA/Strings/Filter.md @@ -14,6 +14,9 @@ Syntax: **Filter(** *sourcearray*, *match* [ **,** *include* [ **,** *compare* ] *sourcearray* : *required* One-dimensional array of strings to be searched. +> [!NOTE] +> twinBASIC also takes a *sourcearray* whose elements are of a type other than **String** or **Variant**, such as **Long** or **Double**. For a **Long** array VB6 raises error 13, *Type mismatch*. + *match* : *required* String to search for. @@ -27,12 +30,29 @@ The *compare* argument can have the following values: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the [**Option Compare**](../../Core/Option) statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + The array returned by the **Filter** function contains only enough elements to contain the number of matched items. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [13 (`&HD`)](tb:Errors#13), *Type mismatch* | *sourcearray* is **Null**, or has more than one dimension | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *match*, *include* or *compare* is **Null**, or an element of *sourcearray* is **Null** | + +> [!NOTE] BETA 1010 +> When *compare* is 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument*; twinBASIC raises none. + + + ### See Also - [Join](Join), [Split](Split) functions diff --git a/docs/Reference/Default/VBA/Strings/Format.md b/docs/Reference/Default/VBA/Strings/Format.md index c3316281f..e1ab23d2a 100644 --- a/docs/Reference/Default/VBA/Strings/Format.md +++ b/docs/Reference/Default/VBA/Strings/Format.md @@ -97,6 +97,16 @@ If the calendar is Gregorian, the meaning of *format* expression symbols is unch | *s* | 0-59 (Second of minute, with no leading zero) | | *ss* | 00-59 (Second of minute, with a leading zero) | + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *firstDayOfWeek* is less than 0 or greater than 7, or *firstWeekOfYear* is less than 0 or greater than 3 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *format*, *firstDayOfWeek* or *firstWeekOfYear* is **Null** | + + + ### Example This example shows various uses of the **Format** function to format values using both named formats and user-defined formats. For the date separator (`/`), time separator (`:`), and AM/PM literal, the actual formatted output displayed by the system depends on the locale settings on which the code is running. When times and dates are displayed in the development environment, the short time format and short date format of the code locale are used. When displayed by running code, the short time format and short date format of the system locale are used, which may differ from the code locale. For this example, English/U.S. is assumed. `MyTime` and `MyDate` are displayed in the development environment using the current system short time setting and short date setting. diff --git a/docs/Reference/Default/VBA/Strings/FormatCurrency.md b/docs/Reference/Default/VBA/Strings/FormatCurrency.md index f462c42f3..213e15c00 100644 --- a/docs/Reference/Default/VBA/Strings/FormatCurrency.md +++ b/docs/Reference/Default/VBA/Strings/FormatCurrency.md @@ -36,6 +36,16 @@ The *includeLeadingDigit*, *useParensForNegativeNumbers*, and *groupDigits* argu When one or more optional arguments are omitted, the values for omitted arguments are provided by the computer's regional settings. The position of the currency symbol relative to the currency value is determined by the system's regional settings. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1 or greater than 255
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | an argument other than *expression* is **Null** | + + + ### See Also - [Format](Format), [FormatNumber](FormatNumber), [FormatPercent](FormatPercent) functions diff --git a/docs/Reference/Default/VBA/Strings/FormatDateTime.md b/docs/Reference/Default/VBA/Strings/FormatDateTime.md index eed19a4d4..bfbb46875 100644 --- a/docs/Reference/Default/VBA/Strings/FormatDateTime.md +++ b/docs/Reference/Default/VBA/Strings/FormatDateTime.md @@ -27,6 +27,16 @@ The *namedFormat* argument has the following settings: | **vbLongTime** | 3 | Display a time by using the time format specified in the system regional settings. | | **vbShortTime** | 4 | Display a time by using the 24-hour format (`hh:mm`). | + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *namedFormat* is less than 0 or greater than 4 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *namedFormat* is **Null** | + + + ### Example This example uses **FormatDateTime** to display a date value in several formats. diff --git a/docs/Reference/Default/VBA/Strings/FormatNumber.md b/docs/Reference/Default/VBA/Strings/FormatNumber.md index 47bd012b9..c6fa1d98c 100644 --- a/docs/Reference/Default/VBA/Strings/FormatNumber.md +++ b/docs/Reference/Default/VBA/Strings/FormatNumber.md @@ -36,6 +36,16 @@ The *includeLeadingDigit*, *useParensForNegativeNumbers*, and *groupDigits* argu When one or more optional arguments are omitted, the values for omitted arguments are provided by the computer's regional settings. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | an argument other than *expression* is **Null** | + + + ### See Also - [Format](Format), [FormatCurrency](FormatCurrency), [FormatPercent](FormatPercent) functions diff --git a/docs/Reference/Default/VBA/Strings/FormatPercent.md b/docs/Reference/Default/VBA/Strings/FormatPercent.md index eec9109c3..c5a2776e0 100644 --- a/docs/Reference/Default/VBA/Strings/FormatPercent.md +++ b/docs/Reference/Default/VBA/Strings/FormatPercent.md @@ -36,6 +36,16 @@ The *includeLeadingDigit*, *useParensForNegativeNumbers*, and *groupDigits* argu When one or more optional arguments are omitted, the values for omitted arguments are provided by the computer's regional settings. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | an argument other than *expression* is **Null** | + + + ### See Also - [Format](Format), [FormatCurrency](FormatCurrency), [FormatNumber](FormatNumber) functions diff --git a/docs/Reference/Default/VBA/Strings/InStr.md b/docs/Reference/Default/VBA/Strings/InStr.md index 5654466a1..7b230541f 100644 --- a/docs/Reference/Default/VBA/Strings/InStr.md +++ b/docs/Reference/Default/VBA/Strings/InStr.md @@ -15,7 +15,7 @@ Syntax: - **InStrB(** [ *start* **,** ] *string1*, *string2* [ **,** *compare* ] **)** *start* -: *optional* Numeric expression that sets the starting position for each search. If omitted, search begins at the first character position. *start* must be 1 or greater: 0 or a negative value raises error 5 (Invalid procedure call or argument), and **Null** raises error 94 (Invalid use of Null). The *start* argument is required if *compare* is specified. +: *optional* Numeric expression that sets the starting position for each search. If omitted, search begins at the first character position. *start* must be 1 or greater. The *start* argument is required if *compare* is specified. *string1* : *required* String expression being searched. @@ -24,16 +24,19 @@ Syntax: : *required* String expression sought. *compare* -: *optional* Specifies the type of string comparison. If *compare* is **Null**, an error occurs. If *compare* is omitted, the [**Option Compare**](../../Core/Option) setting determines the type of comparison. Specify a valid LCID (LocaleID) to use locale-specific rules in the comparison. +: *optional* Specifies the type of string comparison. If *compare* is omitted, the [**Option Compare**](../../Core/Option) setting determines the type of comparison. Specify a valid LCID (LocaleID) to use locale-specific rules in the comparison. The *compare* argument settings are: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the **Option Compare** statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + **Return values:** the first row that applies gives the result. | If | **InStr** returns | @@ -48,6 +51,22 @@ The *compare* argument settings are: The **InStrB** function is used with byte data contained in a string. Instead of returning the character position of the first occurrence of one string within another, **InStrB** returns the byte position. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *start* is less than 1
**InStr** is given a *start* greater than 1,073,741,824
**InStr** is given a *compare* that is less than 0, or greater than 1 and not a valid locale identifier
**InStr** is given a *compare* that is not a valid locale identifier, and *string1* or *string2* is zero-length | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *start* or *compare* is **Null** | + +> [!NOTE] BETA 1010 +> When **InStr** is given a *compare* that is not a valid locale identifier, and *string1* or *string2* is zero-length, VB6 raises no error. + +> [!NOTE] BETA 1010 +> When **InStr** is given a *compare* of 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument*; twinBASIC raises none. + + + ### Example This example uses the **InStr** function to return the position of the first occurrence of one string within another. diff --git a/docs/Reference/Default/VBA/Strings/InStrRev.md b/docs/Reference/Default/VBA/Strings/InStrRev.md index 3aee7a6c2..7c568ef2f 100644 --- a/docs/Reference/Default/VBA/Strings/InStrRev.md +++ b/docs/Reference/Default/VBA/Strings/InStrRev.md @@ -18,7 +18,7 @@ Syntax: **InStrRev(** *stringcheck*, *stringmatch* [ **,** *start* [ **,** *comp : *required* String expression being searched for. *start* -: *optional* Numeric expression that sets the starting position for each search. If omitted, -1 is used, which means that the search begins at the last character position. *start* must be -1, or 1 or greater: 0 or any other negative value raises error 5 (Invalid procedure call or argument). If *start* contains **Null**, an error occurs. +: *optional* Numeric expression that sets the starting position for each search. If omitted, -1 is used, which means that the search begins at the last character position. *start* must be -1, or 1 or greater. *compare* : *optional* Numeric value indicating the kind of comparison to use when evaluating substrings. If omitted, a binary comparison is performed. See settings below. @@ -27,10 +27,13 @@ The *compare* argument can have the following values: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the [**Option Compare**](../../Core/Option) statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + **Return values:** the first row that applies gives the result. | If | **InStrRev** returns | @@ -41,13 +44,23 @@ The *compare* argument can have the following values: | *stringmatch* is not found | 0 | | *stringmatch* is found within *stringcheck* | Position at which match is found | -If *stringcheck* or *stringmatch* is **Null**, error 94 (Invalid use of Null) occurs. Unlike **InStr**, **InStrRev** does not return **Null**. +Unlike **InStr**, **InStrRev** does not return **Null**: a **Null** argument is an error. > [!NOTE] > The syntax for the **InStrRev** function is not the same as the syntax for the [**InStr**](InStr) function --- note the swapped order of the search arguments. **InStrRev** will not find an instance of *stringmatch* unless the position of the end character of *stringmatch* is less than or equal to *start*. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *start* is 0, or less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | any argument is **Null** | + + + ### Example This example uses **InStrRev** to find the last occurrence of a substring. diff --git a/docs/Reference/Default/VBA/Strings/Join.md b/docs/Reference/Default/VBA/Strings/Join.md index ef273f311..7e5791e73 100644 --- a/docs/Reference/Default/VBA/Strings/Join.md +++ b/docs/Reference/Default/VBA/Strings/Join.md @@ -17,6 +17,20 @@ Syntax: **Join(** *sourcearray* [ **,** *delimiter* ] **)** *delimiter* : *optional* String character used to separate the substrings in the returned string. If omitted, the space character (`" "`) is used. If *delimiter* is a zero-length string (`""`), all items in the list are concatenated with no delimiters. +> [!NOTE] +> twinBASIC also takes a *sourcearray* whose elements are of a type other than **String** or **Variant**, such as **Long** or **Double**. For a **Long** array VB6 raises error 5, *Invalid procedure call or argument*. + + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *sourcearray* has more than one dimension | +| [13 (`&HD`)](tb:Errors#13), *Type mismatch* | *sourcearray* is **Null**, or an element of it is an array | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *delimiter* is **Null**, or an element of *sourcearray* is **Null** | + + + ### Example This example uses **Join** to concatenate an array of strings with a delimiter. diff --git a/docs/Reference/Default/VBA/Strings/LCase.md b/docs/Reference/Default/VBA/Strings/LCase.md index e2c09eb58..3bb378926 100644 --- a/docs/Reference/Default/VBA/Strings/LCase.md +++ b/docs/Reference/Default/VBA/Strings/LCase.md @@ -18,6 +18,15 @@ The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Vari Only uppercase letters are converted to lowercase; all lowercase letters and nonletter characters remain unchanged. + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **LCase$** is given a *string* that is **Null** | + + + ### Example This example uses the **LCase** function to return a lowercase version of a string. diff --git a/docs/Reference/Default/VBA/Strings/LTrim.md b/docs/Reference/Default/VBA/Strings/LTrim.md index 7ec27124f..875f61573 100644 --- a/docs/Reference/Default/VBA/Strings/LTrim.md +++ b/docs/Reference/Default/VBA/Strings/LTrim.md @@ -16,6 +16,15 @@ Syntax: **LTrim$(** *string* **)**, **LTrim(** *string* **)** The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Variant** (**String**). + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **LTrim$** is given a *string* that is **Null** | + + + ### Example This example uses the **LTrim** function to strip leading spaces from a string variable. diff --git a/docs/Reference/Default/VBA/Strings/Left.md b/docs/Reference/Default/VBA/Strings/Left.md index 0db760f65..4ef70fc27 100644 --- a/docs/Reference/Default/VBA/Strings/Left.md +++ b/docs/Reference/Default/VBA/Strings/Left.md @@ -27,6 +27,16 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi > [!NOTE] > Use the **LeftB** function with byte data contained in a string. Instead of specifying the number of characters to return, *length* specifies the number of bytes. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *length* is negative
**Left** or **Left$** is given a *length* greater than 1,073,741,823 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **Left$** or **LeftB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | + + + ### Example This example uses the **Left** function to return a specified number of characters from the left side of a string. diff --git a/docs/Reference/Default/VBA/Strings/Mid.md b/docs/Reference/Default/VBA/Strings/Mid.md index 5e2ad823e..8b4ea13d0 100644 --- a/docs/Reference/Default/VBA/Strings/Mid.md +++ b/docs/Reference/Default/VBA/Strings/Mid.md @@ -33,6 +33,16 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi > [!TIP] > Use the [**Mid =**](../../Core/Mid-equals) statement to replace characters within a string. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative
**Mid** or **Mid$** is given a *start* greater than 1,073,741,824 or a *length* greater than 1,073,741,823 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *start* or *length* that is **Null** | + + + ### Example This example uses the **Mid** function to return a specified number of characters from a string. diff --git a/docs/Reference/Default/VBA/Strings/MonthName.md b/docs/Reference/Default/VBA/Strings/MonthName.md index f4e4b0919..fff7091c6 100644 --- a/docs/Reference/Default/VBA/Strings/MonthName.md +++ b/docs/Reference/Default/VBA/Strings/MonthName.md @@ -17,6 +17,16 @@ Syntax: **MonthName(** *month* [ **,** *abbreviate* ] **)** *abbreviate* : *optional* **Boolean** value that indicates if the month name is to be abbreviated. If omitted, the default is **False**, which means that the month name is not abbreviated. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *month* is less than 1 or greater than 12 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *month* or *abbreviate* is **Null** | + + + ### Example This example uses **MonthName** to return the full and abbreviated name of a month. diff --git a/docs/Reference/Default/VBA/Strings/RTrim.md b/docs/Reference/Default/VBA/Strings/RTrim.md index a61ef03ef..8fa757d5a 100644 --- a/docs/Reference/Default/VBA/Strings/RTrim.md +++ b/docs/Reference/Default/VBA/Strings/RTrim.md @@ -16,6 +16,15 @@ Syntax: **RTrim$(** *string* **)**, **RTrim(** *string* **)** The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Variant** (**String**). + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **RTrim$** is given a *string* that is **Null** | + + + ### Example This example uses the **RTrim** function to strip trailing spaces from a string variable. diff --git a/docs/Reference/Default/VBA/Strings/Replace.md b/docs/Reference/Default/VBA/Strings/Replace.md index 51d995989..9bcca4c66 100644 --- a/docs/Reference/Default/VBA/Strings/Replace.md +++ b/docs/Reference/Default/VBA/Strings/Replace.md @@ -33,16 +33,18 @@ The *compare* argument can have the following values: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the [**Option Compare**](../../Core/Option) statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + **Return values:** | If | **Replace** returns | |------------------------------------------|----------------------------------------------------------------------------------------------------| | *expression* is zero-length | Zero-length string (`""`) | -| *expression* is **Null** | An error. | | *find* is zero-length | Copy of *expression*. | | *replace* is zero-length | Copy of *expression* with all occurrences of *find* removed. | | *start* > **Len**(*expression*) | Zero-length string. String replacement begins at the position indicated by *start*. | @@ -50,6 +52,16 @@ The *compare* argument can have the following values: The return value of the **Replace** function is a string, with substitutions made, that begins at the position specified by *start* and concludes at the end of the *expression* string. It's not a copy of the original string from start to finish. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *start* is less than 1, or *count* is less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | any argument is **Null** | + + + ### See Also - [InStr](InStr), [StrComp](StrComp) functions diff --git a/docs/Reference/Default/VBA/Strings/Right.md b/docs/Reference/Default/VBA/Strings/Right.md index d088f2302..e64c6f0fe 100644 --- a/docs/Reference/Default/VBA/Strings/Right.md +++ b/docs/Reference/Default/VBA/Strings/Right.md @@ -27,6 +27,16 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi > [!NOTE] > Use the **RightB** function with byte data contained in a string. Instead of specifying the number of characters to return, *length* specifies the number of bytes. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *length* is negative
**Right** or **Right$** is given a *length* greater than 1,073,741,823 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **Right$** or **RightB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | + + + ### Example This example uses the **Right** function to return a specified number of characters from the right side of a string. diff --git a/docs/Reference/Default/VBA/Strings/Space.md b/docs/Reference/Default/VBA/Strings/Space.md index 02a165a05..8d392ad65 100644 --- a/docs/Reference/Default/VBA/Strings/Space.md +++ b/docs/Reference/Default/VBA/Strings/Space.md @@ -18,6 +18,16 @@ The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Vari The **Space** function is useful for formatting output and clearing data in fixed-length strings. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *number* is negative, or greater than 1,073,741,823 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *number* is **Null** | + + + ### Example This example uses the **Space** function to return a string consisting of a specified number of spaces. diff --git a/docs/Reference/Default/VBA/Strings/Split.md b/docs/Reference/Default/VBA/Strings/Split.md index ae809c8ac..51a2225dc 100644 --- a/docs/Reference/Default/VBA/Strings/Split.md +++ b/docs/Reference/Default/VBA/Strings/Split.md @@ -27,10 +27,23 @@ The *compare* argument can have the following values: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the [**Option Compare**](../../Core/Option) statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *limit* is less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | any argument is **Null** | + + + ### Example This example shows how to use the **Split** function. diff --git a/docs/Reference/Default/VBA/Strings/StrComp.md b/docs/Reference/Default/VBA/Strings/StrComp.md index 90af8536a..be61eda11 100644 --- a/docs/Reference/Default/VBA/Strings/StrComp.md +++ b/docs/Reference/Default/VBA/Strings/StrComp.md @@ -18,16 +18,19 @@ Syntax: **StrComp(** *string1*, *string2* [ **,** *compare* ] **)** : *required* Any valid string expression. *compare* -: *optional* Specifies the type of string comparison. If the *compare* argument is **Null**, an error occurs. If *compare* is omitted, the [**Option Compare**](../../Core/Option) setting determines the type of comparison. +: *optional* Specifies the type of string comparison. If *compare* is omitted, the [**Option Compare**](../../Core/Option) setting determines the type of comparison. The *compare* argument settings are: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the **Option Compare** statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + **Return values:** | If | **StrComp** returns | @@ -37,6 +40,19 @@ The *compare* argument settings are: | *string1* is greater than *string2* | 1 | | *string1* or *string2* is **Null** | **Null** | + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *compare* is **Null** | + +> [!NOTE] BETA 1010 +> When *compare* is 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument*; twinBASIC raises none. + + + ### Example This example uses the **StrComp** function to return the results of a string comparison. If the third argument is 1, a textual comparison is performed; if the third argument is 0 or omitted, a binary comparison is performed. diff --git a/docs/Reference/Default/VBA/Strings/StrConv.md b/docs/Reference/Default/VBA/Strings/StrConv.md index 7bfc724b8..93a25f2db 100644 --- a/docs/Reference/Default/VBA/Strings/StrConv.md +++ b/docs/Reference/Default/VBA/Strings/StrConv.md @@ -18,7 +18,7 @@ Syntax: **StrConv(** *string*, *conversion* [ **,** *LCID* ] **)** : *required* **Integer**. The sum of values specifying the type of conversion to perform. *LCID* -: *optional* The LocaleID, if different than the system LocaleID. (The system LocaleID is the default.) An *LCID* that is not a valid locale identifier raises error 5. +: *optional* The LocaleID, if different than the system LocaleID. (The system LocaleID is the default.) The *conversion* argument settings are: @@ -37,7 +37,7 @@ The *conversion* argument settings are: | **vbFromUTF8** | 512 | Converts UTF-8 text held in a **Byte** array to a string. An invalid byte sequence becomes the replacement character U+FFFD. | > [!NOTE] -> These constants are specified by twinBASIC. As a result, they may be used anywhere in code in place of the actual values. Most can be combined, for example, **vbUpperCase + vbWide**, except when they are mutually exclusive, for example, **vbUnicode + vbFromUnicode**. The constants **vbWide**, **vbNarrow**, **vbKatakana**, and **vbHiragana** cause run-time errors when used in locales where they don't apply. +> These constants are specified by twinBASIC. As a result, they may be used anywhere in code in place of the actual values. Most can be combined, for example, **vbUpperCase + vbWide**, except when they are mutually exclusive, for example, **vbUnicode + vbFromUnicode**. The following are valid word separators for proper casing: **Null** (`Chr$(0)`), horizontal tab (`Chr$(9)`), linefeed (`Chr$(10)`), vertical tab (`Chr$(11)`), form feed (`Chr$(12)`), carriage return (`Chr$(13)`), space (SBCS) (`Chr$(32)`). The actual value for a space varies by country/region for DBCS. No other character, such as a hyphen or an apostrophe, starts a word: `"mc-donald"` becomes `"Mc-donald"`. Every word is capitalised, whatever the number of separators before it: `"a b"` (two spaces) becomes `"A B"`, and `"a" & vbCrLf & "b"` becomes `"A" & vbCrLf & "B"`. @@ -45,10 +45,23 @@ If *string* is **Null**, **StrConv** returns **Null**. The case conversions map each character by the Windows rules for the character itself, and do not apply the language-specific rules of *LCID*. With an *LCID* of 1055 (Turkish), `"i"` becomes `"I"`, not the dotted capital I. The case conversions also work on characters outside the system code page, such as Greek and Cyrillic letters on a system with a Western European code page. -**vbUnicode** and **vbFromUnicode** use the default code page of the system, whatever *LCID* is given. **vbWide**, **vbNarrow**, **vbKatakana** and **vbHiragana** depend on the system locale as well: on a system whose locale is not East Asian they raise error 5 even when *LCID* names a Japanese locale. Visual Basic 6 applies the code page and locale of *LCID* to all six. +**vbUnicode** and **vbFromUnicode** use the default code page of the system, whatever *LCID* is given. **vbWide**, **vbNarrow**, **vbKatakana** and **vbHiragana** depend on the system locale in the same way. Visual Basic 6 applies the code page and locale of *LCID* to all six. When converting from a **Byte** array in ANSI format to a string, use the **StrConv** function. When converting from such an array in Unicode format, use an assignment statement. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *conversion* is a value the table does not list, or combines **vbUnicode**, **vbFromUnicode**, **vbUTF8** or **vbFromUTF8** with another value
*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** on a system whose locale is not East Asian
*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** and *LCID* names an East Asian locale, on a system whose locale is not East Asian
*LCID* is not a valid locale identifier | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *conversion* or *LCID* is **Null** | + +> [!NOTE] BETA 1010 +> When *conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** and *LCID* names an East Asian locale, on a system whose locale is not East Asian, VB6 raises no error. + + + ### Example This example uses the **StrConv** function to convert a Unicode string to an ANSI string. diff --git a/docs/Reference/Default/VBA/Strings/StrReverse.md b/docs/Reference/Default/VBA/Strings/StrReverse.md index 15a2be0df..21dfaaee1 100644 --- a/docs/Reference/Default/VBA/Strings/StrReverse.md +++ b/docs/Reference/Default/VBA/Strings/StrReverse.md @@ -12,7 +12,16 @@ Returns a string in which the character order of a specified string is reversed. Syntax: **StrReverse(** *expression* **)** *expression* -: *required* The string whose characters are to be reversed. If *expression* is a zero-length string (`""`), a zero-length string is returned. If *expression* is **Null**, an error occurs. +: *required* The string whose characters are to be reversed. If *expression* is a zero-length string (`""`), a zero-length string is returned. + + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *expression* is **Null** | + + ### Example diff --git a/docs/Reference/Default/VBA/Strings/String.md b/docs/Reference/Default/VBA/Strings/String.md index a2b9c164a..448dda40d 100644 --- a/docs/Reference/Default/VBA/Strings/String.md +++ b/docs/Reference/Default/VBA/Strings/String.md @@ -12,15 +12,28 @@ Returns a **String** containing a repeating character string of the length speci Syntax: **String$(** *number*, *character* **)**, **String(** *number*, *character* **)** *number* -: *required* **Long**. Length of the returned string. If *number* contains **Null**, **Null** is returned. +: *required* **Long**. Length of the returned string. *character* -: *required* **Variant**. Character code specifying the character or string expression whose first character is used to build the return string. If *character* contains **Null**, **Null** is returned. +: *required* **Variant**. Character code specifying the character or string expression whose first character is used to build the return string. The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Variant** (**String**). When *character* is a number greater than 255, **String** converts the number to a valid character code by using this formula: *character* **Mod** 256. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *number* is negative, or greater than 1,073,741,823
*character* is a zero-length string | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *number* is **Null**, or **String$** is given a *character* that is **Null**
**String** is given a *character* that is **Null** | + +> [!NOTE] BETA 1010 +> When **String** is given a *character* that is **Null**, VB6 raises no error. + + + ### Example This example uses the **String** function to return repeating character strings of the length specified. diff --git a/docs/Reference/Default/VBA/Strings/Trim.md b/docs/Reference/Default/VBA/Strings/Trim.md index a45d39cf5..1909d8451 100644 --- a/docs/Reference/Default/VBA/Strings/Trim.md +++ b/docs/Reference/Default/VBA/Strings/Trim.md @@ -16,6 +16,15 @@ Syntax: **Trim$(** *string* **)**, **Trim(** *string* **)** The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Variant** (**String**). + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **Trim$** is given a *string* that is **Null** | + + + ### Example This example uses the **Trim** function to strip both leading and trailing spaces from a string variable. diff --git a/docs/Reference/Default/VBA/Strings/UCase.md b/docs/Reference/Default/VBA/Strings/UCase.md index a8ed5334b..12acf0850 100644 --- a/docs/Reference/Default/VBA/Strings/UCase.md +++ b/docs/Reference/Default/VBA/Strings/UCase.md @@ -18,6 +18,15 @@ The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Vari Only lowercase letters are converted to uppercase; all uppercase letters and nonletter characters remain unchanged. + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **UCase$** is given a *string* that is **Null** | + + + ### Example This example uses the **UCase** function to return an uppercase version of a string. diff --git a/docs/Reference/Default/VBA/Strings/WeekdayName.md b/docs/Reference/Default/VBA/Strings/WeekdayName.md index c185066ca..37888516c 100644 --- a/docs/Reference/Default/VBA/Strings/WeekdayName.md +++ b/docs/Reference/Default/VBA/Strings/WeekdayName.md @@ -33,6 +33,16 @@ The *firstdayofweek* argument can have the following values: | **vbFriday** | 6 | Friday | | **vbSaturday** | 7 | Saturday | + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *weekday* is less than 1 or greater than 7, or *firstdayofweek* is less than 0 or greater than 7 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | any argument is **Null** | + + + ### Example This example uses **WeekdayName** to return the full and abbreviated name of a weekday. diff --git a/docs/Reference/Default/VBA/Strings/index.md b/docs/Reference/Default/VBA/Strings/index.md index bba5b594d..e8e6c956f 100644 --- a/docs/Reference/Default/VBA/Strings/index.md +++ b/docs/Reference/Default/VBA/Strings/index.md @@ -21,7 +21,7 @@ Debug.Print Chr(65) ' "A" ## Searching and comparing -[**StrComp**](StrComp) compares two strings and returns -1, 0, or 1 to report which is greater (or equal). [**InStr**](InStr) and [**InStrRev**](InStrRev) return the position of one string inside another, scanning forward from a chosen start position or backward from one. All three accept an optional *compare* argument controlling whether the comparison is case-sensitive (**vbBinaryCompare**), case-insensitive (**vbTextCompare**), or governed by the surrounding [**Option Compare**](../../Core/Option) setting (**vbUseCompareOption**). Note that **InStrRev** swaps the order of the haystack and needle arguments relative to **InStr**. +[**StrComp**](StrComp) compares two strings and returns -1, 0, or 1 to report which is greater (or equal). [**InStr**](InStr) and [**InStrRev**](InStrRev) return the position of one string inside another, scanning forward from a chosen start position or backward from one. All three accept an optional *compare* argument controlling whether the comparison is case-sensitive (**vbBinaryCompare**), or case-insensitive (**vbTextCompare**); when it is omitted, **StrComp** and **InStr** follow the surrounding [**Option Compare**](../../Core/Option) setting. Note that **InStrRev** swaps the order of the haystack and needle arguments relative to **InStr**. ```tb check_build Debug.Print InStr("Hello, world", "o") ' 5 (first match, forward) diff --git a/docs/Reference/Run-time-Errors.md b/docs/Reference/Run-time-Errors.md new file mode 100644 index 000000000..bb65cf91c --- /dev/null +++ b/docs/Reference/Run-time-Errors.md @@ -0,0 +1,155 @@ +--- +title: Run-time Errors +parent: Reference Section +nav_order: 12 +permalink: /Reference/Run-time-Errors +shortcut: Errors +index_also: trappable errors, error numbers, error messages +--- + +# Run-time Errors +{: .no_toc } + +Every run-time error number that has a message of its own, with that message, and the reference pages that list each number among the errors their procedure raises. + +A run-time error can be trapped: [**On Error**](../tB/Core/On-Error) sends it to a handler, which reads its number from [**Err.Number**](../tB/Modules/ErrObject/Number) and its message from [**Err.Description**](../tB/Modules/ErrObject/Description). A reference page that documents a procedure, a statement or an operator has a **Common Errors** section when it raises errors of its own: a table of each number it raises, and when. Each number there links to its row below. + +## What the Common Errors sections list + +A section lists the errors that a procedure raises from its own arguments and its own state, as twinBASIC raises them. Where VB6 raises another error, or none, a note under the table says so. Every row is measured in twinBASIC and in VB6, and not copied from another reference. + +A section does not list: + +- errors that any code can raise: 7, *Out of memory*, and 28, *Out of stack space*; +- errors in converting an argument to the type its page gives for the parameter: 13, *Type mismatch*, for an argument of the wrong kind, such as `Mid("abc", "x")`, and 6, *Overflow*, for a number outside the range of that type, such as a *start* of 2147483648 for **Mid**, whose *start* is a **Long**. A **Null** argument is listed, whether the procedure raises the error itself or in converting the argument: `Mid("abc", Null)` raises 94, *Invalid use of Null*; +- compile-time errors: the twinBASIC compiler reports these with diagnostic codes such as `TB5001`, before the program runs, and **On Error** cannot trap them. + +A procedure that raises no error of its own has no Common Errors section. + +## How a number is written + +Each number is given in decimal and in hexadecimal, as a 32-bit **Long**: 5 (`&H5`), -2147467259 (`&H80004005`). Numbers from 1 to 65535 are VB's own. A negative number is an HRESULT, a COM error code. Errors that a program raises for itself with [**Err.Raise**](../tB/Modules/ErrObject/Raise) usually add their own code to **vbObjectError**, -2147221504 (`&H80040000`). + +The message is the one that **Err.Description** gives for the error once it is raised. For the numbers from 1 to 65535, [**Error**](../tB/Modules/Conversion/Error)`(number)` gives the same text. For an HRESULT, **Error** gives *Application-defined or object-defined error*, as it does for any number without a message of its own, and the table shows both. + +> [!NOTE] BETA 1010 +> For a number from 1 to 65535 that has no message of its own, **Error** gives *Application-defined or object-defined error* in twinBASIC and VB6. After `Err.Raise` of such a number, **Err.Description** in VB6 is that same text; in twinBASIC it is empty for a number below 747, and for 747 and above it is the Windows system message of that number, such as *Page fault was a transition fault.* for 747. + +## Error numbers + +The messages are those of twinBASIC 32-bit and VB6, which agree for every number from 1 to 65535. *Listed by* links to the number's line under [Pages that list each error](#pages-that-list-each-error), which names the pages whose Common Errors section lists it, in alphabetical order. + + + +| Error | Message | Listed by | +|---|---|---| +| **3**{: #3 } (`&H3`) | *Return without GoSub* | --- | +| **5**{: #5 } (`&H5`) | *Invalid procedure call or argument* | [22 pages](#listed-5) | +| **6**{: #6 } (`&H6`) | *Overflow* | [1 page](#listed-6) | +| **7**{: #7 } (`&H7`) | *Out of memory* | --- | +| **9**{: #9 } (`&H9`) | *Subscript out of range* | [1 page](#listed-9) | +| **10**{: #10 } (`&HA`) | *This array is fixed or temporarily locked* | --- | +| **11**{: #11 } (`&HB`) | *Division by zero* | --- | +| **13**{: #13 } (`&HD`) | *Type mismatch* | [3 pages](#listed-13) | +| **14**{: #14 } (`&HE`) | *Out of string space* | --- | +| **16**{: #16 } (`&H10`) | *Expression too complex* | --- | +| **17**{: #17 } (`&H11`) | *Can't perform requested operation* | --- | +| **18**{: #18 } (`&H12`) | *User interrupt occurred* | --- | +| **20**{: #20 } (`&H14`) | *Resume without error* | --- | +| **28**{: #28 } (`&H1C`) | *Out of stack space* | --- | +| **35**{: #35 } (`&H23`) | *Sub or Function not defined* | --- | +| **47**{: #47 } (`&H2F`) | *Too many DLL application clients* | --- | +| **48**{: #48 } (`&H30`) | *Error in loading DLL* | --- | +| **49**{: #49 } (`&H31`) | *Bad DLL calling convention* | --- | +| **51**{: #51 } (`&H33`) | *Internal error* | --- | +| **52**{: #52 } (`&H34`) | *Bad file name or number* | --- | +| **53**{: #53 } (`&H35`) | *File not found* | --- | +| **54**{: #54 } (`&H36`) | *Bad file mode* | --- | +| **55**{: #55 } (`&H37`) | *File already open* | --- | +| **57**{: #57 } (`&H39`) | *Device I/O error* | --- | +| **58**{: #58 } (`&H3A`) | *File already exists* | --- | +| **59**{: #59 } (`&H3B`) | *Bad record length* | --- | +| **61**{: #61 } (`&H3D`) | *Disk full* | --- | +| **62**{: #62 } (`&H3E`) | *Input past end of file* | --- | +| **63**{: #63 } (`&H3F`) | *Bad record number* | --- | +| **67**{: #67 } (`&H43`) | *Too many files* | --- | +| **68**{: #68 } (`&H44`) | *Device unavailable* | --- | +| **70**{: #70 } (`&H46`) | *Permission denied* | --- | +| **71**{: #71 } (`&H47`) | *Disk not ready* | --- | +| **74**{: #74 } (`&H4A`) | *Can't rename with different drive* | --- | +| **75**{: #75 } (`&H4B`) | *Path/File access error* | --- | +| **76**{: #76 } (`&H4C`) | *Path not found* | --- | +| **91**{: #91 } (`&H5B`) | *Object variable or With block variable not set* | --- | +| **92**{: #92 } (`&H5C`) | *For loop not initialized* | --- | +| **93**{: #93 } (`&H5D`) | *Invalid pattern string* | --- | +| **94**{: #94 } (`&H5E`) | *Invalid use of Null* | [28 pages](#listed-94) | +| **96**{: #96 } (`&H60`) | *Unable to sink events of object because the object is already firing events to the maximum number of event receivers that it supports* | --- | +| **97**{: #97 } (`&H61`) | *Can not call friend function on object which is not an instance of defining class* | --- | +| **98**{: #98 } (`&H62`) | *A property or method call cannot include a reference to a private object, either as an argument or as a return value* | --- | +| **321**{: #321 } (`&H141`) | *Invalid file format* | --- | +| **322**{: #322 } (`&H142`) | *Can't create necessary temporary file* | --- | +| **325**{: #325 } (`&H145`) | *Invalid format in resource file* | --- | +| **380**{: #380 } (`&H17C`) | *Invalid property value* | --- | +| **381**{: #381 } (`&H17D`) | *Invalid property array index* | --- | +| **382**{: #382 } (`&H17E`) | *Set not supported at runtime* | --- | +| **383**{: #383 } (`&H17F`) | *Set not supported (read-only property)* | --- | +| **385**{: #385 } (`&H181`) | *Need property array index* | --- | +| **387**{: #387 } (`&H183`) | *Set not permitted* | --- | +| **393**{: #393 } (`&H189`) | *Get not supported at runtime* | --- | +| **394**{: #394 } (`&H18A`) | *Get not supported (write-only property)* | --- | +| **422**{: #422 } (`&H1A6`) | *Property not found* | --- | +| **423**{: #423 } (`&H1A7`) | *Property or method not found* | --- | +| **424**{: #424 } (`&H1A8`) | *Object required* | --- | +| **429**{: #429 } (`&H1AD`) | *ActiveX component can't create object* | --- | +| **430**{: #430 } (`&H1AE`) | *Class does not support Automation or does not support expected interface* | --- | +| **432**{: #432 } (`&H1B0`) | *File name or class name not found during Automation operation* | --- | +| **438**{: #438 } (`&H1B6`) | *Object doesn't support this property or method* | --- | +| **440**{: #440 } (`&H1B8`) | *Automation error* | --- | +| **442**{: #442 } (`&H1BA`) | *Connection to type library or object library for remote process has been lost. Press OK for dialog to remove reference.* | --- | +| **443**{: #443 } (`&H1BB`) | *Automation object does not have a default value* | --- | +| **445**{: #445 } (`&H1BD`) | *Object doesn't support this action* | --- | +| **446**{: #446 } (`&H1BE`) | *Object doesn't support named arguments* | --- | +| **447**{: #447 } (`&H1BF`) | *Object doesn't support current locale setting* | --- | +| **448**{: #448 } (`&H1C0`) | *Named argument not found* | --- | +| **449**{: #449 } (`&H1C1`) | *Argument not optional* | --- | +| **450**{: #450 } (`&H1C2`) | *Wrong number of arguments or invalid property assignment* | --- | +| **451**{: #451 } (`&H1C3`) | *Property let procedure not defined and property get procedure did not return an object* | --- | +| **452**{: #452 } (`&H1C4`) | *Invalid ordinal* | --- | +| **453**{: #453 } (`&H1C5`) | *Specified DLL function not found* | --- | +| **454**{: #454 } (`&H1C6`) | *Code resource not found* | --- | +| **455**{: #455 } (`&H1C7`) | *Code resource lock error* | --- | +| **457**{: #457 } (`&H1C9`) | *This key is already associated with an element of this collection* | --- | +| **458**{: #458 } (`&H1CA`) | *Variable uses an Automation type not supported in Visual Basic* | --- | +| **459**{: #459 } (`&H1CB`) | *Object or class does not support the set of events* | --- | +| **460**{: #460 } (`&H1CC`) | *Invalid clipboard format* | --- | +| **461**{: #461 } (`&H1CD`) | *Method or data member not found* | --- | +| **462**{: #462 } (`&H1CE`) | *The remote server machine does not exist or is unavailable* | --- | +| **463**{: #463 } (`&H1CF`) | *Class not registered on local machine* | --- | +| **481**{: #481 } (`&H1E1`) | *Invalid picture* | --- | +| **482**{: #482 } (`&H1E2`) | *Printer error* | --- | +| **735**{: #735 } (`&H2DF`) | *Can't save file to TEMP* | --- | +| **744**{: #744 } (`&H2E8`) | *Search text not found* | --- | +| **746**{: #746 } (`&H2EA`) | *Replacements too long* | --- | +| **-2147467259**{: #-2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | [1 page](#listed--2147467259) | + +## Pages that list each error + +**5**{: #listed-5 } (`&H5`): [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [Filter](../tB/Modules/Strings/Filter), [Format](../tB/Modules/Strings/Format), [FormatCurrency](../tB/Modules/Strings/FormatCurrency), [FormatDateTime](../tB/Modules/Strings/FormatDateTime), [FormatNumber](../tB/Modules/Strings/FormatNumber), [FormatPercent](../tB/Modules/Strings/FormatPercent), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Join](../tB/Modules/Strings/Join), [Left](../tB/Modules/Strings/Left), [Mid](../tB/Modules/Strings/Mid), [MonthName](../tB/Modules/Strings/MonthName), [Replace](../tB/Modules/Strings/Replace), [Right](../tB/Modules/Strings/Right), [Space](../tB/Modules/Strings/Space), [Split](../tB/Modules/Strings/Split), [StrComp](../tB/Modules/Strings/StrComp), [StrConv](../tB/Modules/Strings/StrConv), [String](../tB/Modules/Strings/String), [WeekdayName](../tB/Modules/Strings/WeekdayName) + +**6**{: #listed-6 } (`&H6`): [Chr](../tB/Modules/Strings/Chr) + +**9**{: #listed-9 } (`&H9`): [ReDim](../tB/Core/ReDim) + +**13**{: #listed-13 } (`&HD`): [Filter](../tB/Modules/Strings/Filter), [Join](../tB/Modules/Strings/Join), [ReDim](../tB/Core/ReDim) + +**94**{: #listed-94 } (`&H5E`): [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [Filter](../tB/Modules/Strings/Filter), [Format](../tB/Modules/Strings/Format), [FormatCurrency](../tB/Modules/Strings/FormatCurrency), [FormatDateTime](../tB/Modules/Strings/FormatDateTime), [FormatNumber](../tB/Modules/Strings/FormatNumber), [FormatPercent](../tB/Modules/Strings/FormatPercent), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Join](../tB/Modules/Strings/Join), [LCase](../tB/Modules/Strings/LCase), [Left](../tB/Modules/Strings/Left), [LTrim](../tB/Modules/Strings/LTrim), [Mid](../tB/Modules/Strings/Mid), [MonthName](../tB/Modules/Strings/MonthName), [Replace](../tB/Modules/Strings/Replace), [Right](../tB/Modules/Strings/Right), [RTrim](../tB/Modules/Strings/RTrim), [Space](../tB/Modules/Strings/Space), [Split](../tB/Modules/Strings/Split), [StrComp](../tB/Modules/Strings/StrComp), [StrConv](../tB/Modules/Strings/StrConv), [String](../tB/Modules/Strings/String), [StrReverse](../tB/Modules/Strings/StrReverse), [Trim](../tB/Modules/Strings/Trim), [UCase](../tB/Modules/Strings/UCase), [WeekdayName](../tB/Modules/Strings/WeekdayName) + +**-2147467259**{: #listed--2147467259 } (`&H80004005`): [ReDim](../tB/Core/ReDim) + + + +## See Also + +- [**On Error**](../tB/Core/On-Error) statement +- [**Err** object](../tB/Modules/ErrObject/) +- [**Error**](../tB/Modules/Conversion/Error) function diff --git a/docs/Reference/index.md b/docs/Reference/index.md index a5473cdb9..cbccd3b35 100644 --- a/docs/Reference/index.md +++ b/docs/Reference/index.md @@ -17,6 +17,7 @@ The reference is organized into three layers: the language constructs the compil - [**Enumerations**](Reference/Enumerations) -- index of all {{tbdocs:enumerations}} enumeration types across all {{tbdocs:packages}} packages, grouped by package and A--Z - [**Data Types**](Reference/Data-Types) -- storage size, range, and suffix for every intrinsic type (**Boolean** through **Variant**) - [**Compiler Constants**](Reference/Compiler-Constants) -- the `#If` symbols the compiler recognises +- [**Run-time Errors**](Reference/Run-time-Errors) -- every trappable error number with its message, and the pages that list it - [**Attributes**](../tB/Core/Attributes) -- `[Description(...)]`, `[COMCreatable(...)]`, and the rest of the attribute syntax - [**COM Interfaces**](Reference/COM-Interfaces/) -- the standard COM interfaces twinBASIC code implements or calls, such as **IEnumVARIANT** - [**twinBASIC Additions**](Reference/twinBASIC-Additions) -- curated list of language and runtime additions beyond standard VBA diff --git a/docs/_book.yml b/docs/_book.yml index aa4c7794c..379d104b5 100644 --- a/docs/_book.yml +++ b/docs/_book.yml @@ -357,6 +357,11 @@ parts: no_outline_entry: true no_heading_shift: true outline_closed: true + - title: Run-time Errors + page: /Reference/Run-time-Errors + no_outline_entry: true + no_heading_shift: true + outline_closed: true - title: Attributes page: /tB/Core/Attributes no_outline_entry: true diff --git a/errors/Core.yml b/errors/Core.yml new file mode 100644 index 000000000..6f214e5e5 --- /dev/null +++ b/errors/Core.yml @@ -0,0 +1,27 @@ +# Statements and operators of docs/Reference/Core/. Format: WIP.Errors.md. +beta: 1010 +pages: + Reference/Core/ReDim.md: + rows: + - error: -2147467259 + when: an upper bound is less than its lower bound + vb6: 9 + bug: redim-inverted-bounds-error + cases: + - { decl: Dim X() As Long, run: ReDim X(0 To -1) } + - { decl: Dim X() As Long, run: ReDim X(-1) } + - { decl: Dim X() As Long, run: "ReDim X(5 To 4, 1)" } + - error: 9 + when: "**Preserve** changes a lower bound, or the bounds of any dimension but the last" + cases: + - { decl: Dim X() As Long, setup: ReDim X(1 To 3), run: ReDim Preserve X(0 To 3) } + - { decl: Dim X() As Long, setup: "ReDim X(3, 3)", run: "ReDim Preserve X(4, 3)" } + - error: 13 + when: "**Preserve** is used on a **Variant** that does not hold an array" + cases: + - { decl: Dim V As Variant, setup: V = 1, run: ReDim Preserve V(3) } + quiet: + - { decl: Dim X() As Long, run: ReDim X(0 To 0) } + - { decl: Dim X() As Long, setup: ReDim X(3), run: ReDim Preserve X(5) } + - { decl: Dim V As Variant, setup: V = 1, run: ReDim V(3) } + - { decl: Dim X() As Long, setup: "ReDim X(3, 3)", run: "ReDim Preserve X(3, 5)" } diff --git a/errors/VBA.Strings.yml b/errors/VBA.Strings.yml new file mode 100644 index 000000000..21f1186b8 --- /dev/null +++ b/errors/VBA.Strings.yml @@ -0,0 +1,644 @@ +# The VBA Strings module. Format: WIP.Errors.md. +beta: 1010 +pages: + Reference/Default/VBA/Strings/Asc.md: + rows: + - error: 5 + when: "*string* is zero-length" + cases: + - Asc("") + - AscB("") + - AscW("") + - { decl: Dim S As String, expr: Asc(S) } + - error: 94 + when: "*string* is **Null**" + cases: [Asc(Null), AscB(Null), AscW(Null)] + quiet: + - Asc("A") + - Asc(ChrW(&H20AC)) + - AscW(ChrW(&H20AC)) + - AscB("A") + Reference/Default/VBA/Strings/Chr.md: + rows: + - error: 5 + when: "**Chr** or **Chr$** is given a *charcode* less than 0 or greater than 255" + cases: + - Chr(-1) + - Chr(256) + - Chr$(256) + - Chr(-32768) + - Chr(65535) + - error: 5 + when: "**ChrW** or **ChrW$** is given a *charcode* less than -32,768 or greater than 65,535" + cases: + - ChrW(-32769) + - ChrW(65536) + - ChrW$(65536) + - error: 6 + when: "**ChrB** or **ChrB$** is given a *charcode* less than 0 or greater than 255" + cases: + - ChrB(-1) + - ChrB(256) + - ChrB$(256) + - error: 94 + when: "*charcode* is **Null**" + cases: [Chr(Null), Chr$(Null), ChrB(Null), ChrW(Null)] + quiet: + - Chr(0) + - Chr(255) + - ChrB(255) + - ChrW(-32768) + - ChrW(-1) + - ChrW(65535) + Reference/Default/VBA/Strings/InStr.md: + rows: + - error: 5 + when: "*start* is less than 1" + cases: + - InStr(0, "abc", "a") + - InStr(-1, "abc", "a") + - InStrB(0, "abc", "a") + - error: 5 + when: "**InStr** is given a *start* greater than 1,073,741,824" + cases: + - InStr(1073741825, "abc", "a") + - InStr(2147483647, "abc", "a") + - error: 5 + when: "**InStr** is given a *compare* that is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - InStr(1, "abc", "a", -1) + - InStr(1, "abc", "a", -2) + - InStr(1, "abc", "a", 255) + - InStr(1, "abc", "a", 65536) + - InStr(1, "abc", "a", 99999) + - error: 5 + when: "**InStr** is given a *compare* that is not a valid locale identifier, and *string1* or *string2* is zero-length" + vb6: 0 + bug: instr-invalid-lcid-empty-string + cases: + - InStr(1, "", "a", 99999) + - InStr(1, "abc", "", 99999) + - error: 0 + when: "**InStr** is given a *compare* of 2 (**vbDatabaseCompare**)" + vb6: 5 + bug: database-compare-no-error + cases: + - InStr(1, "abc", "a", 2) + - error: 94 + when: "*start* or *compare* is **Null**" + cases: + - InStr(Null, "abc", "a") + - InStrB(Null, "abc", "a") + - InStr(1, "abc", "a", Null) + - InStrB(1, "abc", "a", Null) + quiet: + - InStr(1073741824, "abc", "a") + - InStrB(2147483647, "abc", "a") + - InStr(5, "abc", "") + - InStr(Null, "abc") + - InStr("abc", Null) + - InStr(1, Null, "a") + - InStr(1, "abc", "a", 0) + - InStr(1, "abc", "a", 1) + - InStr(1, "abc", "a", 1033) + - InStr(1, "abc", "a", 1041) + - InStrB(1, "abc", "a", 2) + - InStrB(1, "abc", "a", 99999) + Reference/Default/VBA/Strings/InStrRev.md: + rows: + - error: 5 + when: "*start* is 0, or less than -1" + cases: + - InStrRev("abc", "a", 0) + - InStrRev("abc", "a", -2) + - error: 5 + when: "*compare* is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - InStrRev("abc", "a", -1, -1) + - InStrRev("abc", "a", -1, -2) + - InStrRev("abc", "a", -1, 65536) + - InStrRev("abc", "a", -1, 99999) + - InStrRev("abc", "a", -1, 2147483647) + - InStrRev("", "a", -1, 99999) + - error: 94 + when: "any argument is **Null**" + cases: + - InStrRev(Null, "a") + - InStrRev("abc", Null) + - InStrRev("abc", "a", Null) + - InStrRev("abc", "a", -1, Null) + quiet: + - InStrRev("abc", "a", 2147483647) + - InStrRev("abc", "", 5) + - InStrRev("abc", "a", -1, 2) + - InStrRev("abc", "a", -1, 1033) + Reference/Default/VBA/Strings/Left.md: + rows: + - error: 5 + when: "*length* is negative" + cases: + - Left("abc", -1) + - Left$("abc", -1) + - LeftB("abc", -1) + - LeftB$("abc", -2) + - error: 5 + when: "**Left** or **Left$** is given a *length* greater than 1,073,741,823" + cases: + - Left("abc", 1073741824) + - Left$("abc", 2147483647) + - error: 94 + when: "**Left$** or **LeftB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null**" + cases: + - Left$(Null, 1) + - LeftB$(Null, 1) + - Left("abc", Null) + - LeftB("abc", Null) + quiet: + - Left(Null, 1) + - LeftB(Null, 1) + - Left("abc", 0) + - Left("abc", 1073741823) + - LeftB("abc", 2147483647) + Reference/Default/VBA/Strings/Len.md: + quiet: + - Len("") + - Len(Null) + - LenB(Null) + - Len(Empty) + - { decl: Dim S As String * 4, expr: Len(S) } + Reference/Default/VBA/Strings/Mid.md: + rows: + - error: 5 + when: "*start* is less than 1, or *length* is negative" + cases: + - Mid("abc", 0) + - Mid("abc", -1, 1) + - Mid("abc", 1, -1) + - Mid$("abc", 0) + - MidB("abc", 0) + - MidB$("abc", 1, -1) + - error: 5 + when: "**Mid** or **Mid$** is given a *start* greater than 1,073,741,824 or a *length* greater than 1,073,741,823" + cases: + - Mid("abc", 1073741825) + - Mid$("abc", 2147483647) + - Mid("abc", 1, 1073741824) + - Mid$("", 1, 2147483647) + - error: 94 + when: "**Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *start* or *length* that is **Null**" + cases: + - Mid$(Null, 1) + - MidB$(Null, 1) + - Mid("abc", Null) + - MidB$("abc", Null) + - Mid("abc", 1, Null) + - MidB$("abc", 1, Null) + quiet: + - Mid(Null, 1) + - Mid("abc", 4) + - Mid("abc", 1073741824) + - Mid("abc", 1, 1073741823) + - MidB(Null, 1) + - MidB("abc", 2147483647) + - MidB("abc", 1, 2147483647) + - Mid("", 1, 0) + Reference/Default/VBA/Strings/Right.md: + rows: + - error: 5 + when: "*length* is negative" + cases: + - Right("abc", -1) + - Right$("abc", -1) + - RightB("abc", -1) + - RightB$("abc", -2) + - error: 5 + when: "**Right** or **Right$** is given a *length* greater than 1,073,741,823" + cases: + - Right("abc", 1073741824) + - Right$("abc", 2147483647) + - error: 94 + when: "**Right$** or **RightB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null**" + cases: + - Right$(Null, 1) + - RightB$(Null, 1) + - Right("abc", Null) + - RightB("abc", Null) + quiet: + - Right(Null, 1) + - RightB(Null, 1) + - Right("abc", 0) + - Right("abc", 1073741823) + - RightB("abc", 2147483647) + Reference/Default/VBA/Strings/Filter.md: + rows: + - error: 5 + when: "*compare* is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - Filter(Array("ab", "c"), "a", True, -1) + - Filter(Array("ab", "c"), "a", True, -2) + - Filter(Array("ab", "c"), "a", True, 99999) + - error: 0 + when: "*compare* is 2 (**vbDatabaseCompare**)" + vb6: 5 + bug: database-compare-no-error + cases: + - Filter(Array("ab", "c"), "a", True, 2) + - error: 13 + when: "*sourcearray* is **Null**, or has more than one dimension" + cases: + - Filter(Null, "a") + - decl: Dim A(1, 1) As String + expr: Filter(A, "a") + - error: 94 + when: "*match*, *include* or *compare* is **Null**, or an element of *sourcearray* is **Null**" + cases: + - Filter(Array("ab", "c"), Null) + - Filter(Array("ab", "c"), "a", Null) + - Filter(Array("ab", "c"), "a", True, Null) + - Filter(Array("ab", Null), "a") + quiet: + - Filter(Array("ab", 1), "a") + - Filter(Array(), "a") + - decl: Dim A(1) As Long + expr: Filter(A, "a") + only: tb + - decl: Dim A(1) As Double + expr: Filter(A, "a") + only: tb + - Filter(Array("ab"), "a", 2) + - Filter(Array("ab", "c"), "a", True, 1033) + - decl: Dim A() As String + expr: Filter(A, "a") + Reference/Default/VBA/Strings/Format.md: + rows: + - error: 5 + when: "*firstDayOfWeek* is less than 0 or greater than 7, or *firstWeekOfYear* is less than 0 or greater than 3" + cases: + - Format(Now, "ww", -1) + - Format(Now, "ww", 8) + - Format(Now, "ww", 1, -1) + - Format(Now, "ww", 1, 4) + - Format$(1, "0", 8) + - error: 94 + when: "*format*, *firstDayOfWeek* or *firstWeekOfYear* is **Null**" + cases: + - Format(1, Null) + - Format(Now, "ww", Null) + - Format(Now, "ww", 1, Null) + quiet: + - Format(Null) + - Format$(Null) + - Format("abc", "0.00") + - Format(1, "") + - Format(Now, "ww", 7, 3) + - Format(Now, "ww", 0, 0) + Reference/Default/VBA/Strings/FormatCurrency.md: + rows: + - error: 5 + when: "*numDigitsAfterDecimal* is less than -1 or greater than 255" + cases: + - FormatCurrency(1, -2) + - FormatCurrency(1, 256) + - FormatCurrency(0, 1000) + - error: 5 + when: "*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0" + cases: + - FormatCurrency(1, 2, -3) + - FormatCurrency(1, 2, 1) + - FormatCurrency(1, 2, -1, 5) + - FormatCurrency(1, 2, -1, -1, 5) + - error: 94 + when: "an argument other than *expression* is **Null**" + cases: + - FormatCurrency(1, Null) + - FormatCurrency(1, 2, Null) + - FormatCurrency(1, 2, -1, Null) + - FormatCurrency(1, 2, -1, -1, Null) + quiet: + - FormatCurrency(Null) + - FormatCurrency(1, 255) + - FormatCurrency(1, 2, -2, -2, -2) + - FormatCurrency(1, 2, 0, 0, 0) + Reference/Default/VBA/Strings/FormatDateTime.md: + rows: + - error: 5 + when: "*namedFormat* is less than 0 or greater than 4" + cases: + - FormatDateTime(Now, -1) + - FormatDateTime(Now, 5) + - error: 94 + when: "*namedFormat* is **Null**" + cases: + - FormatDateTime(Now, Null) + quiet: + - FormatDateTime(Null) + - FormatDateTime(Now, 0) + - FormatDateTime(Now, 4) + Reference/Default/VBA/Strings/FormatNumber.md: + rows: + - error: 5 + when: "*numDigitsAfterDecimal* is less than -1" + cases: + - FormatNumber(1, -2) + - error: 5 + when: "*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0" + cases: + - FormatNumber(1, 2, -3) + - FormatNumber(1, 2, 1) + - FormatNumber(1, 2, -1, 5) + - FormatNumber(1, 2, -1, -1, 5) + - error: 94 + when: "an argument other than *expression* is **Null**" + cases: + - FormatNumber(1, Null) + - FormatNumber(1, 2, Null) + - FormatNumber(1, 2, -1, Null) + - FormatNumber(1, 2, -1, -1, Null) + quiet: + - FormatNumber(Null) + - FormatNumber(1, 1000) + - FormatNumber(1, 2, -2, -2, -2) + - FormatNumber(1, 2, 0, 0, 0) + Reference/Default/VBA/Strings/FormatPercent.md: + rows: + - error: 5 + when: "*numDigitsAfterDecimal* is less than -1" + cases: + - FormatPercent(1, -2) + - error: 5 + when: "*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0" + cases: + - FormatPercent(1, 2, -3) + - FormatPercent(1, 2, 1) + - FormatPercent(1, 2, -1, 5) + - FormatPercent(1, 2, -1, -1, 5) + - error: 94 + when: "an argument other than *expression* is **Null**" + cases: + - FormatPercent(1, Null) + - FormatPercent(1, 2, Null) + - FormatPercent(1, 2, -1, Null) + - FormatPercent(1, 2, -1, -1, Null) + quiet: + - FormatPercent(Null) + - FormatPercent(1, 100) + - FormatPercent(1, 2, -2, -2, -2) + Reference/Default/VBA/Strings/Join.md: + rows: + - error: 5 + when: "*sourcearray* has more than one dimension" + cases: + - decl: Dim A(1, 1) As String + expr: Join(A) + - error: 13 + when: "*sourcearray* is **Null**, or an element of it is an array" + cases: + - Join(Null) + - Join(Array(Array(1))) + - error: 94 + when: "*delimiter* is **Null**, or an element of *sourcearray* is **Null**" + cases: + - Join(Array("a", "b"), Null) + - Join(Array("a", Null)) + quiet: + - Join(Array(1, 2)) + - Join(Array()) + - decl: Dim A(1) As Long + expr: Join(A) + only: tb + - decl: Dim A(1) As Double + expr: Join(A) + only: tb + - decl: Dim A() As String + expr: Join(A) + Reference/Default/VBA/Strings/LCase.md: + rows: + - error: 94 + when: "**LCase$** is given a *string* that is **Null**" + cases: [LCase$(Null)] + quiet: [LCase(Null), LCase(1)] + Reference/Default/VBA/Strings/LTrim.md: + rows: + - error: 94 + when: "**LTrim$** is given a *string* that is **Null**" + cases: [LTrim$(Null)] + quiet: [LTrim(Null)] + Reference/Default/VBA/Strings/MonthName.md: + rows: + - error: 5 + when: "*month* is less than 1 or greater than 12" + cases: [MonthName(0), MonthName(13), MonthName(-1)] + - error: 94 + when: "*month* or *abbreviate* is **Null**" + cases: + - MonthName(Null) + - MonthName(1, Null) + quiet: + - MonthName(12, True) + - MonthName(1, 2) + Reference/Default/VBA/Strings/Replace.md: + rows: + - error: 5 + when: "*start* is less than 1, or *count* is less than -1" + cases: + - Replace("abc", "a", "b", 0) + - Replace("abc", "a", "b", -1) + - Replace("abc", "a", "b", 1, -2) + - error: 5 + when: "*compare* is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - Replace("abc", "a", "b", 1, -1, -1) + - Replace("abc", "a", "b", 1, -1, -2) + - Replace("abc", "a", "b", 1, -1, 99999) + - Replace("", "a", "b", 1, -1, 99999) + - error: 94 + when: "any argument is **Null**" + cases: + - Replace(Null, "a", "b") + - Replace("abc", Null, "b") + - Replace("abc", "a", Null) + - Replace("abc", "a", "b", Null) + - Replace("abc", "a", "b", 1, Null) + - Replace("abc", "a", "b", 1, -1, Null) + quiet: + - Replace("abc", "a", "b", 5) + - Replace("abc", "a", "b", 1073741825) + - Replace("abc", "a", "b", 1, 0) + - Replace("abc", "a", "b", 1, -1, 2) + - Replace("abc", "a", "b", 1, -1, 1033) + Reference/Default/VBA/Strings/RTrim.md: + rows: + - error: 94 + when: "**RTrim$** is given a *string* that is **Null**" + cases: [RTrim$(Null)] + quiet: [RTrim(Null)] + Reference/Default/VBA/Strings/Space.md: + rows: + - error: 5 + when: "*number* is negative, or greater than 1,073,741,823" + cases: [Space(-1), Space$(-1), Space(1073741824), Space(2147483647)] + - error: 94 + when: "*number* is **Null**" + cases: [Space(Null), Space$(Null)] + quiet: [Space(0)] + Reference/Default/VBA/Strings/Split.md: + rows: + - error: 5 + when: "*limit* is less than -1" + cases: + - Split("a b", " ", -2) + - error: 5 + when: "*compare* is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - Split("a b", " ", -1, -1) + - Split("a b", " ", -1, -2) + - Split("a b", " ", -1, 99999) + - Split("", " ", -1, 99999) + - error: 94 + when: "any argument is **Null**" + cases: + - Split(Null) + - Split("a b", Null) + - Split("a b", " ", Null) + - Split("a b", " ", -1, Null) + quiet: + - Split("a b", " ", 0) + - Split("a b", " ", -1, 2) + - Split("a b", " ", -1, 1033) + Reference/Default/VBA/Strings/StrComp.md: + rows: + - error: 5 + when: "*compare* is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - StrComp("a", "b", -1) + - StrComp("a", "b", -2) + - StrComp("a", "b", 99999) + - error: 0 + when: "*compare* is 2 (**vbDatabaseCompare**)" + vb6: 5 + bug: database-compare-no-error + cases: + - StrComp("a", "b", 2) + - error: 94 + when: "*compare* is **Null**" + cases: + - StrComp("a", "b", Null) + quiet: + - StrComp(Null, "b") + - StrComp(Null, "b", 99999) + - StrComp("", "", 99999) + - StrComp("a", "", 99999) + - StrComp("a", "b", 1033) + Reference/Default/VBA/Strings/StrConv.md: + rows: + - error: 5 + when: "*conversion* is a value the table does not list, or combines **vbUnicode**, **vbFromUnicode**, **vbUTF8** or **vbFromUTF8** with another value" + cases: + - StrConv("a", 64 + 128) + - StrConv("a", 1 + 64) + - StrConv("a", 128 + 2) + - StrConv("a", 1024) + - StrConv("a", -1) + - { expr: 'StrConv("a", 256 + 512)', only: tb } + - { expr: 'StrConv("a", 64 + 256)', only: tb } + - error: 5 + when: "*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** on a system whose locale is not East Asian" + cases: + - StrConv("a", 4) + - StrConv("a", 8) + - StrConv("a", 16) + - StrConv("a", 32) + - error: 5 + when: "*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** and *LCID* names an East Asian locale, on a system whose locale is not East Asian" + vb6: 0 + bug: strconv-east-asian-lcid + cases: + - StrConv("a", 4, 1041) + - StrConv("a", 16, 1041) + - error: 5 + when: "*LCID* is not a valid locale identifier" + cases: + - StrConv("a", 1, 99999) + - StrConv("a", 1, -1) + - error: 94 + when: "*conversion* or *LCID* is **Null**" + cases: + - StrConv("a", Null) + - StrConv("a", 1, Null) + quiet: + - StrConv(Null, 1) + - StrConv("a", 0) + - StrConv("a", 1 + 2) + - StrConv("a", 3) + - StrConv("a", 64) + - StrConv("a", 128) + - StrConv("a", 1, 1033) + - { expr: 'StrConv("a", 256)', only: tb } + - { expr: 'StrConv("a", 512)', only: tb } + Reference/Default/VBA/Strings/StrReverse.md: + rows: + - error: 94 + when: "*expression* is **Null**" + cases: [StrReverse(Null)] + quiet: [StrReverse(""), StrReverse(1)] + Reference/Default/VBA/Strings/String.md: + rows: + - error: 5 + when: "*number* is negative, or greater than 1,073,741,823" + cases: + - String(-1, "a") + - String$(-1, "a") + - String(1073741824, "a") + - String(2147483647, "a") + - error: 5 + when: "*character* is a zero-length string" + cases: + - String(3, "") + - error: 94 + when: "*number* is **Null**, or **String$** is given a *character* that is **Null**" + cases: + - String(Null, "a") + - String$(Null, "a") + - String$(3, Null) + - error: 94 + when: "**String** is given a *character* that is **Null**" + vb6: 0 + bug: string-null-character + cases: + - String(3, Null) + quiet: + - String(0, "a") + - String(3, "abc") + - String(3, 65) + - String(3, 256) + - String(3, -1) + Reference/Default/VBA/Strings/Trim.md: + rows: + - error: 94 + when: "**Trim$** is given a *string* that is **Null**" + cases: [Trim$(Null)] + quiet: [Trim(Null)] + Reference/Default/VBA/Strings/UCase.md: + rows: + - error: 94 + when: "**UCase$** is given a *string* that is **Null**" + cases: [UCase$(Null)] + quiet: [UCase(Null)] + Reference/Default/VBA/Strings/WeekdayName.md: + rows: + - error: 5 + when: "*weekday* is less than 1 or greater than 7, or *firstdayofweek* is less than 0 or greater than 7" + cases: + - WeekdayName(0) + - WeekdayName(8) + - WeekdayName(1, False, -1) + - WeekdayName(1, False, 8) + - error: 94 + when: "any argument is **Null**" + cases: + - WeekdayName(Null) + - WeekdayName(1, Null) + - WeekdayName(1, False, Null) + quiet: + - WeekdayName(7, False, 0) + - WeekdayName(1, 2) diff --git a/errors/messages.yml b/errors/messages.yml new file mode 100644 index 000000000..27e7bf0da --- /dev/null +++ b/errors/messages.yml @@ -0,0 +1,97 @@ +# The message of each run-time error number: what Err.Description gives once the error +# is raised, and what Error$(number) gives. A number gives one message where the two +# agree, and raised: and error: where they differ. Every number from 1 to 65535 with a +# message of its own is here; another number is here when the data names it. +# scripts/errors.mjs messages measures the file, and run checks every case against it. +# Measured on BETA 1010, in twinBASIC 32-bit and VB6, which agree on every number below +# 65536. +3: Return without GoSub +5: Invalid procedure call or argument +6: Overflow +7: Out of memory +9: Subscript out of range +10: This array is fixed or temporarily locked +11: Division by zero +13: Type mismatch +14: Out of string space +16: Expression too complex +17: "Can't perform requested operation" +18: User interrupt occurred +20: Resume without error +28: Out of stack space +35: Sub or Function not defined +47: Too many DLL application clients +48: Error in loading DLL +49: Bad DLL calling convention +51: Internal error +52: Bad file name or number +53: File not found +54: Bad file mode +55: File already open +57: Device I/O error +58: File already exists +59: Bad record length +61: Disk full +62: Input past end of file +63: Bad record number +67: Too many files +68: Device unavailable +70: Permission denied +71: Disk not ready +74: "Can't rename with different drive" +75: Path/File access error +76: Path not found +91: Object variable or With block variable not set +92: For loop not initialized +93: Invalid pattern string +94: Invalid use of Null +96: Unable to sink events of object because the object is already firing events to the maximum number of event receivers that it supports +97: Can not call friend function on object which is not an instance of defining class +98: A property or method call cannot include a reference to a private object, either as an argument or as a return value +321: Invalid file format +322: "Can't create necessary temporary file" +325: Invalid format in resource file +380: Invalid property value +381: Invalid property array index +382: Set not supported at runtime +383: Set not supported (read-only property) +385: Need property array index +387: Set not permitted +393: Get not supported at runtime +394: Get not supported (write-only property) +422: Property not found +423: Property or method not found +424: Object required +429: "ActiveX component can't create object" +430: Class does not support Automation or does not support expected interface +432: File name or class name not found during Automation operation +438: "Object doesn't support this property or method" +440: Automation error +442: Connection to type library or object library for remote process has been lost. Press OK for dialog to remove reference. +443: Automation object does not have a default value +445: "Object doesn't support this action" +446: "Object doesn't support named arguments" +447: "Object doesn't support current locale setting" +448: Named argument not found +449: Argument not optional +450: Wrong number of arguments or invalid property assignment +451: Property let procedure not defined and property get procedure did not return an object +452: Invalid ordinal +453: Specified DLL function not found +454: Code resource not found +455: Code resource lock error +457: This key is already associated with an element of this collection +458: Variable uses an Automation type not supported in Visual Basic +459: Object or class does not support the set of events +460: Invalid clipboard format +461: Method or data member not found +462: The remote server machine does not exist or is unavailable +463: Class not registered on local machine +481: Invalid picture +482: Printer error +735: "Can't save file to TEMP" +744: Search text not found +746: Replacements too long +-2147467259: + raised: Unspecified error + error: Application-defined or object-defined error diff --git a/scripts/check_errors.mjs b/scripts/check_errors.mjs new file mode 100644 index 000000000..68febe136 --- /dev/null +++ b/scripts/check_errors.mjs @@ -0,0 +1,391 @@ +#!/usr/bin/env node +// Gate: every page's Common Errors section is what the run-time error data under errors/ +// writes, and the data is in order. +// +// node scripts/check_errors.mjs +// +// The data (errors/*.yml, its format in WIP.Errors.md) lists the cases that raise each +// error a documented procedure raises; scripts/errors.mjs run measures them in +// twinBASIC and VB6, and scripts/errors.mjs write writes each page's section between +// and . This gate needs neither runtime. It checks +// that: +// * every data file and errors/messages.yml reads, with no unknown key, and every +// error number the data names has its message; +// * a row that differs from VB6 names its reproducer under bugs/, or says pending; +// * every page the data names exists, in one data file only, and its section is +// what write would make of it, or absent when the data gives it none; +// * no page under docs/ has a section the data does not name; +// * the index of the numbers on the central page, docs/Reference/Run-time-Errors.md, +// is what write makes of the data and errors/messages.yml. +// It starts with probes of the library on inputs of its own. +// +// Exit codes: +// 0 all pass +// 1 a probe or the corpus failed +// 2 the gate could not run: a refused command line, or a crash + +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import yaml from "js-yaml"; +import { exitOnCrash, parseCli, printHelpAndExit, withUsageError } from "../lib/cli.mjs"; +import { markdownFiles } from "../lib/markdown-files.mjs"; +import { DOCS_DIR, REPO_ROOT } from "../lib/repo-paths.mjs"; +import { + CENTRAL_PAGE, + CENTRAL_SHORTCUT, + caseAtLine, + compareMessages, + dataFiles, + findSection, + loadDataFile, + loadMessages, + messageProbeModule, + pageLevel, + pageMeta, + pagePath, + probeCases, + probeModule, + readData, + readMessageOutput, + readMessages, + readProbeOutput, + relativeLink, + renderIndex, + renderSection, + spliceIndex, + spliceSection, +} from "./lib/errors-data.mjs"; +import { createProbes } from "./lib/gate-probes.mjs"; + +exitOnCrash(); + +const USAGE = `usage: node scripts/check_errors.mjs [-h, --help] + +Checks that every page's Common Errors section is what scripts/errors.mjs write makes +of the data under errors/, and that the data is in order. Needs no twinBASIC or +VB6; measuring the data is scripts/errors.mjs run. + + -h, --help print this text and exit + +Exit codes: + 0 all pass + 1 a probe or the corpus failed + 2 the gate could not run: a refused command line, or a crash`; + +if ( + withUsageError(() => + parseCli(process.argv.slice(2), { options: { help: { type: "boolean", short: "h" } }, stopAt: ["help"] }), + ).values.help +) + printHelpAndExit(USAGE); + +const { check, report } = createProbes( + "check_errors", + "run node scripts/errors.mjs write for a stale section; the data's format is in WIP.Errors.md", +); + +// ------------------------------------------------------------------ probes + +const data = (doc) => { + const problems = []; + const read = readData(yaml.load(doc), "probe.yml", problems); + return { ...read, name: "probe.yml", problems }; +}; +const sample = data(` +beta: 1010 +pages: + A/B.md: + rows: + - error: 5 + when: "*x* is negative" + cases: [F(-1), { decl: Dim X() As Long, run: "ReDim X(1)\\nX(2) = 1" }] + - error: 9 + when: the index | is out + vb6: 5 + bug: pending + cases: [G(1)] + quiet: [F(1)] +`); +const { messages, problems: sampleMessageProblems } = readMessages({ + 5: "Invalid procedure call or argument", + 9: "Subscript out of range", + "-2147467259": { raised: "Unspecified error", error: "Application-defined or object-defined error" }, +}); +check( + "messages read, in both forms", + sampleMessageProblems.length === 0 && + messages.get(-2147467259).raised === "Unspecified error" && + messages.get(5).error === messages.get(5).raised, +); +check( + "a bad message is refused", + readMessages({ + x: "a", + 1: "", + 2: { raised: "a" }, + 3: { raised: "a", error: "a" }, + 4: { raised: "a", error: "b", vb6: "c" }, + }).problems.length === 5, +); +check( + "a sample reads with only the problem it has", + sample.problems.length === 1 && /without \|/.test(sample.problems[0]), + sample.problems.join("\n"), +); +check( + "unknown keys, a missing when and vb6 without bug are refused", + data(` +beta: 1 +pages: + A.md: + rows: + - { error: 5, cases: [x], vb6: 9, colour: red } +`).problems.length === 3, +); +check("a page with nothing to probe is refused", data("beta: 1\npages:\n A.md: {}\n").problems.length === 1); + +const page = sample.pages[0]; +page.rows[1].when = "the index is out"; +const section = renderSection(page, { + name: "probe.yml", + beta: 1010, + messages, + level: "###", + permalink: "/tB/Modules/Strings/Mid", +}); +check( + "the section has the table, its numbers linked to the central page by its shortcut, and the VB6 note", + section + .join("\n") + .includes( + "| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *x* is negative |\n| [9 (`&H9`)](tb:Errors#9), *Subscript out of range* | the index is out |\n\n> [!NOTE] BETA 1010\n> When the index is out, VB6 raises error [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument*, instead.", + ), +); +const merged = renderSection( + { + rows: [ + { error: 5, when: "*x* is negative", cases: [] }, + { error: 9, when: "the index is out", cases: [] }, + { error: 5, when: "*y* is 0", cases: [] }, + ], + quiet: [], + }, + { name: "probe.yml", beta: 1010, messages, level: "##", permalink: "/A" }, +).filter((l) => l.startsWith("| [")); +check( + "rows that raise the same number share one row, their conditions a line each", + merged.length === 2 && merged[0].endsWith("| *x* is negative
*y* is 0 |"), + merged.join("\n"), +); +check("a page with only quiet cases gets no section", renderSection({ rows: [], quiet: [1] }, { messages }) === null); +const vb6Only = renderSection( + { rows: [{ error: 0, when: "*x* is 2", vb6: 5, bug: "pending", cases: [] }], quiet: [] }, + { name: "probe.yml", beta: 1010, messages, level: "##", permalink: "/A" }, +).join("\n"); +check( + "a row of error 0 has its VB6 note and no table", + !vb6Only.includes("| Error |") && + vb6Only.includes( + "## Common Errors\n\n> [!NOTE] BETA 1010\n> When *x* is 2, VB6 raises error [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument*; twinBASIC raises none.\n\n", + ), + vb6Only, +); +check( + "error 0 is refused without a vb6: number", + data("beta: 1\npages:\n A.md:\n rows:\n - { error: 0, when: x, cases: [x] }\n").problems.length === 1, +); + +const doc = "# T\n\ntext\n\n```tb\n### Example\n\n```\n\n### Example\n\ncode\n\n### See Also\n"; +const added = spliceSection(doc, section); +check( + "a new section goes before the Example heading, not one in a fence", + added.indexOf("### Common Errors") < added.lastIndexOf("### Example") && + added.indexOf("### Common Errors") > added.indexOf("```\n\n"), +); +check("writing it again changes nothing", spliceSection(added, section) === added); +check("removing it gives the page back", spliceSection(added, null) === doc); +check("a marker in a fence is not one", findSection(doc).section === null); +check("CRLF stays CRLF", spliceSection(doc.replace(/\n/g, "\r\n"), section) === added.replace(/\n/g, "\r\n")); +check("an unclosed section is reported", findSection("\n### Common Errors\n").problems.length === 1); +check( + "the level follows the page's Example heading", + pageLevel(doc) === "###" && pageLevel("# T\n## See Also\n") === "##", +); + +const cases = probeCases([sample], "tb"); +const vb6Cases = probeCases([sample], "vb6"); +check( + "cases carry what each runtime expects", + cases.map((c) => c.expect).join() === "5,5,9,0" && vb6Cases[2].expect === 5, +); +for (const dialect of ["tb", "vb6"]) { + const mod = probeModule(cases, dialect); + const lines = mod.text.split("\r\n"); + check( + `${dialect}: each case's span is its own Sub`, + mod.spans.every((s) => lines[s.from - 1] === `Private Sub C${s.id}()` && lines[s.to - 1] === "End Sub") && + caseAtLine(mod.spans, mod.spans[1].from + 3) === 2, + ); +} +check( + "links are relative to the permalinks", + relativeLink("/Reference/Run-time-Errors", "/tB/Core/ReDim") === "../tB/Core/ReDim" && + relativeLink("/tB/Modules/ErrObject/", "/Reference/Run-time-Errors") === "../../../Reference/Run-time-Errors" && + relativeLink("/Reference/Run-time-Errors", "/tB/Modules/ErrObject/") === "../tB/Modules/ErrObject/", +); +check( + "the title and the permalink come from the front matter", + pageMeta("---\ntitle: Mid\npermalink: /tB/Modules/Strings/Mid\n---\n# Mid\n").permalink === + "/tB/Modules/Strings/Mid" && pageMeta("# no front matter\n").title === null, +); + +const index = renderIndex([sample], messages, new Map([["A/B.md", { title: "B", permalink: "/tB/B" }]])); +check( + "the index has a row per number, in order, with its anchor and a link to its pages", + index.slice(4, 7).join("\n") === + "| **5**{: #5 } (`&H5`) | *Invalid procedure call or argument* | [1 page](#listed-5) |\n| **9**{: #9 } (`&H9`) | *Subscript out of range* | [1 page](#listed-9) |\n| **-2147467259**{: #-2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | --- |", + index.join("\n"), +); +check( + "the section after the table lists each named number's pages, only those numbers", + index + .join("\n") + .endsWith( + "## Pages that list each error\n\n**5**{: #listed-5 } (`&H5`): [B](../tB/B)\n\n**9**{: #listed-9 } (`&H9`): [B](../tB/B)\n\n", + ), + index.join("\n"), +); +const central = "# T\n\n\nold\n\n\n## See Also\n"; +const spliced = spliceIndex(central, index); +check( + "the index goes between its markers, and writing it again changes nothing", + spliced.problem === null && + spliced.text.includes("| **5**") && + spliceIndex(spliced.text, index).text === spliced.text, +); +check("a page without the index markers is refused", spliceIndex("# T\n", index).problem !== null); + +const msgOut = readMessageOutput([ + "msg\t5\tInvalid procedure call or argument\t5\tInvalid procedure call or argument", + "done", +]); +const msg5 = new Map([ + [5, { raised: "Invalid procedure call or argument", error: "Invalid procedure call or argument" }], +]); +check( + "messages agree with a probe that says the same, and not with one that differs", + msgOut.done && + compareMessages(msg5, { tb: msgOut.seen }).length === 0 && + compareMessages(new Map(), { tb: msgOut.seen }).length === 1 && + compareMessages(msg5, { tb: new Map() }).length === 1, +); +check( + "VB6's Err.Raise of an HRESULT is not compared, its Error$ is", + compareMessages(new Map([[-2147467259, { raised: "Unspecified error", error: "G" }]]), { + vb6: new Map([[-2147467259, { error: "G", number: -2147467259, description: "Automation error" }]]), + }).length === 0 && + compareMessages(new Map([[-2147467259, { raised: "Unspecified error", error: "G" }]]), { + vb6: new Map([[-2147467259, { error: "H", number: -2147467259, description: "Automation error" }]]), + }).length === 1, +); + +for (const dialect of ["tb", "vb6"]) { + const lines = messageProbeModule([-2147467259], dialect).split("\r\n"); + check( + `${dialect}: the messages' probe runs every VB number, then the others, then ends`, + lines.includes(" For N = 1 To 65535") && + lines.includes(" One -2147467259, True") && + lines.filter((l) => l === "Public Sub Main()").length === 1 && + (dialect === "vb6" || lines.includes("End Module")), + ); +} + +const out = readProbeOutput(["case 1 5 Invalid procedure call or argument", "setup 2 9 x", "done"]); +check("probe output reads", out.done && out.seen.get(1).number === 5 && out.seen.get(2).setup); + +// ------------------------------------------------------------------ the corpus + +const { messages: known, problems: messageProblems } = loadMessages(); +const files = dataFiles().map((n) => loadDataFile(n)); +check("errors/ holds data", files.length > 0); +const problems = [...messageProblems, ...files.flatMap((f) => f.problems)]; +const owner = new Map(); +for (const f of files) { + for (const p of f.pages) { + if (owner.has(p.page)) problems.push(`${f.name}: ${p.page} is in ${owner.get(p.page)} too`); + owner.set(p.page, f.name); + for (const r of p.rows) { + for (const n of [r.error, r.vb6]) { + if (n !== undefined && n !== 0 && !known.has(n)) + problems.push(`${f.name}: ${p.page}: no message for ${n} in messages.yml`); + } + const slug = r.bug; + if ( + slug && + slug !== "pending" && + ![ + ["bugs", slug], + ["bugs", "filed", slug], + ].some((s) => existsSync(path.join(REPO_ROOT, ...s))) + ) { + problems.push(`${f.name}: ${p.page}: no reproducer bugs/${slug}`); + } + } + } +} +check("the data is in order", problems.length === 0, problems.join("\n")); + +const stale = []; +for (const f of files) { + for (const p of f.pages) { + const file = pagePath(p.page); + if (!existsSync(file)) { + stale.push(`docs/${p.page}: no such page (${f.name})`); + continue; + } + const text = readFileSync(file, "utf8"); + const marks = findSection(text).problems; + const meta = pageMeta(text); + if (!meta.title || !meta.permalink) marks.push("no title: or permalink: in the front matter, to link it by"); + if (marks.length) { + stale.push(...marks.map((m) => `docs/${p.page}: ${m}`)); + continue; + } + const want = renderSection(p, { + name: f.name, + beta: f.beta, + messages: known, + level: pageLevel(text), + }); + if (spliceSection(text, want) !== text) stale.push(`docs/${p.page}: the section is not what ${f.name} writes`); + } +} +for (const rel of await markdownFiles(DOCS_DIR)) { + if (owner.has(rel)) continue; + const { section: at, problems: marks } = findSection(readFileSync(path.join(DOCS_DIR, rel), "utf8")); + if (at || marks.length) stale.push(`docs/${rel}: an errors section that no data file names`); +} +check("every section is what the data writes", stale.length === 0, stale.join("\n")); + +const metas = new Map(); +for (const f of files) { + for (const p of f.pages) { + if (existsSync(pagePath(p.page))) metas.set(p.page, pageMeta(readFileSync(pagePath(p.page), "utf8"))); + } +} +const centralFile = pagePath(CENTRAL_PAGE); +const centralText = existsSync(centralFile) ? readFileSync(centralFile, "utf8") : null; +const indexed = + centralText === null ? { problem: "no such page" } : spliceIndex(centralText, renderIndex(files, known, metas)); +check( + `the index on docs/${CENTRAL_PAGE} is what the data writes`, + !indexed.problem && indexed.text === centralText, + `docs/${CENTRAL_PAGE}: ${indexed.problem ?? "the index is stale"}`, +); +check( + `docs/${CENTRAL_PAGE} has the shortcut the sections link to it by`, + centralText !== null && pageMeta(centralText).shortcut === CENTRAL_SHORTCUT, + `its front matter needs shortcut: ${CENTRAL_SHORTCUT}`, +); + +process.exit(report()); diff --git a/scripts/errors.mjs b/scripts/errors.mjs new file mode 100644 index 000000000..e8ebe86e5 --- /dev/null +++ b/scripts/errors.mjs @@ -0,0 +1,428 @@ +#!/usr/bin/env node +// The run-time errors each documented procedure raises: measuring them, and +// writing each page's Common Errors section from the data under errors/. +// +// node scripts/errors.mjs run [...] [--only ] [--runtime tb|vb6|win64]... +// node scripts/errors.mjs generate [...] [--only ] +// node scripts/errors.mjs write [...] +// node scripts/errors.mjs messages [--runtime tb|vb6]... +// +// Exit codes: +// 0 run, messages: every runtime agrees with the data; generate, write: done +// 1 run, messages: a runtime disagrees with the data +// 2 refused command line, bad data, or a runtime could not be run +// +// WHY THIS EXISTS +// +// No VB or VBA reference says which errors a procedure can raise, and a reader +// writing an error handler needs exactly that. Hand-written tables cannot be +// re-checked on a new BETA, so each page's errors are data -- the cases that +// raise them, errors/*.yml, whose format WIP.Errors.md describes -- and the +// section, the probe and the check are generated from it. `run` builds one +// probe from the data for each runtime (a twinBASIC exe through scripts/tbrun.mjs, +// a VB6 exe through lib/vb6.mjs) and compares what each case raised with what +// the data says; `messages` does the same for errors/messages.yml, the message +// of every number. They need twinBASIC and VB6, so they are run by a person when +// a BETA is installed, never by CI. `write` -- each page's section, and the +// index of the numbers on the central page -- needs neither; check_errors.mjs is +// its gate. + +import { spawn } from "node:child_process"; +import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { die, exitOnCrash, parseCli, printHelpAndExit, regexOption, withUsageError } from "../lib/cli.mjs"; +import { REPO_ROOT } from "../lib/repo-paths.mjs"; +import { + CENTRAL_PAGE, + caseAtLine, + compareMessages, + dataFiles, + findSection, + loadDataFile, + loadMessages, + messageProbeModule, + pageLevel, + pageMeta, + pagePath, + probeCases, + probeModule, + readMessageOutput, + readProbeOutput, + renderIndex, + renderSection, + spliceIndex, + spliceSection, + VB_NUMBERS, +} from "./lib/errors-data.mjs"; +import { encodeAnsi, findVb6, NO_VB6, parseMakeLog, runRepro } from "./lib/vb6.mjs"; + +exitOnCrash(); + +const USAGE = `usage: node scripts/errors.mjs run [...] [--only ] [--runtime R]... [--ide P] [--vb6 P] [--keep] + node scripts/errors.mjs generate [...] [--only ] + node scripts/errors.mjs write [...] + node scripts/errors.mjs messages [--runtime R]... [--ide P] [--vb6 P] [--keep] [-h, --help] + +The run-time errors of the documented procedures, from the data under errors/ +(its format is in WIP.Errors.md). A file is named as it is in errors/; with none +named, every file there is used. + + run builds a probe of the cases for each runtime, runs it, and compares + what each case raised with the data. A case the compiler refuses is + reported and left out, and the probe built again without it. + generate writes the probes to /tb (a tree for scripts/tbrun.mjs --exe) + and /vb6 (Probe.vbp), without running them + write writes each page's Common Errors section from the data, and the index of + the numbers on the central page, docs/Reference/Run-time-Errors.md + messages builds a probe of the messages for each runtime, runs it, and + compares what Error$(n) and Err.Raise n give with errors/messages.yml: + every number from 1 to 65535 with a message of its own, and every + other number the file lists + + --only only the pages whose path under docs/ matches + --runtime R tb (32-bit twinBASIC), vb6 or win64 (64-bit twinBASIC); may be + repeated. By default tb and vb6, and win64 for a file that says + win64: true; messages measures tb and vb6 only + --ide P the twinBASIC IDE, as for tbrun (default $TB_IDE, or the newest + on the Desktop) + --vb6 P VB6.EXE (default $VB6_EXE, or VB6's own install folder) + --keep run, messages: keep the generated probes, and print where + -h, --help print this text and exit + +Exit codes: + 0 run, messages: every runtime agrees with the data; generate, write: done + 1 run, messages: a runtime disagrees with the data + 2 a refused command line, bad data, or a runtime that could not be run`; + +const RUNTIMES = ["tb", "vb6", "win64"]; +const COMMANDS = ["run", "generate", "write", "messages"]; +const TBRUN = path.join(REPO_ROOT, "scripts", "tbrun.mjs"); +const TB_TEMPLATE = path.join(REPO_ROOT, "test", "example-projects", "console", "Settings"); +const VB6_PROJECT = path.join(REPO_ROOT, "test", "repro-templates", "vb6", "Probe.vbp"); +const ROUNDS = 25; + +const { values, positionals } = withUsageError( + () => + parseCli(process.argv.slice(2), { + options: { + only: { type: "string" }, + runtime: { type: "string", multiple: true }, + ide: { type: "string" }, + vb6: { type: "string" }, + keep: { type: "boolean", default: false }, + help: { type: "boolean", short: "h" }, + }, + positionals: { min: 0 }, + stopAt: ["help"], + }), + { format: (err) => `${err.message}\n${USAGE}` }, +); +if (values.help) printHelpAndExit(USAGE); + +const usage = (msg) => die(2, `${msg}\n${USAGE}`); +const [command, ...rest] = positionals; +if (!COMMANDS.includes(command)) usage(command ? `unknown command: ${command}` : "give a command"); +const only = withUsageError(() => (values.only ? regexOption(values.only, { option: "--only" }) : null)); +for (const r of values.runtime) if (!RUNTIMES.includes(r)) usage(`--runtime: one of ${RUNTIMES.join(", ")}`); +let outDir = null; +if (command === "generate") { + if (!rest.length) usage("generate needs the folder to write to"); + outDir = path.resolve(rest.shift()); +} +if (command !== "run" && command !== "messages") { + for (const k of ["runtime", "ide", "vb6"]) if (values[k]?.length) usage(`--${k} is for run and messages`); + if (values.keep) usage("--keep is for run and messages"); +} +if ((command === "write" || command === "messages") && only) usage("--only is for run and generate"); +if (command === "messages") { + if (rest.length) usage("messages takes no data file: it measures errors/messages.yml"); + if (values.runtime.includes("win64")) usage("--runtime: messages measures tb and vb6"); +} + +// ------------------------------------------------------------------ the data + +const known = dataFiles(); +const names = rest.length ? rest.map((n) => path.basename(n)) : known; +for (const n of names) if (!known.includes(n)) die(2, `no such data file: errors/${n}`); +const files = names.map((n) => loadDataFile(n)); +const { messages, problems: messageProblems } = loadMessages(); +const problems = [...messageProblems, ...files.flatMap((f) => f.problems)]; +if (problems.length) + die(2, `errors: the data has problems; scripts/check_errors.mjs lists them all\n ${problems.join("\n ")}`); + +if (command === "write") { + let changed = 0; + for (const f of files) { + for (const p of f.pages) { + const file = pagePath(p.page); + let text; + try { + text = readFileSync(file, "utf8"); + } catch { + die(2, `errors/${f.name}: no such page: docs/${p.page}`); + } + const { problems: marks } = findSection(text); + const meta = pageMeta(text); + if (!meta.title || !meta.permalink) marks.push("no title: or permalink: in the front matter, to link it by"); + if (marks.length) die(2, `docs/${p.page}: ${marks.join("; ")}`); + const section = renderSection(p, { + name: f.name, + beta: f.beta, + messages, + level: pageLevel(text), + }); + const next = spliceSection(text, section); + if (next !== text) { + writeFileSync(file, next); + changed++; + console.log(`wrote docs/${p.page}`); + } + } + } + // The index names the pages of every data file, whichever were named. + const every = known.map((n) => loadDataFile(n)); + const metas = new Map(); + for (const f of every) for (const p of f.pages) metas.set(p.page, pageMeta(readFileSync(pagePath(p.page), "utf8"))); + const centralFile = pagePath(CENTRAL_PAGE); + const central = readFileSync(centralFile, "utf8"); + const { text: index, problem } = spliceIndex(central, renderIndex(every, messages, metas)); + if (problem) die(2, `docs/${CENTRAL_PAGE}: ${problem}`); + if (index !== central) { + writeFileSync(centralFile, index); + changed++; + console.log(`wrote docs/${CENTRAL_PAGE}`); + } + console.log(`errors: ${changed} page${changed === 1 ? "" : "s"} written`); + process.exit(0); +} + +// ------------------------------------------------------------------ the probes + +// The module is the cases' probe, or `text` when it is given (the messages' probe). +function writeTbTree(dir, cases, text) { + mkdirSync(path.join(dir, "Sources"), { recursive: true }); + cpSync(TB_TEMPLATE, path.join(dir, "Settings")); + const mod = text === undefined ? probeModule(cases, "tb") : { text, spans: [] }; + writeFileSync(path.join(dir, "Sources", "Probe.twin"), mod.text); + return mod; +} + +function writeVb6Project(dir, cases, text) { + mkdirSync(dir, { recursive: true }); + cpSync(VB6_PROJECT, path.join(dir, "Probe.vbp")); + const mod = text === undefined ? probeModule(cases, "vb6") : { text, spans: [] }; + writeFileSync(path.join(dir, "Module1.bas"), encodeAnsi(mod.text)); + return mod; +} + +if (command === "generate") { + const tb = probeCases(files, "tb", { only }); + const vb6 = probeCases(files, "vb6", { only }); + writeTbTree(path.join(outDir, "tb"), tb); + writeVb6Project(path.join(outDir, "vb6"), vb6); + console.log(`errors: ${tb.length} cases in ${path.join(outDir, "tb")}, ${vb6.length} in ${path.join(outDir, "vb6")}`); + process.exit(0); +} + +// run: node scripts/tbrun.mjs as a child, as bug_repro does; it runs on import. +function runNode(args) { + return new Promise((resolve) => { + const child = spawn(process.execPath, [TBRUN, ...args], { + cwd: REPO_ROOT, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (d) => { + stdout += d; + }); + child.stderr.on("data", (d) => { + stderr += d; + }); + child.on("close", (code) => resolve({ code, stdout, stderr })); + }); +} + +const work = mkdtempSync(path.join(tmpdir(), "errors-")); + +// What each case raised in one runtime: Map id -> { number, description } or +// { refused: message } or { missing: true }; plus a note for the harness. +async function measure(runtime, cases, run) { + const results = new Map(); + let left = cases; + for (let round = 1; round <= ROUNDS && left.length; round++) { + const dir = path.join(work, `${runtime}-${round}`); + const got = await run(dir, left); + if (got.harness) return { results, harness: got.harness }; + if (got.refused) { + for (const [id, message] of got.refused) results.set(id, { refused: message }); + left = left.filter((c) => !got.refused.has(c.id)); + continue; + } + const { seen, done } = readProbeOutput(got.lines); + for (const c of left) results.set(c.id, seen.get(c.id) ?? { missing: true }); + return { results, harness: done ? null : `the probe stopped before it ended${got.note ? `: ${got.note}` : ""}` }; + } + return { results, harness: left.length ? `still refused after ${ROUNDS} builds` : null }; +} + +async function runTb(dir, cases, arch, text) { + const mod = writeTbTree(dir, cases, text); + const args = [dir, "--exe", "--json", "--arch", arch]; + if (text !== undefined) args.push("--exe-run-timeout", "600"); + if (values.ide) args.push("--ide", values.ide); + const r = await runNode(args); + if (r.code === 1) { + const refused = new Map(); + for (const m of r.stdout.matchAll(/^\{ERROR\} \S*Probe\.twin \[(\d+),\d+\]: (.*)$/gm)) { + const id = caseAtLine(mod.spans, Number(m[1])); + if (id === null) return { harness: `a compile error outside every case: ${m[0]}` }; + refused.set(id, m[2]); + } + if (refused.size) return { refused }; + } + let json = null; + try { + json = JSON.parse(r.stdout.slice(r.stdout.indexOf("{"))); + } catch {} + if (!json?.exeRun) + return { harness: `tbrun exit ${r.code}: ${(r.stderr || r.stdout).trim().split("\n").slice(-3).join(" / ")}` }; + const e = json.exeRun; + const note = [e.fault, e.timedOut && "timed out", ...e.dialogs.map((d) => `a box: ${d.text}`)] + .filter(Boolean) + .join("; "); + return { lines: e.lines, note }; +} + +async function runVb6(dir, cases, vb6, text) { + const mod = writeVb6Project(dir, cases, text); + let r; + try { + r = await runRepro(vb6, dir, { timeoutMs: text === undefined ? 120000 : 600000 }); + } catch (e) { + return { harness: e.message.split("\n")[0] }; + } + if (!r.built) { + // VB6 reports the first error only. Its line counts from 0 and skips the + // Attribute line: the file's line less two. + const { line, message } = parseMakeLog(r.log, ["Module1"]); + const id = line === null ? null : caseAtLine(mod.spans, line + 2); + if (id === null) return { harness: `VB6 did not build the probe: ${message}` }; + return { refused: new Map([[id, message]]) }; + } + const note = [r.fault, r.timedOut && "timed out", ...r.dialogs.map((d) => `a box: ${d.text}`)] + .filter(Boolean) + .join("; "); + return { lines: r.lines, note }; +} + +if (command === "messages") { + // The numbers past VB's own that the list gives are measured as well. + const others = [...messages.keys()].filter((n) => n < VB_NUMBERS.from || n > VB_NUMBERS.to).sort((a, b) => a - b); + const runtimes = values.runtime.length ? [...new Set(values.runtime)] : ["tb", "vb6"]; + const vb6 = runtimes.includes("vb6") ? findVb6(values.vb6) : null; + if (runtimes.includes("vb6") && !vb6) die(2, values.vb6 ? `no such file: ${values.vb6}\n${NO_VB6}` : NO_VB6); + const run = { + tb: (d) => runTb(d, [], "win32", messageProbeModule(others, "tb")), + vb6: (d) => runVb6(d, [], vb6, messageProbeModule(others, "vb6")), + }; + const seen = {}; + const broken = []; + await Promise.all( + runtimes.map(async (rt) => { + const got = await run[rt](path.join(work, rt)); + const read = got.lines ? readMessageOutput(got.lines) : null; + if (read?.done) seen[rt] = read.seen; + else if (got.harness) broken.push(`${rt}: ${got.harness}`); + else if (got.refused) broken.push(`${rt}: the probe did not build: ${[...got.refused.values()].join("; ")}`); + else broken.push(`${rt}: the probe stopped before it ended${got.note ? `: ${got.note}` : ""}`); + }), + ); + if (values.keep) console.log(`errors: the probes are in ${work}`); + else rmSync(work, { recursive: true, force: true }); + if (broken.length) die(2, broken.join("\n")); + const wrong = compareMessages(messages, seen); + for (const w of wrong) console.log(` ${w}`); + const against = runtimes.join(" and "); + console.log( + `errors: ${messages.size} numbers in messages.yml, ${wrong.length ? `${wrong.length} disagreements with ${against}` : `all agree with ${against}`}`, + ); + process.exit(wrong.length ? 1 : 0); +} + +const wanted = values.runtime.length ? values.runtime : null; +const plan = []; +const want = (rt) => (wanted ? wanted.includes(rt) : rt !== "win64"); +if (want("tb")) plan.push(["tb", (d, c) => runTb(d, c, "win32")]); +if (wanted ? wanted.includes("win64") : files.some((f) => f.win64)) { + plan.push(["win64", (d, c) => runTb(d, c, "win64")]); +} +if (want("vb6")) { + const vb6 = findVb6(values.vb6); + if (!vb6) die(2, values.vb6 ? `no such file: ${values.vb6}\n${NO_VB6}` : NO_VB6); + plan.push(["vb6", (d, c) => runVb6(d, c, vb6)]); +} + +const caseLists = Object.fromEntries( + plan.map(([rt]) => { + // win64 is measured for the files that ask for it, or for all when --runtime names it. + const from = rt === "win64" && !wanted?.includes("win64") ? files.filter((f) => f.win64) : files; + return [rt, probeCases(from, rt, { only })]; + }), +); +const measured = Object.fromEntries( + await Promise.all(plan.map(async ([rt, run]) => [rt, await measure(rt, caseLists[rt], run)])), +); +if (values.keep) console.log(`errors: the probes are in ${work}`); +else rmSync(work, { recursive: true, force: true }); + +// ------------------------------------------------------------------ the report + +let disagreements = 0; +let harness = 0; +for (const [rt] of plan) { + if (measured[rt].harness) { + harness++; + console.log(`${rt}: ${measured[rt].harness}`); + } +} +const shown = (r) => + !r ? "-" : r.refused ? "refused" : r.missing ? "none" : `${r.number}${r.setup ? " (setup)" : ""}`; +let lastPage = null; +const all = new Map(); +for (const [rt] of plan) for (const c of caseLists[rt]) all.set(c.id, c); +for (const c of [...all.values()].sort((a, b) => a.id - b.id)) { + if (c.page !== lastPage) { + console.log(`\ndocs/${c.page} (errors/${c.file})`); + lastPage = c.page; + } + const cells = []; + const wrong = []; + for (const [rt] of plan) { + const mine = caseLists[rt].find((x) => x.id === c.id); + if (!mine) continue; + const r = measured[rt].results.get(c.id); + cells.push(`${rt} ${shown(r)}`); + if (!r) continue; + if (r.refused) wrong.push(`${rt} refused it: ${r.refused}`); + else if (r.missing) wrong.push(`${rt} reported nothing`); + else if (r.setup) wrong.push(`${rt}: its setup raised ${r.number}, ${r.description}`); + else if (r.number !== mine.expect) wrong.push(`${rt} raised ${r.number}, the data says ${mine.expect}`); + else if (r.number !== 0 && r.description !== messages.get(r.number)?.raised) { + wrong.push( + `${rt}'s message is "${r.description}", errors/messages.yml has "${messages.get(r.number)?.raised ?? "nothing"}"`, + ); + } + } + console.log( + ` ${wrong.length ? "FAIL" : "ok "} ${String(c.row ? c.row.error : 0).padStart(11)} ${c.label} [${cells.join(", ")}]`, + ); + for (const w of wrong) console.log(` ${w}`); + if (wrong.length) disagreements++; +} +console.log( + `\nerrors: ${all.size} cases, ${disagreements ? `${disagreements} disagree with the data` : "all agree with the data"}`, +); +process.exit(harness ? 2 : disagreements ? 1 : 0); diff --git a/scripts/lib/cli-cases.mjs b/scripts/lib/cli-cases.mjs index e7dcd2ae9..f07833129 100644 --- a/scripts/lib/cli-cases.mjs +++ b/scripts/lib/cli-cases.mjs @@ -123,6 +123,18 @@ const CASES = [ { tool: "scripts/vb6run.mjs", args: ["--docs", "--timeout"], exit: 2, stderr: /^--timeout needs a value\nusage: node scripts\/vb6run\.mjs / }, { tool: "scripts/vb6run.mjs", args: ["no-such-file.bas"], exit: 2, stderr: "cannot read no-such-file.bas: no such file\n" }, { tool: "scripts/vb6run.mjs", args: ["--docs", "--vb6", "no-such/VB6.EXE"], exit: 2, stderr: /^no such file: no-such\/VB6\.EXE\nno VB6 found: pass --vb6 / }, + { tool: "scripts/errors.mjs", args: [], exit: 2, stderr: /^give a command\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["measure"], exit: 2, stderr: /^unknown command: measure\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["run", "--help", "--bogus"], exit: 0, stdout: /^usage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["generate"], exit: 2, stderr: /^generate needs the folder to write to\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["run", "--runtime", "vb5"], exit: 2, stderr: /^--runtime: one of tb, vb6, win64\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["write", "--only", "Mid"], exit: 2, stderr: /^--only is for run and generate\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["write", "--keep"], exit: 2, stderr: /^--keep is for run and messages\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["generate", "x", "--runtime", "tb"], exit: 2, stderr: /^--runtime is for run and messages\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["messages", "Core.yml"], exit: 2, stderr: /^messages takes no data file: it measures errors\/messages\.yml\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["messages", "--runtime", "win64"], exit: 2, stderr: /^--runtime: messages measures tb and vb6\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["messages", "--only", "Mid"], exit: 2, stderr: /^--only is for run and generate\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["write", "no-such.yml"], exit: 2, stderr: "no such data file: errors/no-such.yml\n" }, { tool: "scripts/addin_test.mjs", args: ["--help"], exit: 0, stdout: /^usage: node scripts\/addin_test\.mjs / }, { tool: "scripts/addin_test.mjs", args: ["--ide"], exit: 2, stderr: "--ide needs a value\n" }, { tool: "scripts/addin_test.mjs", args: ["--ide", ""], exit: 2, stderr: "--ide needs a non-empty value\n" }, @@ -391,6 +403,7 @@ const HELP_TOOLS = { "scripts/check_attribute_sweep.mjs": null, "scripts/check_book_coverage.mjs": null, "scripts/check_ci_workflows.mjs": null, + "scripts/check_errors.mjs": null, "scripts/check_cli.mjs": null, "scripts/check_code_regions.mjs": null, "scripts/check_dot_fit.mjs": null, @@ -412,6 +425,7 @@ const HELP_TOOLS = { "scripts/compare_trees.mjs": null, "scripts/convert_em_dash_separators.mjs": null, "scripts/crawl_check.mjs": null, + "scripts/errors.mjs": null, "scripts/gen_attribute_probes.mjs": "Generate a twinBASIC probe project for Reference/Attributes.md applicability.\n", "scripts/ide_js.mjs": null, "scripts/impexp.mjs": "Usage:\n", @@ -484,6 +498,7 @@ const REFUSALS = { "scripts/check_links_diff.mjs": ["a"], "scripts/check_lint.mjs": [null, { prefix: "check_lint: " }], "scripts/check_package_tests.mjs": [null], + "scripts/check_errors.mjs": [null], "scripts/check_page_baseline.mjs": [null], "scripts/check_pdf_shims_equiv.mjs": [null], "scripts/check_publish_policy.mjs": ["src"], @@ -511,6 +526,7 @@ const REFUSALS = { "scripts/tbbuild.mjs": ["ide"], "scripts/tbrun.mjs": ["ide"], "scripts/vb6run.mjs": ["vb6"], + "scripts/errors.mjs": ["only"], }; // The cases whose folder must stay empty, as for a help request: a refusal // starts no IDE or browser and writes nothing, and a tool that read the flag as diff --git a/scripts/lib/errors-data.mjs b/scripts/lib/errors-data.mjs new file mode 100644 index 000000000..736f948e3 --- /dev/null +++ b/scripts/lib/errors-data.mjs @@ -0,0 +1,587 @@ +// What scripts/errors.mjs and scripts/check_errors.mjs share: reading the run-time +// error data under errors/, the probe projects generated from it, and the `Common Errors` +// section each page gets from it. The format is written down in WIP.Errors.md. +// +// Every export here does its work when called, never on import. + +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; +import yaml from "js-yaml"; +import { parseFrontmatter } from "../../lib/frontmatter.mjs"; +import { DOCS_DIR, REPO_ROOT } from "../../lib/repo-paths.mjs"; + +export const ERRORS_DIR = path.join(REPO_ROOT, "errors"); +export const MESSAGES_FILE = "messages.yml"; + +const ROW_KEYS = new Set(["error", "when", "cases", "vb6", "win64", "bug"]); +const CASE_KEYS = new Set(["expr", "run", "decl", "setup", "only"]); +const PAGE_KEYS = new Set(["rows", "quiet", "none"]); +const FILE_KEYS = new Set(["beta", "win64", "pages"]); + +// ------------------------------------------------------------------ the data + +/** The data files under `dir`, as names relative to it, sorted; messages.yml is not one. */ +export function dataFiles(dir = ERRORS_DIR) { + if (!existsSync(dir)) return []; + return readdirSync(dir) + .filter((n) => n.endsWith(".yml") && n !== MESSAGES_FILE) + .sort(); +} + +/** What Error$(n) gives for a number with no message of its own, in both runtimes. */ +export const GENERIC_MESSAGE = "Application-defined or object-defined error"; + +/** + * errors/messages.yml read: Map number -> { raised, error }, where `raised` is the + * Err.Description of the error once raised and `error` what Error$(number) gives. + * The file gives a number's one message, or `{ raised, error }` where they differ. + */ +export function readMessages(raw, name = MESSAGES_FILE) { + const messages = new Map(); + const problems = []; + const text = (v) => typeof v === "string" && v.length > 0; + for (const [k, v] of Object.entries(raw ?? {})) { + const pair = v && typeof v === "object" && !Array.isArray(v); + if (!/^-?\d+$/.test(k) || !(text(v) || (pair && text(v.raised) && text(v.error)))) + problems.push(`${name}: ${k}: not a number and its message, or its raised: and error: messages`); + else if (pair && Object.keys(v).some((x) => x !== "raised" && x !== "error")) + problems.push(`${name}: ${k}: only raised: and error:`); + else if (pair && v.raised === v.error) problems.push(`${name}: ${k}: raised: and error: agree; give one message`); + else messages.set(Number(k), pair ? { raised: v.raised, error: v.error } : { raised: v, error: v }); + } + return { messages, problems }; +} + +/** errors/messages.yml, read by readMessages, or a problem. */ +export function loadMessages(dir = ERRORS_DIR) { + const file = path.join(dir, MESSAGES_FILE); + if (!existsSync(file)) return { messages: new Map(), problems: [`${MESSAGES_FILE}: missing`] }; + return readMessages(yaml.load(readFileSync(file, "utf8"))); +} + +const isInt = (v) => Number.isInteger(v); + +// A case as the data writes it, a string (an expression) or a mapping, made one shape: +// { expr?, run?, decl: [], setup: [], only? } +function normaliseCase(c, where, problems) { + if (typeof c === "string") return { expr: c, decl: [], setup: [] }; + if (!c || typeof c !== "object" || Array.isArray(c)) { + problems.push(`${where}: a case is an expression or a mapping`); + return null; + } + for (const k of Object.keys(c)) if (!CASE_KEYS.has(k)) problems.push(`${where}: unknown key ${k}`); + if ((c.expr === undefined) === (c.run === undefined)) problems.push(`${where}: give one of expr and run`); + if (c.only !== undefined && c.only !== "tb") problems.push(`${where}: only: can only be tb`); + const lines = (v) => (v === undefined ? [] : String(v).replace(/\r/g, "").replace(/\n$/, "").split("\n")); + return { + expr: c.expr === undefined ? undefined : String(c.expr), + run: c.run === undefined ? undefined : lines(c.run), + decl: lines(c.decl), + setup: lines(c.setup), + only: c.only, + }; +} + +/** + * One data file, read and checked. `name` is relative to `dir`. + * @returns {{name, beta, win64, pages: Array<{page, rows, quiet, none}>, problems: string[]}} + * a row is { error, when, vb6?, win64?, bug?, cases }; `page` is relative to docs/ + */ +export function loadDataFile(name, dir = ERRORS_DIR) { + const problems = []; + let raw; + try { + raw = yaml.load(readFileSync(path.join(dir, name), "utf8")); + } catch (e) { + return { name, pages: [], problems: [`${name}: ${e.message.split("\n")[0]}`] }; + } + return { name, ...readData(raw, name, problems), problems }; +} + +/** The checking half of loadDataFile, over an already parsed document. */ +export function readData(raw, name, problems) { + if (!raw || typeof raw !== "object") { + problems.push(`${name}: not a mapping`); + return { beta: null, win64: false, pages: [] }; + } + for (const k of Object.keys(raw)) if (!FILE_KEYS.has(k)) problems.push(`${name}: unknown key ${k}`); + if (!isInt(raw.beta)) problems.push(`${name}: beta: the BETA the data was measured on, a number`); + const pages = []; + for (const [page, entry] of Object.entries(raw.pages ?? {})) { + const at = `${name}: ${page}`; + if (!page.endsWith(".md") || page.includes("\\")) + problems.push(`${at}: a page is a path under docs/, with / and .md`); + if (!entry || typeof entry !== "object") { + problems.push(`${at}: not a mapping`); + continue; + } + for (const k of Object.keys(entry)) if (!PAGE_KEYS.has(k)) problems.push(`${at}: unknown key ${k}`); + const rows = []; + (entry.rows ?? []).forEach((r, i) => { + const w = `${at}: row ${i + 1}`; + if (!r || typeof r !== "object") { + problems.push(`${w}: not a mapping`); + return; + } + for (const k of Object.keys(r)) if (!ROW_KEYS.has(k)) problems.push(`${w}: unknown key ${k}`); + // error: 0 is a condition where twinBASIC raises nothing and VB6 does: a NOTE, no table row. + if (!isInt(r.error) || (r.error === 0 && !r.vb6)) { + problems.push(`${w}: error: a number other than 0, or 0 with a vb6: other than 0`); + } + if (typeof r.when !== "string" || !r.when.trim()) + problems.push(`${w}: when: the condition, in the reader's terms`); + else if (/\||\n/.test(r.when.trim())) problems.push(`${w}: when: one line, without |`); + if (r.vb6 !== undefined && !isInt(r.vb6)) problems.push(`${w}: vb6: a number`); + if (r.vb6 !== undefined && r.vb6 === r.error) problems.push(`${w}: vb6: the same as error; leave it out`); + if (r.vb6 !== undefined && typeof r.bug !== "string") { + problems.push(`${w}: bug: a row that differs from VB6 names its bugs/ slug, or pending`); + } + if (r.bug !== undefined && r.vb6 === undefined) problems.push(`${w}: bug: only on a row with vb6:`); + if (r.win64 !== undefined && !isInt(r.win64)) problems.push(`${w}: win64: a number`); + if (!Array.isArray(r.cases) || !r.cases.length) problems.push(`${w}: cases: at least one`); + const cases = (Array.isArray(r.cases) ? r.cases : []) + .map((c, j) => normaliseCase(c, `${w} case ${j + 1}`, problems)) + .filter(Boolean); + rows.push({ error: r.error, when: String(r.when ?? "").trim(), vb6: r.vb6, win64: r.win64, bug: r.bug, cases }); + }); + const quiet = (entry.quiet ?? []) + .map((c, j) => normaliseCase(c, `${at}: quiet ${j + 1}`, problems)) + .filter(Boolean); + if (entry.none !== undefined && (typeof entry.none !== "string" || rows.length)) { + problems.push(`${at}: none: a sentence, on a page without rows`); + } + if (!rows.length && !quiet.length) problems.push(`${at}: neither rows nor quiet cases`); + pages.push({ page, rows, quiet, none: entry.none }); + } + return { beta: raw.beta ?? null, win64: raw.win64 === true, pages }; +} + +// ------------------------------------------------------------------ the probes + +/** + * The cases of `files` to run in `runtime` ("tb", "vb6" or "win64"), each with + * what it expects. Ids are 1-based and stable for one list of files. + */ +export function probeCases(files, runtime, { only } = {}) { + const out = []; + let id = 0; + for (const f of files) { + for (const p of f.pages) { + const take = !only || only.test(p.page); + const push = (c, row, label) => { + id++; + if (!take || (runtime === "vb6" && c.only === "tb")) return; + let expect = row ? row.error : 0; + if (row && runtime === "vb6" && row.vb6 !== undefined) expect = row.vb6; + if (row && runtime === "win64" && row.win64 !== undefined) expect = row.win64; + out.push({ id, file: f.name, page: p.page, label, row, expect, ...c }); + }; + for (const row of p.rows) for (const c of row.cases) push(c, row, c.expr ?? c.run.join(" : ")); + for (const c of p.quiet) push(c, null, c.expr ?? c.run.join(" : ")); + } + } + return out; +} + +// A case's own procedure. `report` is the name of the procedure that writes a line. +function caseLines(c, report) { + const ind = (l) => ` ${l}`; + return [ + `Private Sub C${c.id}()`, + ind("Dim Result As Variant"), + ...c.decl.map(ind), + ind("On Error Resume Next"), + ...c.setup.map(ind), + ind(`If Err.Number <> 0 Then ${report} "setup ${c.id} " & Err.Number & " " & Err.Description: Exit Sub`), + ...(c.expr !== undefined ? [ind(`Result = ${c.expr}`)] : c.run.map(ind)), + ind(`${report} "case ${c.id} " & Err.Number & " " & Err.Description`), + "End Sub", + ]; +} + +/** + * The probe module for `cases`: the twinBASIC module (dialect "tb", a .twin file run as + * an exe by tbrun) or the VB6 one (dialect "vb6", Module1.bas of Probe.vbp). Each case + * is a Sub of its own, run under On Error Resume Next, that reports + * `case `; Main reports `done` last. + * @returns {{text: string, spans: Array<{id, from, to}>}} spans are 1-based file lines + */ +export function probeModule(cases, dialect) { + const head = + dialect === "tb" + ? [ + "' Generated by scripts/errors.mjs from errors/*.yml; do not edit.", + "Module Probe", + "Private Sub Report(ByVal S As String)", + " TbRun.Out S", + "End Sub", + "[RunAfterBuild]", + "Private Sub Built()", + " Debug.Cls", + ' TbRun.Out "ide"', + "End Sub", + ] + : [ + 'Attribute VB_Name = "Module1"', + "' Generated by scripts/errors.mjs from errors/*.yml; do not edit.", + "Option Explicit", + "Private Sub Report(ByVal S As String)", + " Dim F As Integer", + " F = FreeFile", + ' Open App.Path & "\\out.txt" For Append As #F', + " Print #F, S", + " Close #F", + "End Sub", + ]; + const lines = [...head, "Public Sub Main()", ...cases.map((c) => ` C${c.id}`), ' Report "done"', "End Sub"]; + const spans = []; + for (const c of cases) { + const from = lines.length + 1; + lines.push(...caseLines(c, "Report")); + spans.push({ id: c.id, from, to: lines.length }); + } + if (dialect === "tb") lines.push("End Module"); + return { text: `${lines.join("\r\n")}\r\n`, spans }; +} + +// ------------------------------------------------------------------ the messages + +/** The numbers whose message is VB's own: Error$ is defined for these. */ +export const VB_NUMBERS = { from: 1, to: 65535 }; + +/** + * The probe of the messages, in `dialect` as probeModule's: for every number from 1 + * to 65535 whose Error$ is not GENERIC_MESSAGE, and for each of `others`, a line + * `msg `, tab-separated, after + * `Err.Raise n`. An Error$ that raises is written `!`, and a line break in a + * description `\n`. + */ +export function messageProbeModule(others, dialect) { + const one = [ + "Private Sub One(ByVal N As Long, ByVal All As Boolean)", + " Dim E As String, D As String", + " On Error Resume Next", + " E = Error$(N)", + ' If Err.Number <> 0 Then E = "!" & Err.Number', + ` If Not All And E = "${GENERIC_MESSAGE}" Then Exit Sub`, + " Err.Clear", + " Err.Raise N", + ' D = Replace(Replace(Err.Description, vbCr, ""), vbLf, "\\n")', + ' Report "msg" & vbTab & N & vbTab & E & vbTab & Err.Number & vbTab & D', + " Err.Clear", + "End Sub", + ]; + const main = [ + "Public Sub Main()", + " Dim N As Long", + ` For N = ${VB_NUMBERS.from} To ${VB_NUMBERS.to}`, + " One N, False", + " Next", + ...others.map((n) => ` One ${n}, True`), + ' Report "done"', + "End Sub", + ]; + const { text } = probeModule([], dialect); + const lines = text.replace(/\r\n$/, "").split("\r\n"); + const at = lines.indexOf("Public Sub Main()"); + const end = lines.indexOf("End Sub", at); + lines.splice(at, end - at + 1, ...one, ...main); + return `${lines.join("\r\n")}\r\n`; +} + +/** A message probe's output: Map n -> { error, number, description }, and whether it ended. */ +export function readMessageOutput(lines) { + const seen = new Map(); + for (const l of lines) { + const p = l.split("\t"); + if (p[0] === "msg" && p.length === 5) + seen.set(Number(p[1]), { error: p[2], number: Number(p[3]), description: p[4] }); + } + return { seen, done: lines.includes("done") }; +} + +/** + * What is wrong with `messages` (readMessages's) against what the probes saw, one + * line each. Every number from 1 to 65535 with a message of its own in either + * runtime must be listed; each listed number's Error$ must be its `error` in both + * runtimes, and the description of the error raised its `raised`: in both runtimes + * for a number from 1 to 65535, in twinBASIC only for another (VB6's Err.Raise turns + * many HRESULTs into its own numbers). `seen` maps a runtime to readMessageOutput's map. + */ +export function compareMessages(messages, seen) { + const out = []; + const vbs = (n) => n >= VB_NUMBERS.from && n <= VB_NUMBERS.to; + for (const [rt, got] of Object.entries(seen)) { + for (const [n, g] of got) if (vbs(n) && !messages.has(n)) out.push(`${n}: ${rt} has "${g.error}", not listed`); + for (const [n, m] of messages) { + const g = got.get(n); + if (!g) { + if (vbs(n)) out.push(`${n}: ${rt}'s Error$ is "${GENERIC_MESSAGE}", the list has "${m.error}"`); + continue; + } + if (g.error !== m.error) out.push(`${n}: ${rt}'s Error$ is "${g.error}", the list has "${m.error}"`); + if (rt !== "vb6" || vbs(n)) { + if (g.number !== n) out.push(`${n}: ${rt}'s Err.Raise ${n} raised ${g.number}`); + else if (g.description !== m.raised) + out.push(`${n}: ${rt}'s description is "${g.description}", the list has "${m.raised}"`); + } + } + } + return out; +} + +/** The id of the case whose procedure holds 1-based `line`, or null. */ +export const caseAtLine = (spans, line) => spans.find((s) => line >= s.from && line <= s.to)?.id ?? null; + +/** + * A probe's output lines, read: Map id -> { number, description, setup }, and + * whether `done` was reached. + */ +export function readProbeOutput(lines) { + const seen = new Map(); + let done = false; + for (const l of lines) { + if (l === "done") done = true; + const m = /^(case|setup) (\d+) (-?\d+) ?(.*)$/.exec(l); + if (m) seen.set(Number(m[2]), { number: Number(m[3]), description: m[4], setup: m[1] === "setup" }); + } + return { seen, done }; +} + +// ------------------------------------------------------------------ the section + +export const OPEN_MARKER = /^[ \t]*$/; +export const CLOSE_MARKER = /^[ \t]*$/; +const FENCE = /^[ \t]*(`{3,}|~{3,})/; +const ANCHOR = /^(#{2,6})[ \t]+(Examples?|See Also)[ \t]*$/; + +/** The page's lines with the fence state of each: [{text, fenced}]. */ +function scan(lines) { + let fence = null; + return lines.map((text) => { + const m = FENCE.exec(text); + const fenced = fence !== null || Boolean(m); + if (m) { + if (fence === null) fence = m[1]; + else if (m[1][0] === fence[0] && m[1].length >= fence.length && text.trim() === m[1]) fence = null; + } + return { text, fenced }; + }); +} + +/** + * The `errors` region of a page: { open, close } as 0-based line indexes (close + * inclusive), null when there is none, and problems with its markers. + */ +export function findSection(text) { + const lines = scan(text.split("\n")); + const problems = []; + let open = null; + let found = null; + lines.forEach(({ text: t, fenced }, i) => { + if (fenced) return; + const l = t.replace(/\r$/, ""); + if (OPEN_MARKER.test(l)) { + if (open !== null || found) problems.push(`line ${i + 1}: a second marker`); + else open = i; + } else if (CLOSE_MARKER.test(l)) { + if (open === null) problems.push(`line ${i + 1}: without its opening marker`); + else { + found = { open, close: i }; + open = null; + } + } + }); + if (open !== null) problems.push(`line ${open + 1}: the errors section is not closed with `); + return { section: found, problems }; +} + +/** The heading level the page uses for Example and See Also: "##" or "###"; "##" when it has neither. */ +export function pageLevel(text) { + for (const { text: t, fenced } of scan(text.split("\n"))) { + const m = !fenced && ANCHOR.exec(t.replace(/\r$/, "")); + if (m) return m[1]; + } + return "##"; +} + +/** An error number in decimal and in VB hexadecimal, as a 32-bit Long: -2147467259 (`&H80004005`). */ +export const errorNumber = (n) => `${n} (\`&H${(n >>> 0).toString(16).toUpperCase()}\`)`; + +const escapeCell = (s) => s.replace(/\|/g, "\\|"); + +/** The central page of the run-time errors, as a path under docs/, and its permalink. */ +export const CENTRAL_PAGE = "Reference/Run-time-Errors.md"; +export const CENTRAL_PERMALINK = "/Reference/Run-time-Errors"; +/** The central page's shortcut (`shortcut:` in its front matter): sections link to it as tb:Errors#5. */ +export const CENTRAL_SHORTCUT = "Errors"; + +/** The anchor of number `n` on the central page, the number itself: 5, -2147467259 (tb:Errors#5). */ +export const errorAnchor = (n) => `${n}`; + +/** The anchor of number `n`'s list of pages on the central page: listed-5, listed--2147467259. */ +export const listedAnchor = (n) => `listed-${n}`; + +/** The link from the page at permalink `from` to the page at permalink `to`, as the site writes it: relative. */ +export function relativeLink(from, to) { + const base = from.endsWith("/") ? from : path.posix.dirname(from); + const rel = path.posix.relative(base, to.endsWith("/") ? to.slice(0, -1) : to) || "."; + return to.endsWith("/") && rel !== "." ? `${rel}/` : rel; +} + +/** A page's title and permalink, from its front matter; null for either it lacks. */ +export function pageMeta(text) { + let data = {}; + try { + data = parseFrontmatter(text)?.data ?? {}; + } catch {} + const s = (v) => (typeof v === "string" && v ? v : null); + return { title: s(data.title), permalink: s(data.permalink), shortcut: s(data.shortcut) }; +} + +/** + * The section's lines for page entry `p` of data file `name`, or null when the page + * gets none. `level` is the heading's "##"/"###". Each number links to its row on + * the central page through the page's shortcut, tb:Errors#5. + */ +export function renderSection(p, { name, beta, messages, level }) { + if (!p.rows.length && p.none === undefined) return null; + const msg = (n) => messages.get(n)?.raised ?? "?"; + const num = (n) => `[${errorNumber(n)}](tb:${CENTRAL_SHORTCUT}#${errorAnchor(n)})`; + const out = [ + ``, + `${level} Common Errors`, + "", + ]; + if (p.none !== undefined) out.push(p.none.trim()); + else { + // Rows that raise the same number share one row, their conditions a line each. + // A row of error 0 raises nothing in twinBASIC: it has its NOTE and no table row. + const byError = new Map(); + for (const r of p.rows.filter((x) => x.error !== 0)) + byError.set(r.error, [...(byError.get(r.error) ?? []), escapeCell(r.when)]); + if (byError.size) out.push("| Error | When |", "|---|---|"); + for (const [n, whens] of byError) out.push(`| ${num(n)}, *${msg(n)}* | ${whens.join("
")} |`); + for (const r of p.rows.filter((x) => x.vb6 !== undefined)) { + const what = + r.vb6 === 0 + ? "raises no error" + : r.error === 0 + ? `raises error ${num(r.vb6)}, *${msg(r.vb6)}*; twinBASIC raises none` + : `raises error ${num(r.vb6)}, *${msg(r.vb6)}*, instead`; + if (out.at(-1) !== "") out.push(""); + out.push(`> [!NOTE] BETA ${beta}`, `> When ${r.when}, VB6 ${what}.`); + } + } + out.push("", ""); + return out; +} + +/** + * The page text with its section set to `section` (lines, or null to remove it). + * A new section goes before the first Example heading, else before See Also, else + * at the end. + */ +export function spliceSection(text, section) { + const crlf = text.includes("\r\n"); + const lines = text.replace(/\r\n/g, "\n").split("\n"); + const { section: at } = findSection(lines.join("\n")); + if (at) { + let end = at.close + 1; + if (!section) while (end < lines.length && lines[end] === "") end++; + lines.splice(at.open, end - at.open, ...(section ?? [])); + } else if (section) { + const scanned = scan(lines); + const find = (re) => scanned.findIndex(({ text: t, fenced }) => !fenced && re.test(t)); + let i = find(/^#{2,6}[ \t]+Examples?[ \t]*$/); + if (i < 0) i = find(/^#{2,6}[ \t]+See Also[ \t]*$/); + if (i < 0) { + while (lines.length && lines[lines.length - 1] === "") lines.pop(); + lines.push("", ...section, ""); + } else lines.splice(i, 0, ...section, ""); + } + const out = lines.join("\n"); + return crlf ? out.replace(/\n/g, "\r\n") : out; +} + +/** The page's path on disk, for `page` relative to docs/. */ +export const pagePath = (page, docs = DOCS_DIR) => path.join(docs, ...page.split("/")); + +// ------------------------------------------------------------------ the central page + +export const INDEX_OPEN = /^[ \t]*$/; +export const INDEX_CLOSE = /^[ \t]*$/; + +/** VB's own numbers in order, then the others in the order of their hexadecimal. */ +const byNumber = (a, b) => ((a > 0) - (b > 0) === 0 ? (a >>> 0) - (b >>> 0) : b - a); + +/** + * The central page's index of the numbers, as lines: one row for each number of + * `messages`, with its anchor, its message (and Error$'s where it differs) and links + * to the pages whose tables name it. `pages` maps a page under docs/ to its + * { title, permalink }. + */ +export function renderIndex(files, messages, pages) { + const naming = new Map(); + for (const f of files) { + for (const p of f.pages) { + for (const r of p.rows) { + if (!naming.has(r.error)) naming.set(r.error, new Set()); + naming.get(r.error).add(p.page); + } + } + } + const out = [ + "", + "", + "| Error | Message | Listed by |", + "|---|---|---|", + ]; + // The table links each number to its line in the section after it, which names the pages. + const listed = []; + for (const n of [...messages.keys()].sort(byNumber)) { + const m = messages.get(n); + const hex = errorNumber(n).slice(String(n).length + 1); + const message = m.raised === m.error ? `*${m.raised}*` : `*${m.raised}*; **Error** gives *${m.error}*`; + const links = [...(naming.get(n) ?? [])] + .map((page) => pages.get(page) ?? { title: page, permalink: null }) + .sort((a, b) => a.title.localeCompare(b.title, "en")) + .map((p) => (p.permalink ? `[${p.title}](${relativeLink(CENTRAL_PERMALINK, p.permalink)})` : p.title)); + const count = links.length + ? `[${links.length} ${links.length === 1 ? "page" : "pages"}](#${listedAnchor(n)})` + : "---"; + out.push(`| **${n}**{: #${errorAnchor(n)} } ${hex} | ${escapeCell(message)} | ${count} |`); + if (links.length) listed.push(`**${n}**{: #${listedAnchor(n)} } ${hex}: ${links.join(", ")}`); + } + if (listed.length) { + out.push("", "## Pages that list each error", ""); + for (const line of listed) out.push(line, ""); + out.pop(); + } + out.push("", ""); + return out; +} + +/** + * The central page's text with its index set to `index` (lines), or a problem: the + * page must hold the two markers, once each and in order, outside a fence. + * @returns {{text: string|null, problem: string|null}} + */ +export function spliceIndex(text, index) { + const crlf = text.includes("\r\n"); + const lines = text.replace(/\r\n/g, "\n").split("\n"); + const at = (re) => + scan(lines) + .map(({ text: t, fenced }, i) => (!fenced && re.test(t) ? i : -1)) + .filter((i) => i >= 0); + const open = at(INDEX_OPEN); + const close = at(INDEX_CLOSE); + if (open.length !== 1 || close.length !== 1 || close[0] < open[0]) + return { + text: null, + problem: "the page needs one and one , in order", + }; + lines.splice(open[0], close[0] - open[0] + 1, ...index); + const out = lines.join("\n"); + return { text: crlf ? out.replace(/\n/g, "\r\n") : out, problem: null }; +} diff --git a/scripts/lib/tb-cdp.mjs b/scripts/lib/tb-cdp.mjs index e7891b851..036e53c46 100644 --- a/scripts/lib/tb-cdp.mjs +++ b/scripts/lib/tb-cdp.mjs @@ -32,10 +32,23 @@ export async function attach(port, match = "main.htm", { timeout = 30 * 1000, ty ).json(); const t = list.find((x) => x.type === type && x.url.includes(match)); if (!t) throw new Error(`no ${type} target matching ${JSON.stringify(match)} on port ${port}`); + return { url: t.url, ...(await connect(t.webSocketDebuggerUrl, { timeout, what: `port ${port}` })) }; +} - const ws = new WebSocket(t.webSocketDebuggerUrl); +/** + * Connect to a DevTools websocket: a page's, or the browser's own (the path on the + * second line of `DevToolsActivePort`), where `send` takes a `sessionId` to talk to a + * target the connection attached to. The time limits are those of `attach`. + * + * @param {string} wsUrl the `ws://` address + * @param {object} [o] + * @param {number} [o.timeout] milliseconds a call may take (default 30000) + * @param {string} [o.what] what the socket is for, in the message when it does not open + */ +export async function connect(wsUrl, { timeout = 30 * 1000, what = wsUrl } = {}) { + const ws = new WebSocket(wsUrl); await new Promise((res, rej) => { - const timer = setTimeout(() => rej(new Error(`the DevTools socket on port ${port} did not open`)), 10 * 1000); + const timer = setTimeout(() => rej(new Error(`the DevTools socket on ${what} did not open`)), 10 * 1000); ws.onopen = () => { clearTimeout(timer); res(); @@ -73,7 +86,7 @@ export async function attach(port, match = "main.htm", { timeout = 30 * 1000, ty pending.clear(); }; - const send = (method, params = {}, { timeout: ms = timeout } = {}) => + const send = (method, params = {}, { timeout: ms = timeout, sessionId } = {}) => new Promise((res, rej) => { // A WebSocket that is closing or closed discards what it is sent and does // not throw, so the call would wait out its whole timeout. @@ -103,7 +116,7 @@ export async function attach(port, match = "main.htm", { timeout = 30 * 1000, ty }, }); try { - ws.send(JSON.stringify({ id: i, method, params })); + ws.send(JSON.stringify({ id: i, method, params, ...(sessionId ? { sessionId } : {}) })); } catch { pending.delete(i); clearTimeout(timer); @@ -120,7 +133,6 @@ export async function attach(port, match = "main.htm", { timeout = 30 * 1000, ty }; return { - url: t.url, send, evaluate, on: (fn) => listeners.push(fn), diff --git a/scripts/lib/tb-defaults.mjs b/scripts/lib/tb-defaults.mjs new file mode 100644 index 000000000..108199c19 --- /dev/null +++ b/scripts/lib/tb-defaults.mjs @@ -0,0 +1,178 @@ +// The IDE with its default settings, whatever the user's own IDE saved. +// +// The IDE reads its per-user settings (options, panel layouts, keyboard groups, ...) +// through `hostAppObject.GetIDESetting(key)`. IDE_FLAGS (tb-ide.mjs) stop it writing them, +// and nothing stops it reading them: a font size the owner changed in their own IDE showed +// in every IDE the tools started. So each IDE a tool starts is answered "no saved value" for +// those keys, in the page only. The registry is never written. try-help-addin, which is the +// user's own IDE in all but its project, keeps its settings (`userSettings` of launchIde). +// WIP.Harness.md, "Default settings, whatever the user saved", says why it is built this way. +// +// A script has to run before main.js reads the settings, and main.htm is not loaded when +// the IDE has started: the IDE's browser process writes DevToolsActivePort, main.htm +// commits some 0.2 to 0.8 s after. So installDefaultsHook, started before the IDE is, waits +// for the file, connects to the *browser* target and auto-attaches to every target the +// browser creates, paused (waitForDebuggerOnStart); each page gets a script that runs at its +// document's start (Page.addScriptToEvaluateOnNewDocument), and is resumed. The script puts +// a setter on window.hostAppObject, which main2.js assigns, to wrap the host object. The +// connection stays open for the IDE's life: the script is the session's, and a page that +// reloads is hooked again only while it is. + +import { readFileSync, statSync } from "node:fs"; +import path from "node:path"; +import { connect } from "./tb-cdp.mjs"; + +// The keys answered with no saved value, and the answer each takes. GENERAL and GENERAL2 are +// parsed as the options' objects, and "" in their place is the first run's mark: the IDE then +// opens its Options dialog and goes no further. The others take "" as a fresh install's +// answer: JSON.parse fails inside a try and the variable keeps its default, parseJson gives +// undefined, and an empty targetArchitectureMemory reads as "unknown". Not here, and +// answered from the registry: the licence's keys, LICENCEAGREEMENTV3, ALLOWERRORREPORTINGV3, +// PUBLISHER and NEWYEAR_*, which are not settings of how the IDE looks. +export const BLANKED_KEYS = { + GENERAL: "{}", + GENERAL2: "{}", + LAYOUT: "", + LAYOUTv2: "", + CUSTOM_PANEL_LAYOUTS: "", + ACTIVE_PANEL_LAYOUT_NAME: "", + CUSTOM_KEYBOARD_SHORTCUT_GROUPS: "", + ACTIVE_KEYBOARD_SHORTCUT_GROUPS: "", + WEBPANEL_SETTINGS: "", + FORMS_windowsPalette: "", + targetArchitectureMemory: "", +}; + +// Runs in the page, in every document of the IDE's browser; only main.htm does anything. +// `window.__tbDefaults` is the marker that it ran, and its `keys` the keys it answered +// (never their values). Every function of the host object is bound to the real object: the +// proxy of a WebView2 host object refuses to be called with another `this`. +export function pageHook(answers) { + if (!/main\.htm$/i.test(location.pathname)) return; + const marker = { keys: [] }; + window.__tbDefaults = marker; + let real; + const wrap = (host) => + new Proxy(host, { + get(target, name) { + const member = target[name]; + if (typeof member !== "function") return member; + if (name !== "GetIDESetting") return (...a) => member.apply(target, a); + return (...a) => { + const key = String(a[0]); + if (!Object.hasOwn(answers, key)) return member.apply(target, a); + marker.keys.push(key); + return Promise.resolve(answers[key]); + }; + }, + }); + Object.defineProperty(window, "hostAppObject", { + configurable: true, + enumerable: true, + get: () => real, + set: (v) => { + real = v ? wrap(v) : v; + }, + }); +} + +export const hookSource = () => `(${pageHook})(${JSON.stringify(BLANKED_KEYS)})`; + +const wait = (ms) => new Promise((r) => setTimeout(r, ms)); + +/** + * Start hooking the IDE whose WebView2 profile is `profile`, from before it has started. + * The file is polled every 5 ms: launchIde's own poll is at 100 ms and answers too late. + * + * @param {object} o + * @param {string} o.profile the IDE's WEBVIEW2_USER_DATA_FOLDER + * @param {number} o.since when it was started; a DevToolsActivePort older than 2 s before is left over + * @returns {{ error: Error | null, closed: Promise, stop: () => void }} + * `error` is why no hook was installed (set once known); `stop` ends the polling and closes the + * connection, and is called when the IDE ends or fails to start. A hook that was installed leaves + * the page's `window.__tbDefaults`, which attachIde checks. + */ +export function installDefaultsHook({ profile, since }) { + const hook = { error: null, closed: null, stop: () => {} }; + let stopped = false; + let client = null; + hook.stop = () => { + stopped = true; + client?.close(); + }; + hook.closed = (async () => { + const file = path.join(profile, "EBWebView", "DevToolsActivePort"); + const until = Date.now() + 60 * 1000; + let port; + let wsPath; + while (!stopped) { + try { + if (statSync(file).mtimeMs >= since - 2000) { + const [a, b] = readFileSync(file, "utf8").split(/\r?\n/); + if (Number(a) > 0 && b) { + port = Number(a); + wsPath = b; + break; + } + } + } catch { + // not written yet + } + if (Date.now() > until) throw new Error("the IDE wrote no DevToolsActivePort in a minute"); + await wait(5); + } + if (stopped) return; + client = await connect(`ws://127.0.0.1:${port}${wsPath}`, { what: `the IDE's browser on port ${port}` }); + const source = hookSource(); + const prepare = async ({ sessionId, targetInfo }) => { + try { + if (targetInfo.type === "page") { + await client.send("Page.enable", {}, { sessionId }); + await client.send("Page.addScriptToEvaluateOnNewDocument", { source }, { sessionId }); + } + } finally { + // A target left paused never loads; one that is gone answers with an error. + await client.send("Runtime.runIfWaitingForDebugger", {}, { sessionId }).catch(() => {}); + } + }; + client.on((m) => { + if (m.method === "Target.attachedToTarget") prepare(m.params).catch(() => {}); + }); + await client.send("Target.setDiscoverTargets", { discover: true }); + await client.send("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }); + // A page that existed before the auto-attach is hooked late, when the document is still + // to commit; if it has, the reload attachIde does picks the script up. + const { targetInfos } = await client.send("Target.getTargets"); + for (const t of targetInfos.filter((x) => x.type === "page" && !x.attached)) { + const { sessionId } = await client.send("Target.attachToTarget", { targetId: t.targetId, flatten: true }); + await prepare({ sessionId, targetInfo: t }); + } + })().catch((e) => { + if (!stopped) hook.error = e; + }); + return hook; +} + +/** + * Whether the main page ran the hook, and if it did not, one reload, which the hook's session + * has the script waiting for: the page was hooked after its document had begun (about 1 launch + * in 50 on the dev box). Throws when it still did not, since the IDE would have the user's + * settings. Called by attachIde on a connection to the page. + * + * @param {{ evaluate: Function, send: Function }} c the connection + * @param {{ error: Error | null }} hook + */ +export async function checkDefaults(c, hook) { + const ran = () => c.evaluate("!!window.__tbDefaults").catch(() => false); + if (await ran()) return; + if (hook.error) throw new Error(`the IDE's settings were not made the defaults: ${hook.error.message}`); + await c.send("Page.reload", {}); + for (let i = 0; i < 400; i++) { + await wait(50); + if (await ran()) return; + } + throw new Error( + "the IDE's settings were not made the defaults: its page ran no hook, even after a reload " + + "(it would have the user's settings; userSettings: true is the opt-out)", + ); +} diff --git a/scripts/lib/tb-ide.mjs b/scripts/lib/tb-ide.mjs index ded95dd91..1e8319dac 100644 --- a/scripts/lib/tb-ide.mjs +++ b/scripts/lib/tb-ide.mjs @@ -17,6 +17,7 @@ import { secondsText, timeoutMs } from "./harness-settings.mjs"; import { connectBroker } from "./tb-broker.mjs"; import { attach } from "./tb-cdp.mjs"; import { click, pagePicture } from "./tb-click.mjs"; +import { checkDefaults, installDefaultsHook } from "./tb-defaults.mjs"; import { removeTree, removeTreeAsync } from "./tb-ide-copy.mjs"; import { consoleMark, keepClears, keptClears, linesSince, readConsole } from "./tb-ide-console.mjs"; import { unsupportedIde } from "./tb-install.mjs"; @@ -245,7 +246,12 @@ export async function leaseSlot({ tag = "", port = false } = {}) { * Only a test of the very thing a flag guards leaves * one out, and it puts back what it writes: the recent * list's test (test/ide/recent-projects-copies.test.mjs) - * @returns {Promise<{pid: number, launcher: import("node:child_process").ChildProcess | null, folders: string[], port: number | null, slot: number, desktop: string | null}>} + * @param {boolean} [o.userSettings] the IDE keeps the user's own settings (default false: + * the IDE is answered "no saved value" for its options, panel + * layouts and keyboard groups, in the page only, so it runs with + * the defaults; tb-defaults.mjs). Only a tool that is the user's own + * IDE in all but its project passes true: try-help-addin + * @returns {Promise<{pid: number, launcher: import("node:child_process").ChildProcess | null, folders: string[], port: number | null, slot: number, desktop: string | null, hook: object | null}>} * `pid` is the IDE's own. `folders` are its temp folder and WebView2 profile, * which shutdownIde removes. `port` is its DevTools port, for attachIde, or * null when the IDE never opened one within a minute. `desktop` is the private @@ -262,6 +268,7 @@ export async function launchIde({ env = {}, browserArgs = "", flags = IDE_FLAGS, + userSettings = false, }) { // Before anything starts: an IDE older than the flags would write the user's settings. const tooOld = unsupportedIde(exe); @@ -272,7 +279,7 @@ export async function launchIde({ const s = slot ?? own; try { const leased = Date.now(); - const started = await startIde({ exe, project, s, own, port, show, keep, env, browserArgs, flags }); + const started = await startIde({ exe, project, s, own, port, show, keep, env, browserArgs, flags, userSettings }); timing("launch", { project: project ? path.resolve(project) : null, leaseMs: leased - t0, @@ -286,7 +293,7 @@ export async function launchIde({ } // launchIde once the slot is settled; `own` is the slot when it is launchIde's own to hand on. -async function startIde({ exe, project, s, own, port, show, keep, env, browserArgs, flags }) { +async function startIde({ exe, project, s, own, port, show, keep, env, browserArgs, flags, userSettings }) { const exeWin = exe.split("/").join("\\"); const target = project ? path.resolve(project).split("/").join("\\") : ""; // Each IDE has a temp folder of its own, emptied at each launch in its slot @@ -326,6 +333,15 @@ async function startIde({ exe, project, s, own, port, show, keep, env, browserAr ...env, }; const found = (pid) => (port != null ? Promise.resolve(port) : devToolsPort(ideProfile, pid, started)); + // Started before the IDE is, since main.htm commits a fraction of a second after its port file + // is written; attachIde checks the hook ran, by the port. The hook's connection lives as long + // as this process, so a kept IDE (`keep`) is hooked only until this process ends. + const hook = userSettings ? null : installDefaultsHook({ profile: ideProfile, since: started }); + const hooked = async (pid, launcher) => { + const p = await found(pid); + if (hook && p) defaultsHooks.set(p, hook); + return { pid, launcher, folders, port: p, slot: s.slot, desktop: show ? null : s.desktop, hook }; + }; if (show) { const child = spawn(exeWin, [...flags, ...(target ? [target] : [])], { @@ -343,7 +359,7 @@ async function startIde({ exe, project, s, own, port, show, keep, env, browserAr child.unref(); // The slot names its folders; the IDE outlives nothing here, so it keeps the slot. await own?.handTo(child.pid); - return { pid: child.pid, launcher: null, folders, port: await found(child.pid), slot: s.slot, desktop: null }; + return hooked(child.pid, null); } // lib/tb-launch.ps1 does the Win32 calls Node cannot -- CreateDesktop, @@ -381,6 +397,7 @@ async function startIde({ exe, project, s, own, port, show, keep, env, browserAr env: fullEnv, }); } catch (e) { + hook?.stop(); // No IDE started, so nothing holds the folders and nothing else would remove them. for (const dir of folders) rmSync(dir, { recursive: true, force: true, maxRetries: 3 }); throw new Error( @@ -389,9 +406,13 @@ async function startIde({ exe, project, s, own, port, show, keep, env, browserAr } const { pid, launcher } = launched; await own?.handTo(keep ? pid : launcher.pid); - return { pid, launcher, folders, port: await found(pid), slot: s.slot, desktop: s.desktop }; + return hooked(pid, launcher); } +// The hook of each IDE launchIde started with the defaults, by its DevTools port: attachIde checks +// the IDEs it is told the port of, so no caller says whether to. +const defaultsHooks = new Map(); + // The DevTools port the IDE's WebView2 opened: the first line of DevToolsActivePort in its // profile, written once its browser process has bound the port. Only a file written since the // launch counts, in case a folder could not be emptied. Null when the IDE ends, or a minute @@ -741,6 +762,7 @@ export function keptIdeLines(pid) { export function shutdownIde(ide) { if (!ide) return; killTree(ide.pid); + ide.hook?.stop(); // The launcher holds the private desktop open and the only handle to the // IDE's job. It exits once the IDE does, but do not wait on that: killing it // closes the job, which ends anything the tree kill missed. @@ -772,6 +794,7 @@ export async function shutdownIdeAsync(ide) { await new Promise((resolve) => { execFile("taskkill", ["/PID", String(ide.pid), "/T", "/F"], { windowsHide: true }, () => resolve()); }); + ide.hook?.stop(); try { ide.launcher?.kill(); } catch { @@ -859,6 +882,7 @@ export async function attachIde(port, { tries = 60 } = {}) { } // still starting c.dialogs = []; c.pageBlocked = false; + const hook = defaultsHooks.get(port); c.on((m) => { if (m.method !== "Page.javascriptDialogOpening") return; const { type, message } = m.params; @@ -872,6 +896,8 @@ export async function attachIde(port, { tries = 60 } = {}) { } catch { c.pageBlocked = true; } + // A blocked page answers nothing, the hook's marker included. + if (hook && !c.pageBlocked) await checkDefaults(c, hook); c.wire = null; if (!c.pageBlocked) { const wire = watchCompiles(c); diff --git a/scripts/lib/tb-lane.mjs b/scripts/lib/tb-lane.mjs index 9eec699b6..e9b946cde 100644 --- a/scripts/lib/tb-lane.mjs +++ b/scripts/lib/tb-lane.mjs @@ -93,12 +93,24 @@ export class Lane { * @param {boolean} [o.show] IDEs on the user's desktop instead of a private one * @param {string} [o.browserArgs] more WebView2 browser arguments for every IDE * the lane opens, as launchIde takes them + * @param {boolean} [o.userSettings] every IDE keeps the user's own settings instead of the + * defaults, as launchIde takes it (try-help-addin only) * @param {{ width: number, height: number } | null} [o.page] the size each IDE's * page is given as the lane attaches to it (fixPageSize); null, the default, * leaves the window's own. addinLane gives a runner's lane PAGE */ - constructor({ name, slot = null, desktop = null, work, ide, show = false, browserArgs = "", page = null }) { - Object.assign(this, { name, slot, desktop, work, ide, show, browserArgs, page }); + constructor({ + name, + slot = null, + desktop = null, + work, + ide, + show = false, + browserArgs = "", + page = null, + userSettings = false, + }) { + Object.assign(this, { name, slot, desktop, work, ide, show, browserArgs, page, userSettings }); this.port = null; // the open IDE's DevTools port this.lease = null; // the slot, as launchIde takes it (leased()) this.exe = null; // the lane's copy of the install, made on first use @@ -292,6 +304,7 @@ export class Lane { env: { APPDATA: appdata, ...env }, browserArgs: this.browserArgs, flags, + userSettings: this.userSettings, }); Object.assign(this, { src, project, port: this.run.port }); this.c = await attachIde(this.port); @@ -327,6 +340,7 @@ export class Lane { show: this.show, env: { APPDATA: this.appdataDir(), ...env }, browserArgs: this.browserArgs, + userSettings: this.userSettings, }); Object.assign(this, { src: null, project: null, port: this.run.port }); this.c = await attachIde(this.port); diff --git a/scripts/shoot_docs.mjs b/scripts/shoot_docs.mjs index 2da41ac96..994b88aa9 100644 --- a/scripts/shoot_docs.mjs +++ b/scripts/shoot_docs.mjs @@ -12712,7 +12712,8 @@ function helpTakes(ctx) { await showPage(AT.msgBox, HELP_PAGE); await click(c, { toolWindow: PANE, css: "#helpSettings" }); if (!(await waitFor(c, settingsOpen))) throw new Error("the gear did not open the settings panel"); - // the picture shows the defaults, both unticked: the Hover picture ticks Hover help + // the picture shows the defaults: Hover help and All packages unticked, the two + // fixes ticked; the Hover picture ticks Hover help if (await inPane(`return root.querySelector("#helpHover").checked;`)) { await click(c, { toolWindow: PANE, css: "#helpHover" }); await sleep(300); @@ -12720,8 +12721,8 @@ function helpTakes(ctx) { await sleep(500); await paneAside(); const pane = await paneRect(); - // 150 CSS pixels ends between the page's heading and its first paragraph. - const png = await snap(c, "Settings", inside({ ...pane, height: 150 }), { frame: origin }); + // 200 CSS pixels holds the four rows and ends between the page's heading and its first paragraph. + const png = await snap(c, "Settings", inside({ ...pane, height: 200 }), { frame: origin }); await closeSettings(); return png; }); diff --git a/scripts/try_help_addin.mjs b/scripts/try_help_addin.mjs index 04a8b68c5..a8a068ede 100644 --- a/scripts/try_help_addin.mjs +++ b/scripts/try_help_addin.mjs @@ -120,7 +120,15 @@ const tidy = startTidy({ prefixes: [work], window: false }); if (!tidy) die(2, "could not record the registry, so it could not be put back afterwards"); const settingsBefore = snapshotKeys([settingsKey(SETTINGS)]); -const lane = new Lane({ name: "try", slot: slot.slot, desktop: slot.desktop, work, ide, show: true }); +const lane = new Lane({ + name: "try", + slot: slot.slot, + desktop: slot.desktop, + work, + ide, + show: true, + userSettings: true, +}); let server = null; let stopping = false; diff --git a/test.bat b/test.bat index a58a4b12c..9859c4b62 100644 --- a/test.bat +++ b/test.bat @@ -177,6 +177,11 @@ node scripts/check_book_coverage.mjs @rem no tree, no install, well under a second. node scripts/check_package_tests.mjs @if errorlevel 1 goto :fail +@rem Every page's Common Errors section is what scripts\errors.mjs write makes of the +@rem run-time error data under errors\, and the data is in order. Its probes +@rem run on inputs built in memory first. Sources only: no twinBASIC, no VB6. +node scripts/check_errors.mjs +@if errorlevel 1 goto :fail @rem The symbol index (tB/symbols.json) is read by the IDE help add-in, @rem and a build that indexes the reference cleanly says nothing about the @rem rules that did not fire on it. These probes assert each one against diff --git a/test/addin/help.test.mjs b/test/addin/help.test.mjs index 125ff261d..44357f9e2 100644 --- a/test/addin/help.test.mjs +++ b/test/addin/help.test.mjs @@ -939,6 +939,15 @@ scenario("the help add-in: F1 and the help pane", (lane) => { // The All packages box in the pane, and in the window's page. const allBox = () => inPane(c, `return root.querySelector("#helpAllPackages")?.checked ?? null;`); const windowAllBox = () => win.evaluate(`document.getElementById("helpAllPackages").checked`); + // The two page-side fixes: a box each in the settings panel, and what the + // fix's script leaves on the IDE's page while it is on. + const FIXES = [ + { box: "helpKeysOn", global: "tbDocsAddinKeys", label: "Ctrl and Alt shortcuts of add-ins" }, + { box: "helpFindOn", global: "tbDocsTextFind", label: "Find in the shortcut editors" }, + ]; + const paneFixBox = (id) => inPane(c, `return root.querySelector("#helpSettingsPanel #${id}")?.checked ?? null;`); + const windowFixBox = (id) => win.evaluate(`document.getElementById(${JSON.stringify(id)}).checked`); + const fixInstalled = (name) => c.evaluate(`!!window.${name}`); // Waits for the results to be `want`, a function of the rows, and fails with the rows. async function resultsBecome(want, what) { const rows = await waitFor(c, async (c) => { @@ -1085,6 +1094,158 @@ scenario("the help add-in: F1 and the help pane", (lane) => { ); }); + test("the settings panel has a box for each page-side fix, ticked, and the fixes are in the page", async () => { + for (const { box, global, label } of FIXES) { + assert.equal(await paneFixBox(box), true, `${label}: the pane's box is missing or unticked`); + assert.equal(await fixInstalled(global), true, `${label}: window.${global} is not set`); + const text = await inPane(c, `return root.querySelector("#${box}")?.parentNode.textContent.trim() ?? null;`); + assert.equal(text, label); + } + }); + + // Unticking a box takes the fix out of the page at once, ticking it puts it back. + test("unticking a fix's box removes it from the page, and ticking it puts it back", async () => { + for (const { box, global, label } of FIXES) { + await clickSettingBox(`#${box}`); + assert.ok(await waitFor(c, async () => (await fixInstalled(global)) === false), `${label}: still in the page`); + assert.equal(await paneFixBox(box), false); + await clickSettingBox(`#${box}`); + assert.ok(await waitFor(c, () => fixInstalled(global)), `${label}: not back in the page`); + assert.equal(await paneFixBox(box), true); + } + }); + + // The IDE's own command opens Manage Keyboard Shortcuts, whose first entry, + // the built-in group, is a read-only text area of JSON. + test("Ctrl+F in Manage Keyboard Shortcuts finds text, scrolls to it, and Esc leaves the dialog open", async () => { + const WORD = "ctrl"; + const AREA = ".modalDialogContainer textarea.genericMonospaceTextArea"; + const onArea = (expr) => + c.evaluate(`(() => { const a = document.querySelector(${JSON.stringify(AREA)}); return a ? ${expr} : null; })()`); + const modals = () => c.evaluate(`modalDialogs.length`); + const selection = () => + onArea( + `({ start: a.selectionStart, end: a.selectionEnd, text: a.value.substring(a.selectionStart, a.selectionEnd), focused: document.activeElement === a, top: a.scrollTop })`, + ); + const findBox = () => + c.evaluate(`(() => { + const i = document.querySelector(".tbDocsFindBar input"); + return i ? { focused: document.activeElement === i, missing: i.classList.contains("tbDocsFindMissing") } : null; + })()`); + // What the script measures for the selection: in view, and not under the find box. + const visibility = () => onArea(`window.tbDocsTextFind.visible(a)`); + // The selection is the match at `start` for `word`, has the focus, and shows. + const selectionIs = async (start, what, word = WORD) => { + const ok = await waitFor(c, async () => (await selection())?.start === start); + const s = await selection(); + assert.ok(ok, `${what}: the selection is ${JSON.stringify(s)}, expected a match at ${start}`); + assert.equal(s.text.toLowerCase(), word, what); + assert.equal(s.focused, true, `${what}: the text area has not got the focus`); + const v = await visibility(); + assert.ok(v.visible, `${what}: the match is not in view: ${JSON.stringify({ ...v, scrollTop: s.top })}`); + }; + const keyDown = (key, mods) => pressKey(c, key, mods); + assert.equal(await modals(), 0, "a dialog is open already"); + await c.evaluate(`executeIdeCommand("tbKeyboardShortcuts_ShowManageKeyboardShortcuts")`); + try { + assert.ok(await waitFor(c, async () => (await onArea("true")) === true), "the dialog has no text area"); + const { at, length, room } = await onArea(`(() => { + const t = a.value.toLowerCase(), at = []; + for (let i = t.indexOf(${JSON.stringify(WORD)}); i >= 0; i = t.indexOf(${JSON.stringify(WORD)}, i + 1)) at.push(i); + return { at, length: t.length, room: a.scrollHeight - a.clientHeight }; + })()`); + assert.ok(at.length >= 3, `"${WORD}" is in the text ${at.length} times`); + assert.ok(room > 0, "the text does not need scrolling"); + await onArea(`(a.focus(), a.setSelectionRange(0, 0), true)`); + + await pressKey(c, "f", { ctrl: true }); + assert.ok( + await waitFor(c, async () => (await findBox())?.focused === true), + `Ctrl+F opened no find box: ${JSON.stringify(await findBox())}`, + ); + assert.equal(await modals(), 1, "Ctrl+F closed the dialog"); + await typeText(c, WORD); + await pressKey(c, "Enter"); + await selectionIs(at[0], "Enter: the first match"); + await pressKey(c, "F3"); + await selectionIs(at[1], "F3: the next match"); + await pressKey(c, "F3", { shift: true }); + await selectionIs(at[0], "Shift+F3: the match before"); + // Round past the start to the last match, which is far down the text. + await pressKey(c, "F3", { shift: true }); + const last = at[at.length - 1]; + await selectionIs(last, "Shift+F3 from the first: the last match"); + const scrolledDown = (await selection()).top; + assert.ok(scrolledDown > 0, `the text area did not scroll to a match at ${last} of ${length}`); + // Backwards from the last match to the first scrolls up, a match at a time. + for (let n = at.length - 2; n >= 0; n--) { + await keyDown("F3", { shift: true }); + await selectionIs(at[n], `Shift+F3 to match ${n}`); + } + assert.ok((await selection()).top < scrolledDown, "going back to the first match did not scroll up"); + // Round past the start to the last, and past the end to the first. + await keyDown("F3", { shift: true }); + await selectionIs(last, "Shift+F3 round past the start"); + await keyDown("F3"); + await selectionIs(at[0], "F3 round past the end"); + + // A word that is not there marks the box, and leaves the selection. + const was = await selection(); + await pressKey(c, "f", { ctrl: true }); + assert.ok(await waitFor(c, async () => (await findBox())?.focused === true), "Ctrl+F did not return to the box"); + await typeText(c, "zzqq"); + await pressKey(c, "Enter"); + assert.ok( + await waitFor(c, async () => (await findBox())?.missing === true), + `no match was not shown: ${JSON.stringify(await findBox())}`, + ); + assert.equal((await selection()).start, was.start, "a search with no match moved the selection"); + + // The box searches for what it holds; Esc then closes it and only it. + await pressKey(c, "f", { ctrl: true }); + await typeText(c, WORD); + await pressKey(c, "Enter"); + await selectionIs(at[1], "Enter in the box after a search with no match"); + await pressKey(c, "Escape"); + assert.ok(await waitFor(c, async () => (await findBox()) === null), "Esc did not close the find box"); + assert.equal(await modals(), 1, "Esc closed the dialog"); + assert.equal((await selection()).focused, true, "the text area has not got the focus after Esc"); + + // With the box closed, F3 and Shift+F3 go on with the last query. + await keyDown("F3"); + await selectionIs(at[2], "F3 with the box closed"); + assert.equal(await findBox(), null, "F3 opened the box"); + await keyDown("F3", { shift: true }); + await selectionIs(at[1], "Shift+F3 with the box closed"); + + // Ctrl+F3 takes the word at the caret: its next occurrence, then, from there, the one before. + const { word, first, next, before } = await onArea(`(() => { + const t = a.value, counts = {}; + for (const m of t.matchAll(/[A-Za-z0-9_]+/g)) if (m[0].length >= 8) (counts[m[0]] ??= []).push(m.index); + const word = Object.keys(counts).find((w) => counts[w].length >= 3); + const lower = t.toLowerCase(), w = word.toLowerCase(); + const first = counts[word][0]; + const next = lower.indexOf(w, first + w.length); + return { word: w, first, next, before: lower.lastIndexOf(w, next - 1) }; + })()`); + await onArea(`(a.focus(), a.setSelectionRange(${first + 3}, ${first + 3}), true)`); + await keyDown("F3", { ctrl: true }); + await selectionIs(next, `Ctrl+F3 on ${word}`, word); + assert.equal(await findBox(), null, "Ctrl+F3 opened the box"); + await onArea(`(a.setSelectionRange(${next + 3}, ${next + 3}), true)`); + await keyDown("F3", { ctrl: true, shift: true }); + await selectionIs(before, `Ctrl+Shift+F3 on ${word}`, word); + } finally { + // Cancel, the dialog's own button; leaves the IDE as it was found. + await c.evaluate(`(() => { + const m = [...document.querySelectorAll(".modalDialogContainer")].pop(); + const b = m && [...m.querySelectorAll("button, .dialogButton, [class*=Button]")].find((e) => e.textContent.trim() === "Cancel"); + if (b) b.click(); else if (typeof hideModalDialog === "function" && modalDialogs.length) hideModalDialog(); + })()`); + assert.ok(await waitFor(c, async () => (await modals()) === 0), "the dialog did not close"); + } + }); + test("hover help is off until its box is ticked", async () => { assert.equal(await hoverBox(), false); const mark = await consoleMark(c); @@ -1372,6 +1533,14 @@ scenario("the help add-in: F1 and the help pane", (lane) => { ); }); + test("the window is set in the IDE's font size", async () => { + const ide = await c.evaluate( + `getComputedStyle(document.documentElement).getPropertyValue("--ide-fontsize").trim()`, + ); + assert.ok(ide, "the IDE has no --ide-fontsize"); + assert.equal(await win.evaluate(`getComputedStyle(document.body).fontSize`), ide); + }); + test("the window's title bar is one row, with the title and five buttons, and Windows draws no caption", async () => { const bar = await win.evaluate(`(() => { const el = (id) => document.getElementById(id); @@ -1664,6 +1833,36 @@ scenario("the help add-in: F1 and the help pane", (lane) => { assert.equal(await allBox(), false); }); + // Both ways, as for All packages: the window's box reaches the pane's and the + // page, and the pane's box, clicked though the pane is hidden, the window's. + test("the page-side fixes can be unticked in the window or the pane, and the other follows", async () => { + for (const { box, global, label } of FIXES) { + assert.equal(await windowFixBox(box), true, `${label}: the window's box is not ticked`); + await clickInWindow("#helpSettings"); + await settingsBecome(windowSettings, SETTINGS_OPEN, "the gear did not open the panel"); + await clickInWindow(`#${box}`); + assert.ok(await waitFor(c, async () => (await paneFixBox(box)) === false), `${label}: the pane's box is ticked`); + assert.ok(await waitFor(c, async () => (await fixInstalled(global)) === false), `${label}: still in the page`); + assert.equal(await windowFixBox(box), false); + assert.deepEqual(await windowSettings(), SETTINGS_OPEN, "unticking the box closed the panel"); + await clickInWindow(`#${box}`); + assert.ok(await waitFor(c, async () => (await paneFixBox(box)) === true), `${label}: the pane's box is unticked`); + assert.ok(await waitFor(c, () => fixInstalled(global)), `${label}: not back in the page`); + await clickInWindow("#helpSettings"); + await settingsBecome(windowSettings, SETTINGS_CLOSED, "the gear did not close the panel"); + + await inPane(c, `root.querySelector("#${box}").click(); return null;`); + assert.ok( + await waitFor(c, async () => (await windowFixBox(box)) === false), + `${label}: the window's box is ticked`, + ); + assert.equal(await fixInstalled(global), false); + await inPane(c, `root.querySelector("#${box}").click(); return null;`); + assert.ok(await waitFor(c, () => windowFixBox(box)), `${label}: the window's box is unticked`); + assert.ok(await waitFor(c, () => fixInstalled(global)), `${label}: not back in the page`); + } + }); + test("the window's results follow the All packages box", async () => { await win.evaluate(`document.getElementById("helpSearch").focus()`); await win.evaluate(`document.getElementById("helpSearch").value = ""`); diff --git a/test/addin/keys.test.mjs b/test/addin/keys.test.mjs index d205e5d71..e92bb8f26 100644 --- a/test/addin/keys.test.mjs +++ b/test/addin/keys.test.mjs @@ -8,9 +8,15 @@ // docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md and the filed report in // bugs/filed/ (twinbasic/twinbasic#2445), and then this file. // +// The lane named keys-help loads the help add-in beside the probe, and asserts +// that its addinkeys.js makes every add-in's {ctrl} and {alt} shortcuts fire +// once, on key-up, as the IDE runs the others. The cases that state the bug +// are the lane keys' alone. +// // Run it with addin-test.bat, which gives it a lane; on its own it is skipped. import assert from "node:assert/strict"; +import fs from "node:fs"; import path from "node:path"; import { before, test } from "node:test"; import { fileURLToPath } from "node:url"; @@ -23,6 +29,7 @@ import { scenario } from "./scenario.mjs"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const HOST = path.join(HERE, "host"); const PROBE = path.join(HERE, "probes", "keys"); +const ADDIN = path.join(HERE, "..", "..", "add-in"); // The key strings the probe says fired since a mark, in order. const firedSince = async (c, mark) => @@ -75,9 +82,21 @@ const paletteOpen = (c) => scenario("P1 and P2: add-in keyboard shortcuts", (lane) => { let c; + const withHelp = lane.name === "keys-help"; + // The tests that state the bug do not hold with the help add-in loaded. + const bug = { skip: withHelp && "the IDE's bug, which the help add-in repairs" }; before(async () => { await lane.addAddin(PROBE); - c = await lane.open(HOST); + if (withHelp) { + // The add-in as committed, without a help archive. F1 makes its pane, + // which is told a site nothing answers at, so no page comes from the network. + const src = path.join(lane.work, "help-src"); + fs.rmSync(src, { recursive: true, force: true }); + const skip = [path.join(ADDIN, "Build"), path.join(ADDIN, "Resources", "HELP")]; + fs.cpSync(ADDIN, src, { recursive: true, filter: (f) => !skip.includes(f) }); + await lane.addAddin(src); + } + c = await lane.open(HOST, withHelp ? { env: { TB_DOCS_HELP_SITE: "http://localhost:1" } } : undefined); }); test("the add-in loads and registers its shortcuts, lowercased", async () => { @@ -97,6 +116,12 @@ scenario("P1 and P2: add-in keyboard shortcuts", (lane) => { "{shift}d", "{shift}f1", ]); + if (withHelp) { + assert.ok( + await waitFor(c, async (c) => ((await readConsole(c)) ?? "").includes("[tbDocsHelp] loaded")), + "the help add-in never printed that it had loaded", + ); + } assert.ok(await blurAll(c), "the focus stayed on an element"); }); @@ -107,14 +132,14 @@ scenario("P1 and P2: add-in keyboard shortcuts", (lane) => { assert.deepEqual(await pressAlone(c, "F1", { shift: true }), ["{shift}f1"]); }); - test("P1: Ctrl and Alt keys do not fire", async () => { + test("P1: Ctrl and Alt keys do not fire", bug, async () => { assert.deepEqual(await pressAlone(c, "d", { ctrl: true, shift: true }), []); assert.deepEqual(await pressAlone(c, "d", { ctrl: true }), []); assert.deepEqual(await pressAlone(c, "f", { alt: true }), []); assert.equal(await menuOpen(c), false, "Alt+F opened a menu"); }); - test("P1: a Ctrl or Alt key fires when the same key was pressed alone just before", async () => { + test("P1: a Ctrl or Alt key fires when the same key was pressed alone just before", bug, async () => { assert.deepEqual( await pressThenQ(c, [ ["d"], @@ -127,6 +152,53 @@ scenario("P1 and P2: add-in keyboard shortcuts", (lane) => { ); }); + test("help add-in: its script is in, and Ctrl and Alt shortcuts fire once each", { + skip: !withHelp && "keys-help only", + }, async () => { + assert.equal(await c.evaluate("!!window.tbDocsAddinKeys"), true, "window.tbDocsAddinKeys is not set"); + assert.deepEqual(await pressAlone(c, "d", { ctrl: true, shift: true }), ["{ctrl}{shift}d"]); + assert.deepEqual(await pressAlone(c, "d", { ctrl: true }), ["{ctrl}d"]); + assert.deepEqual(await pressAlone(c, "f", { alt: true }), ["{alt}f"]); + assert.equal(await menuOpen(c), false, "Alt+F opened a menu"); + }); + + test("help add-in: the plain, Shift and function keys fire once each, as before", { + skip: !withHelp && "keys-help only", + }, async () => { + assert.deepEqual(await pressAlone(c, "d"), ["d"]); + assert.deepEqual(await pressAlone(c, "D"), ["{shift}d"]); + assert.deepEqual(await pressAlone(c, "F1"), ["f1"]); + assert.deepEqual(await pressAlone(c, "F1", { shift: true }), ["{shift}f1"]); + }); + + test("help add-in: d then Ctrl+D within 500 ms fires d once, then Ctrl+D once", { + skip: !withHelp && "keys-help only", + }, async () => { + assert.deepEqual(await pressThenQ(c, [["d"], ["d", { ctrl: true }]]), ["d", "{ctrl}d"]); + assert.deepEqual(await pressThenQ(c, [["f"], ["f", { alt: true }]]), ["{alt}f"]); + }); + + test("help add-in: a Ctrl+D held longer than 500 ms fires nothing", { + skip: !withHelp && "keys-help only", + }, async () => { + await sleep(600); + const mark = await consoleMark(c); + await c.evaluate( + `window.dispatchEvent(new KeyboardEvent("keydown", { key: "d", code: "KeyD", ctrlKey: true, bubbles: true }))`, + ); + await sleep(700); + await c.evaluate( + `window.dispatchEvent(new KeyboardEvent("keyup", { key: "d", code: "KeyD", ctrlKey: true, bubbles: true }))`, + ); + await pressKey(c, "q"); + assert.ok(await waitFired(c, mark, "q")); + await sleep(300); + assert.deepEqual( + (await firedSince(c, mark)).filter((k) => k !== "q"), + [], + ); + }); + test("P2: in the code editor F1 fires and types nothing, and a plain letter fires and types", async () => { await openFile(c, "/AddinHost/Sources/Main.twin", { line: 3, column: 9 }); await setCursor(c, 3, 9); diff --git a/test/addin/lanes.mjs b/test/addin/lanes.mjs index 658433698..d3297286e 100644 --- a/test/addin/lanes.mjs +++ b/test/addin/lanes.mjs @@ -31,6 +31,8 @@ export default [ // references (the hosts it opens are in probes/references-*); neither saves // anything. { file: "keys.test.mjs" }, + // The same file with the help add-in loaded, which makes {ctrl} and {alt} shortcuts fire. + { file: "keys.test.mjs", name: "keys-help", settings: ["tbDocsHelp"] }, { file: "panes.test.mjs" }, { file: "symbols.test.mjs" }, { file: "ideserver.test.mjs" }, diff --git a/test/render.test.mjs b/test/render.test.mjs index 35f34c639..28cf40186 100644 --- a/test/render.test.mjs +++ b/test/render.test.mjs @@ -6,7 +6,8 @@ // builder/theme-pictures.mjs); and for a diagram's SVG as it is inlined, its // boxes linking or not (builder/inline-diagram.mjs), on what Graphviz itself // writes; and for a callout's label, `> [!WARNING] BETA 1005` -// (lib/callout-labels.mjs). +// (lib/callout-labels.mjs); and for a page shortcut, `tb:Errors#error-5` +// (relativeLinksPlugin, buildLinkTables). // // The build compares whole pages, so a plugin that goes wrong only on input // the corpus does not hold passes it: kramdownEllipsisPlugin shortened every @@ -25,7 +26,13 @@ import { stripXmlPrologue } from "../builder/dot.mjs"; import { inlineDiagram } from "../builder/inline-diagram.mjs"; import { isWebsiteOnlyLink, rewriteHtml, websiteOf } from "../builder/offline-rewrite.mjs"; import { assignPackageVersions, calloutLabelsOf, parseCalloutLabel } from "../lib/callout-labels.mjs"; -import { applyPreRenderRewrites, createMarkdownIt, initHighlighter } from "../builder/render.mjs"; +import { + applyPreRenderRewrites, + buildLinkTables, + createMarkdownIt, + initHighlighter, + serializeLinkTables, +} from "../builder/render.mjs"; import { lightOnly, pairSizes, @@ -209,6 +216,46 @@ describe("rewriteHtml with a website behind the offline tree", () => { }); }); +// A page shortcut: `tb:Errors#error-5` links to the page whose front matter says +// `shortcut: Errors`, its fragment kept; an unknown or doubled name fails the build. +describe("page shortcuts (tb:)", () => { + const page = (srcRel, permalink, frontmatter = {}) => ({ srcRel, permalink, frontmatter }); + const tables = buildLinkTables([ + page("Reference/Run-time-Errors.md", "/Reference/Run-time-Errors", { shortcut: "Errors" }), + page("Reference/Other.md", "/Reference/Other"), + ]); + const site = createMarkdownIt({ highlighter: null, linkTables: tables, baseurl: "/b", staticFiles: new Set() }); + const render = (src) => site.render(src, { page: { srcRel: "Reference/Default/VBA/Strings/Mid.md" } }).trim(); + + test("a shortcut resolves to the page's permalink, with or without a fragment", () => { + assert.equal( + render("[5](tb:Errors#error-5) and [all](tb:Errors)"), + '

5 and all

', + ); + }); + + test("a shortcut in a code span is left as written, unknown or not", () => { + assert.match(render("`[5](tb:Nobody)`"), /]*>\[5\]\(tb:Nobody\)<\/code>/); + }); + + test("an unknown shortcut, and a shortcut as an image, fail the render, naming the page", () => { + assert.throws(() => render("[x](tb:Nobody#a)"), /Mid\.md: no page has the shortcut in tb:Nobody#a/); + assert.throws(() => render("![x](tb:Errors)"), /Mid\.md: tb:Errors is a page shortcut, not an image/); + }); + + test("a shortcut that two pages claim, or that is not a name, fails the link tables", () => { + assert.throws( + () => buildLinkTables([page("A.md", "/A", { shortcut: "X" }), page("B.md", "/B", { shortcut: "X" })]), + /B\.md: shortcut X is already A\.md's/, + ); + assert.throws(() => buildLinkTables([page("A.md", "/A", { shortcut: "a b" })]), /A\.md: shortcut: must be a name/); + }); + + test("the shortcuts cross to the workers with the other tables", () => { + assert.deepEqual(serializeLinkTables(tables).byShortcut, [["Errors", "/Reference/Run-time-Errors"]]); + }); +}); + // A picture in two themes: the page names X.png, and the build writes the light picture and // the dark one when X.light.png is among the static files, the book the light one alone. describe("themePairPlugin and lightOnly", () => {