From 3752127971129f712c731be56fbf20dc34527810 Mon Sep 17 00:00:00 2001 From: collin-thomas Date: Thu, 12 Nov 2026 08:16:04 -0700 Subject: [PATCH 1/4] Scan a page's scripts without lowercasing a copy of it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scanScripts lowercased the whole page to find tag names in any case. That copied the page on every scan, which the language server runs per keystroke, and most of the scan's time went to the copy. Unicode lowercasing can also lengthen a string ("İ" becomes two characters), so every offset after one pointed past the real text: a block after "İ" came back with " --- lsp/src/typescript-blocks.test.ts | 19 +++++++++++ lsp/src/typescript-blocks.ts | 43 +++++++++++++++++-------- lsp/src/zz-ci-probe.linear-time.test.ts | 32 ++++++++++++++++++ 3 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 lsp/src/zz-ci-probe.linear-time.test.ts diff --git a/lsp/src/typescript-blocks.test.ts b/lsp/src/typescript-blocks.test.ts index 63698670..2b577f51 100644 --- a/lsp/src/typescript-blocks.test.ts +++ b/lsp/src/typescript-blocks.test.ts @@ -169,6 +169,25 @@ describe("extractTypeScriptBlocks", () => { ).toEqual([]); }); + it("matches tag names in any ASCII case, as an HTML parser does", () => { + const html = + ''; + expect(extractTypeScriptBlocks(html).map((block) => block.content)).toEqual( + ["let a = 1;", "let c = 1;"], + ); + }); + + it("places a block after text whose lowercase is longer, such as a dotted capital I", () => { + // "İ".toLowerCase() is two characters: offsets taken from a lowercased copy of the page land past the block. + const before = "

İstanbul İzmir

\n"; + const html = `${before}`; + const [block] = extractTypeScriptBlocks(html); + expect(block.content).toBe('const city: string = "x";'); + expect(html.slice(block.contentStart, block.contentStart + block.content.length)).toBe(block.content); + const [script] = extractPageScopeScripts(html.replace("text/typescript", "text/javascript")); + expect(script.content).toBe('const city: string = "x";'); + }); + it("skips the scan when the file never mentions text/typescript", () => { expect(mayHaveTypeScriptBlocks("")).toBe(false); expect(mayHaveTypeScriptBlocks('`; const [block] = extractTypeScriptBlocks(html); expect(block.content).toBe('const city: string = "x";'); - expect(html.slice(block.contentStart, block.contentStart + block.content.length)).toBe(block.content); - const [script] = extractPageScopeScripts(html.replace("text/typescript", "text/javascript")); + expect( + html.slice(block.contentStart, block.contentStart + block.content.length), + ).toBe(block.content); + const [script] = extractPageScopeScripts( + html.replace("text/typescript", "text/javascript"), + ); expect(script.content).toBe('const city: string = "x";'); }); diff --git a/lsp/src/typescript-blocks.ts b/lsp/src/typescript-blocks.ts index fe14aa23..b236f3cc 100644 --- a/lsp/src/typescript-blocks.ts +++ b/lsp/src/typescript-blocks.ts @@ -91,7 +91,11 @@ function hasTagNameAt(html: string, at: number, name: string): boolean { /** The index of `= 0; at = html.indexOf("= 0; + at = html.indexOf(" `text/javascript