partial-tsx 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/partial.ts +20 -8
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "partial-tsx",
3
3
  "description": "Utilities for completing prefix-growing TSX.",
4
- "version": "0.0.4",
4
+ "version": "0.0.5",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
package/src/partial.ts CHANGED
@@ -4,7 +4,7 @@ const BLOCK_CLOSERS = new Set(Object.values(BLOCK_FOLLOWERS));
4
4
  export type PartialTsxMode = "streaming" | "final";
5
5
  export type PartialTsxOptions = { mode?: PartialTsxMode };
6
6
  type BlockKind = "function" | "control" | "arrow" | "class" | "method" | "block" | "other";
7
- type CompletionFrame = { type: "block"; closer: string; statement: boolean; open?: number; close?: number; jsxExpression?: boolean; statementStart?: number; kind?: BlockKind } | { type: "jsx"; tag: string; open?: number };
7
+ type CompletionFrame = { type: "block"; closer: string; statement: boolean; open?: number; close?: number; headerEnd?: number; jsxExpression?: boolean; statementStart?: number; kind?: BlockKind } | { type: "jsx"; tag: string; open?: number };
8
8
 
9
9
  // Single source of truth for the JSX-text fast path in the four lexer loops below: only `<` (child tag) and `{` (child
10
10
  // expression) leave text mode, so none of them reads `(`/`[`/quotes/`//` in ordinary text as unfinished JavaScript.
@@ -321,10 +321,10 @@ const isLikelyJsxTagStart = (code: string, index: number) => {
321
321
  return previousChar !== ">";
322
322
  };
323
323
 
324
- const isLikelyStatementBlock = (code: string, index: number, stack: CompletionFrame[]) => {
324
+ const isLikelyStatementBlock = (code: string, index: number, headerEnd: number, stack: CompletionFrame[]) => {
325
325
  // A JSX child expression stays an expression even when the preceding prose looks like a JavaScript block header.
326
326
  if (code[index] !== "{" || isInsideJsxText(stack)) return false;
327
- const before = code.slice(0, index).trimEnd();
327
+ const before = code.slice(0, headerEnd).trimEnd();
328
328
  return /\bfunction(?:\s+[\w$]+)?\s*\([^)]*\)$/.test(before) || /\b(?:if|for|while|switch|with)\s*\([^)]*\)$/.test(before) || /\bcatch(?:\s*\([^)]*\))?$/.test(before) || /(?:^|[^\w$])(?:else|try|finally|do)$/.test(before) || before.endsWith("=>");
329
329
  };
330
330
 
@@ -1240,10 +1240,13 @@ const scanStreamingSource = (code: string): StreamingScan => {
1240
1240
  statementRegexBoundary = false;
1241
1241
  const inheritedStart = currentStatementStart();
1242
1242
  const jsxExpression = char === "{" && insideJsxText;
1243
- const kind = jsxExpression ? undefined : getBlockKind(code, index, inheritedStart);
1243
+ // Both classification and later JSX-shell checks must stop before trailing comments. Reuse lexical provenance
1244
+ // instead of rescanning the prefix or stripping comment-like text inside literals (#1814).
1245
+ const headerEnd = lastSignificantEnd;
1246
+ const kind = jsxExpression ? undefined : getBlockKind(code, headerEnd, inheritedStart);
1244
1247
  const statement = !jsxExpression && kind !== "other";
1245
- const statementStart = statement ? findKeywordStart(code, index, inheritedStart, kind ?? "other") : inheritedStart;
1246
- stack.push({ type: "block", closer: BLOCK_FOLLOWERS[char], statement, open: index, jsxExpression, statementStart, kind });
1248
+ const statementStart = statement ? findKeywordStart(code, headerEnd, inheritedStart, kind ?? "other") : inheritedStart;
1249
+ stack.push({ type: "block", closer: BLOCK_FOLLOWERS[char], statement, open: index, headerEnd, jsxExpression, statementStart, kind });
1247
1250
  if (statement && char === "{") scopes.push({ statementStart: index + 1 });
1248
1251
  lastSignificantEnd = index + 1;
1249
1252
  index += 1;
@@ -1286,7 +1289,7 @@ const scanStreamingSource = (code: string): StreamingScan => {
1286
1289
  return { stack, closedBlocks, scopes, regexLiteralStarts, lastSignificantEnd, unclosedLiteralStart, unclosedLiteralStatementStart, incompleteJsxTagStart };
1287
1290
  };
1288
1291
 
1289
- const getFrameHeader = (code: string, frame: Extract<CompletionFrame, { type: "block" }>) => code.slice(frame.statementStart ?? 0, frame.open ?? 0).trim();
1292
+ const getFrameHeader = (code: string, frame: Extract<CompletionFrame, { type: "block" }>) => code.slice(frame.statementStart ?? 0, frame.headerEnd ?? frame.open ?? 0).trim();
1290
1293
  const isComponentFunctionFrame = (code: string, frame: Extract<CompletionFrame, { type: "block" }>) => /^[A-Z]/.test(FUNCTION_HEADER.exec(getFrameHeader(code, frame))?.[1] ?? "");
1291
1294
  const isFunctionDeclarationFrame = (code: string, frame: Extract<CompletionFrame, { type: "block" }>) => FUNCTION_HEADER.test(getFrameHeader(code, frame));
1292
1295
  const isComponentClassFrame = (code: string, frame: Extract<CompletionFrame, { type: "block" }>) => /^[A-Z]/.test(CLASS_HEADER.exec(getFrameHeader(code, frame))?.[1] ?? "");
@@ -1642,6 +1645,7 @@ export function completePartialTsx(code: string, options: PartialTsxOptions = {}
1642
1645
  };
1643
1646
  const stack: CompletionFrame[] = [];
1644
1647
  let index = 0;
1648
+ let lastSignificantEnd = 0;
1645
1649
  const push = (value: string) => {
1646
1650
  output += value;
1647
1651
  };
@@ -1650,6 +1654,7 @@ export function completePartialTsx(code: string, options: PartialTsxOptions = {}
1650
1654
  // Keep completion and committed-prefix scanning in the same lexical mode; never append ')' or ']' to plain text.
1651
1655
  if (isInsideJsxText(stack) && !isJsxTextBoundary(char)) {
1652
1656
  push(char);
1657
+ if (!/\s/.test(char)) lastSignificantEnd = index + 1;
1653
1658
  index += 1;
1654
1659
  continue;
1655
1660
  }
@@ -1674,16 +1679,22 @@ export function completePartialTsx(code: string, options: PartialTsxOptions = {}
1674
1679
  index = closeIndex + 2;
1675
1680
  continue;
1676
1681
  }
1682
+ // Match scanStreamingSource's header boundary without losing comments from the completed output.
1683
+ const headerEnd = lastSignificantEnd;
1684
+ if (!/\s/.test(char)) lastSignificantEnd = index + 1;
1677
1685
  if (regexLiteralStarts?.has(index) || isLikelyRegexLiteralStart(code, index)) {
1678
1686
  index = consumeRegexLiteral(code, index, push);
1687
+ lastSignificantEnd = index;
1679
1688
  continue;
1680
1689
  }
1681
1690
  if (char === '"' || char === "'") {
1682
1691
  index = consumeStringLiteral(code, index, push);
1692
+ lastSignificantEnd = index;
1683
1693
  continue;
1684
1694
  }
1685
1695
  if (char === "`") {
1686
1696
  index = consumeTemplateLiteral(code, index, push);
1697
+ lastSignificantEnd = index;
1687
1698
  continue;
1688
1699
  }
1689
1700
  if (char === "<" && isInsideJsxText(stack) && !shouldStartJsxTag(code, index, stack) && index < code.length - 1) {
@@ -1712,11 +1723,12 @@ export function completePartialTsx(code: string, options: PartialTsxOptions = {}
1712
1723
  const tagName = match?.[1] ?? (tag === "<>" || tag === "</>" ? "" : null);
1713
1724
  if (!(tagName && VOID_TAGS.has(tagName) && tag.startsWith("</"))) push(tagName && VOID_TAGS.has(tagName) && !tag.endsWith("/>") ? tag.replace(/\s*\/?\s*>$/, " />") : tag);
1714
1725
  index = closeIndex + 1;
1726
+ lastSignificantEnd = index;
1715
1727
  updateJsxStackForTag(stack, tag);
1716
1728
  continue;
1717
1729
  }
1718
1730
  if (char in BLOCK_FOLLOWERS) {
1719
- stack.push({ type: "block", closer: BLOCK_FOLLOWERS[char], statement: isLikelyStatementBlock(code, index, stack), open: output.length, jsxExpression: char === "{" && isInsideJsxText(stack) });
1731
+ stack.push({ type: "block", closer: BLOCK_FOLLOWERS[char], statement: isLikelyStatementBlock(code, index, headerEnd, stack), open: output.length, jsxExpression: char === "{" && isInsideJsxText(stack) });
1720
1732
  push(char);
1721
1733
  index += 1;
1722
1734
  continue;