fix: retain completed results during live editing
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.4.1 — 2026-07-27
|
||||||
|
|
||||||
|
- Keep the last completed syntax and execution frame visible while edited
|
||||||
|
inputs are parsed and executed, then replace it atomically with the newest
|
||||||
|
revision instead of rendering transient empty panels.
|
||||||
|
- Bind every retained result to its exact flavour, options, pattern, flags,
|
||||||
|
subject, replacement, mode and syntax snapshots. Mark it as stale, disable
|
||||||
|
execution until current syntax is ready, and suppress stale editor ranges
|
||||||
|
and cross-panel selection.
|
||||||
|
|
||||||
## 0.4.0 — 2026-07-27
|
## 0.4.0 — 2026-07-27
|
||||||
|
|
||||||
- Expand the workbench from four to twelve selectable execution profiles:
|
- Expand the workbench from four to twelve selectable execution profiles:
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Patterns, replacement templates, test text and project files stay in the
|
|||||||
browser. There is no account, backend, upload, remote execution, telemetry,
|
browser. There is no account, backend, upload, remote execution, telemetry,
|
||||||
runtime CDN, remote font, or cloud-save service.
|
runtime CDN, remote font, or cloud-save service.
|
||||||
|
|
||||||
## Current release — 0.4.0
|
## Current release — 0.4.1
|
||||||
|
|
||||||
The v0.4.0 release contains twelve selectable, executable profiles:
|
The v0.4.1 release contains twelve selectable, executable profiles:
|
||||||
|
|
||||||
- **JavaScript (ECMAScript)** — the current browser's native `RegExp`, with
|
- **JavaScript (ECMAScript)** — the current browser's native `RegExp`, with
|
||||||
ECMAScript 2025 syntax supplied by regexpp 4.12.2;
|
ECMAScript 2025 syntax supplied by regexpp 4.12.2;
|
||||||
@@ -99,6 +99,9 @@ relabeled Scala engine.
|
|||||||
- Parsing and execution run in separate workers.
|
- Parsing and execution run in separate workers.
|
||||||
- Actual execution can be terminated by killing its worker.
|
- Actual execution can be terminated by killing its worker.
|
||||||
- The worker is recreated after timeout, crash or cancellation.
|
- The worker is recreated after timeout, crash or cancellation.
|
||||||
|
- Live editing retains the last completed, input-bound syntax and result frame
|
||||||
|
until the newest revision finishes. Stale frames are labelled and cannot
|
||||||
|
enable Run, decorate current editors, or drive current-input selections.
|
||||||
- ECMAScript capture ranges use the engine's `d` indices result; every other
|
- ECMAScript capture ranges use the engine's `d` indices result; every other
|
||||||
profile returns its native range records through a bounded fixed bridge.
|
profile returns its native range records through a bounded fixed bridge.
|
||||||
- Named, numbered, optional, empty and repeated-final captures are represented.
|
- Named, numbered, optional, empty and repeated-final captures are represented.
|
||||||
|
|||||||
10
SOURCE.md
10
SOURCE.md
@@ -1,10 +1,10 @@
|
|||||||
# Source identity
|
# Source identity
|
||||||
|
|
||||||
- Project: Regex Tools
|
- Project: Regex Tools
|
||||||
- Release version: `0.4.0`
|
- Release version: `0.4.1`
|
||||||
- Release tag: `v0.4.0`
|
- Release tag: `v0.4.1`
|
||||||
- Repository: <https://git.add-ideas.de/zemion/regex-tools>
|
- Repository: <https://git.add-ideas.de/zemion/regex-tools>
|
||||||
- Previous release tag: `v0.3.0`
|
- Previous release tag: `v0.4.0`
|
||||||
- Licence: GPL-3.0-or-later
|
- Licence: GPL-3.0-or-later
|
||||||
- Copyright: © 2026 Albrecht Degering
|
- Copyright: © 2026 Albrecht Degering
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ metadata, closed file sets, checksums and exact licence/notice material are
|
|||||||
installed under `public/engines/` and included in the application source and
|
installed under `public/engines/` and included in the application source and
|
||||||
build.
|
build.
|
||||||
|
|
||||||
The v0.4.0 release includes native browser ECMAScript, standalone PCRE2 10.47,
|
The v0.4.1 release includes native browser ECMAScript, standalone PCRE2 10.47,
|
||||||
PHP 8.5.8 `preg` with PCRE2 10.44, legacy Perl 5.28.1 via WebPerl 0.09-beta,
|
PHP 8.5.8 `preg` with PCRE2 10.44, legacy Perl 5.28.1 via WebPerl 0.09-beta,
|
||||||
CPython 3.14.2 `re`, CRuby 4.0.0 `Regexp`, TeaVM 0.15.0 `java.util.regex`,
|
CPython 3.14.2 `re`, CRuby 4.0.0 `Regexp`, TeaVM 0.15.0 `java.util.regex`,
|
||||||
Emscripten 6.0.4 libc++ `std::wregex`, Go 1.26.5 `regexp`, Rust `regex` 1.13.1,
|
Emscripten 6.0.4 libc++ `std::wregex`, Go 1.26.5 `regexp`, Rust `regex` 1.13.1,
|
||||||
@@ -91,4 +91,4 @@ TeaVM-backed Scala/JVM compatibility profile.
|
|||||||
|
|
||||||
Earlier releases remain available under their historical tags and artifact
|
Earlier releases remain available under their historical tags and artifact
|
||||||
coordinates. A future public artifact must bump package, manifest, tag and
|
coordinates. A future public artifact must bump package, manifest, tag and
|
||||||
source identity together; it must not reuse `v0.4.0`.
|
source identity together; it must not reuse `v0.4.1`.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ budget. Consumers must inspect the preview status rather than treating a
|
|||||||
preview as complete. In particular, list export is disabled if its template
|
preview as complete. In particular, list export is disabled if its template
|
||||||
would consume an incomplete value.
|
would consume an incomplete value.
|
||||||
|
|
||||||
Repeated captures expose only the final retained value in all twelve v0.4.0
|
Repeated captures expose only the final retained value in all twelve v0.4.1
|
||||||
profiles. This includes .NET even though its native API can retain capture
|
profiles. This includes .NET even though its native API can retain capture
|
||||||
history: the current adapter deliberately normalizes only the final record and
|
history: the current adapter deliberately normalizes only the final record and
|
||||||
reports `captureHistory: false`. The UI labels the limitation and does not
|
reports `captureHistory: false`. The UI labels the limitation and does not
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Flavour support
|
# Flavour support
|
||||||
|
|
||||||
Version 0.4.0 exposes twelve executable profiles. “Partial lexical provider”
|
Version 0.4.1 exposes twelve executable profiles. “Partial lexical provider”
|
||||||
means that Regex Tools recognizes common structure and replacement tokens for
|
means that Regex Tools recognizes common structure and replacement tokens for
|
||||||
explanation only; the named runtime compiler and replacement operation remain
|
explanation only; the named runtime compiler and replacement operation remain
|
||||||
authoritative.
|
authoritative.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ selected real ECMAScript or PCRE2 worker. It can preserve:
|
|||||||
authoritative, untruncated output.
|
authoritative, untruncated output.
|
||||||
|
|
||||||
PHP, Perl, Python, Ruby, Java, C++, Go, Rust, .NET and Scala compatibility are
|
PHP, Perl, Python, Ruby, Java, C++, Go, Rust, .NET and Scala compatibility are
|
||||||
not supported minimization targets in v0.4.0. The workspace disables reduction
|
not supported minimization targets in v0.4.1. The workspace disables reduction
|
||||||
for those active profiles; it does not substitute ECMAScript, PCRE2 or a
|
for those active profiles; it does not substitute ECMAScript, PCRE2 or a
|
||||||
lexical provider as an execution oracle.
|
lexical provider as an execution oracle.
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ should test both a direct root mount and a nested mount such as
|
|||||||
`/apps/regex/`, including cold loading of all twelve profiles.
|
`/apps/regex/`, including cold loading of all twelve profiles.
|
||||||
|
|
||||||
The Portal must consume the immutable release ZIP, verify its SHA-256, verify
|
The Portal must consume the immutable release ZIP, verify its SHA-256, verify
|
||||||
manifest ID `de.add-ideas.regex-tools` and version `0.4.0`, and mount it at a
|
manifest ID `de.add-ideas.regex-tools` and version `0.4.1`, and mount it at a
|
||||||
relative target such as `apps/regex/`.
|
relative target such as `apps/regex/`.
|
||||||
|
|
||||||
The package, manifest, tag, artifact URL and checksum must all use the v0.4.0
|
The package, manifest, tag, artifact URL and checksum must all use the v0.4.1
|
||||||
identity. Historical artifact coordinates remain immutable and must not be
|
identity. Historical artifact coordinates remain immutable and must not be
|
||||||
overwritten or reused.
|
overwritten or reused.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Inspection date: 2026-07-27.
|
|||||||
|
|
||||||
| Reference | Exact revision/version | Licence | Use and copying status |
|
| Reference | Exact revision/version | Licence | Use and copying status |
|
||||||
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Regex Tools | Release identity `v0.4.0` | GPL-3.0-or-later | Application source written for this project. |
|
| Regex Tools | Release identity `v0.4.1` | GPL-3.0-or-later | Application source written for this project. |
|
||||||
| Toolbox SDK | `53c40a61ba1581246f65773fcbb1c1cfd31ac98e` / 0.2.2 | Apache-2.0 | Contract, shell and release conventions; package APIs used, no source adapted. |
|
| Toolbox SDK | `53c40a61ba1581246f65773fcbb1c1cfd31ac98e` / 0.2.2 | Apache-2.0 | Contract, shell and release conventions; package APIs used, no source adapted. |
|
||||||
| Toolbox Portal | `9347554324299c8ec9dfaec2c9730907736f62c3` / aggregate v0.8.0 / package 0.2.7 | AGPL-3.0-only | Assembly/lock conventions inspected; no source copied into the app. |
|
| Toolbox Portal | `9347554324299c8ec9dfaec2c9730907736f62c3` / aggregate v0.8.0 / package 0.2.7 | AGPL-3.0-only | Assembly/lock conventions inspected; no source copied into the app. |
|
||||||
| regexpp | 4.12.2; pinned npm integrity in `package-lock.json` | MIT | ECMAScript 2025 syntax provider; normalized in the syntax worker. |
|
| regexpp | 4.12.2; pinned npm integrity in `package-lock.json` | MIT | ECMAScript 2025 syntax provider; normalized in the syntax worker. |
|
||||||
@@ -30,6 +30,6 @@ Inspection date: 2026-07-27.
|
|||||||
| RegexLib | Website inspected | Redistribution licence not established | No fixture copied. |
|
| RegexLib | Website inspected | Redistribution licence not established | No fixture copied. |
|
||||||
| RGXP.RU | Website inspected | Redistribution licence not established | No fixture copied. |
|
| RGXP.RU | Website inspected | Redistribution licence not established | No fixture copied. |
|
||||||
|
|
||||||
All v0.1.0 through v0.4.0 conformance cases are project-authored. See
|
All v0.1.0 through v0.4.1 conformance cases are project-authored. See
|
||||||
`tests/fixtures/README.md`. Runtime pack metadata and adjacent licence/notice
|
`tests/fixtures/README.md`. Runtime pack metadata and adjacent licence/notice
|
||||||
files are authoritative for the exact binary distribution.
|
files are authoritative for the exact binary distribution.
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ on publication failure, replaces an existing exact version only with
|
|||||||
`--force`, and produces:
|
`--force`, and produces:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
release/regex-tools-0.4.0.zip
|
release/regex-tools-0.4.1.zip
|
||||||
release/regex-tools-0.4.0.zip.sha256
|
release/regex-tools-0.4.1.zip.sha256
|
||||||
```
|
```
|
||||||
|
|
||||||
The release scan checks ASCII/UTF-8, UTF-16LE and UTF-16BE content in text,
|
The release scan checks ASCII/UTF-8, UTF-16LE and UTF-16BE content in text,
|
||||||
@@ -43,6 +43,5 @@ anchored base-runtime legal files. The C++ gate requires its exact traced
|
|||||||
component/source manifest and six independently pinned legal files. The release
|
component/source manifest and six independently pinned legal files. The release
|
||||||
packager names every one of those files explicitly.
|
packager names every one of those files explicitly.
|
||||||
|
|
||||||
The existing v0.1.0, v0.2.0 and v0.3.0 ZIPs and matching sidecars are
|
The existing v0.1.0 through v0.4.0 ZIPs and matching sidecars are historical
|
||||||
historical immutable artifacts. Creating v0.4.0 must not replace, rename or
|
immutable artifacts. Creating v0.4.1 must not replace, rename or remove them.
|
||||||
remove them.
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "regex-tools",
|
"name": "regex-tools",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "regex-tools",
|
"name": "regex-tools",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.2",
|
"@add-ideas/toolbox-contract": "0.2.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "regex-tools",
|
"name": "regex-tools",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "Develop, explain, test and apply regular expressions locally in the browser.",
|
"description": "Develop, explain, test and apply regular expressions locally in the browser.",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"author": "Albrecht Degering",
|
"author": "Albrecht Degering",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.regex-tools",
|
"id": "de.add-ideas.regex-tools",
|
||||||
"name": "Regex Tools",
|
"name": "Regex Tools",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "Develop, explain, test and apply regular expressions locally in the browser.",
|
"description": "Develop, explain, test and apply regular expressions locally in the browser.",
|
||||||
"entry": "./",
|
"entry": "./",
|
||||||
"icon": "./favicon.svg",
|
"icon": "./favicon.svg",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|||||||
const zipEpoch = new Date(1980, 0, 1, 0, 0, 0);
|
const zipEpoch = new Date(1980, 0, 1, 0, 0, 0);
|
||||||
const gplVersion3TextSha256 =
|
const gplVersion3TextSha256 =
|
||||||
"fb981668c18a279e285fc4d83fba1e836cc84dd4daa73c9697d3cfd2d8aca6e0";
|
"fb981668c18a279e285fc4d83fba1e836cc84dd4daa73c9697d3cfd2d8aca6e0";
|
||||||
const expectedApplicationVersion = "0.4.0";
|
const expectedApplicationVersion = "0.4.1";
|
||||||
const requiredRootFiles = [
|
const requiredRootFiles = [
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"README.md",
|
"README.md",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface ExplanationTreeProps {
|
|||||||
readonly root?: NormalizedRegexNode;
|
readonly root?: NormalizedRegexNode;
|
||||||
readonly selectedId?: string;
|
readonly selectedId?: string;
|
||||||
readonly flags?: readonly string[];
|
readonly flags?: readonly string[];
|
||||||
|
readonly stale?: boolean;
|
||||||
readonly onSelect: (node: NormalizedRegexNode) => void;
|
readonly onSelect: (node: NormalizedRegexNode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +166,7 @@ export function ExplanationTree({
|
|||||||
root,
|
root,
|
||||||
selectedId,
|
selectedId,
|
||||||
flags = [],
|
flags = [],
|
||||||
|
stale = false,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: ExplanationTreeProps) {
|
}: ExplanationTreeProps) {
|
||||||
const totalNodes = useMemo(() => (root ? countNodes(root) : 0), [root]);
|
const totalNodes = useMemo(() => (root ? countNodes(root) : 0), [root]);
|
||||||
@@ -191,8 +193,18 @@ export function ExplanationTree({
|
|||||||
<p className="eyebrow">Pattern structure</p>
|
<p className="eyebrow">Pattern structure</p>
|
||||||
<h2 id="explanation-heading">Explanation tree</h2>
|
<h2 id="explanation-heading">Explanation tree</h2>
|
||||||
</div>
|
</div>
|
||||||
<span className="provenance-badge">
|
<span
|
||||||
Deterministic · syntax provider
|
className={`provenance-badge ${stale ? "is-stale" : ""}`}
|
||||||
|
data-testid={stale ? "stale-pattern-syntax" : undefined}
|
||||||
|
title={
|
||||||
|
stale
|
||||||
|
? "This is the last parsed pattern. Its ranges are not applied to the current editor."
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{stale
|
||||||
|
? "Stale snapshot · checking current pattern"
|
||||||
|
: "Deterministic · syntax provider"}
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
{totalNodes > renderedNodes ? (
|
{totalNodes > renderedNodes ? (
|
||||||
|
|||||||
@@ -74,6 +74,24 @@ describe("ReplacementPanel completeness", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("labels retained tokens with the flavour that parsed the snapshot", () => {
|
||||||
|
render(
|
||||||
|
<ReplacementPanel
|
||||||
|
flavour="python"
|
||||||
|
presentedFlavour="java"
|
||||||
|
replacement="$1"
|
||||||
|
onReplacementChange={() => undefined}
|
||||||
|
onSelectToken={() => undefined}
|
||||||
|
onSelectContribution={() => undefined}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Python re template syntax")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Java Pattern derived token explanation"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("labels a match-limited replacement as partial", () => {
|
it("labels a match-limited replacement as partial", () => {
|
||||||
renderResult({
|
renderResult({
|
||||||
execution: { ...execution, truncated: true },
|
execution: { ...execution, truncated: true },
|
||||||
|
|||||||
@@ -71,7 +71,11 @@ export function ReplacementPanel({
|
|||||||
replacement,
|
replacement,
|
||||||
onReplacementChange,
|
onReplacementChange,
|
||||||
syntax,
|
syntax,
|
||||||
|
presentedSyntax,
|
||||||
|
syntaxStale = false,
|
||||||
|
presentedFlavour,
|
||||||
result,
|
result,
|
||||||
|
resultFlavour,
|
||||||
preview,
|
preview,
|
||||||
selectedToken,
|
selectedToken,
|
||||||
selectedContributionId,
|
selectedContributionId,
|
||||||
@@ -83,7 +87,11 @@ export function ReplacementPanel({
|
|||||||
readonly replacement: string;
|
readonly replacement: string;
|
||||||
readonly onReplacementChange: (value: string) => void;
|
readonly onReplacementChange: (value: string) => void;
|
||||||
readonly syntax?: ReplacementSyntaxResult;
|
readonly syntax?: ReplacementSyntaxResult;
|
||||||
|
readonly presentedSyntax?: ReplacementSyntaxResult;
|
||||||
|
readonly syntaxStale?: boolean;
|
||||||
|
readonly presentedFlavour?: RegexFlavourId;
|
||||||
readonly result?: RegexReplacementResult;
|
readonly result?: RegexReplacementResult;
|
||||||
|
readonly resultFlavour?: RegexFlavourId;
|
||||||
readonly preview?: ReplacementPreview;
|
readonly preview?: ReplacementPreview;
|
||||||
readonly selectedToken?: ReplacementToken;
|
readonly selectedToken?: ReplacementToken;
|
||||||
readonly selectedContributionId?: string;
|
readonly selectedContributionId?: string;
|
||||||
@@ -96,8 +104,16 @@ export function ReplacementPanel({
|
|||||||
const activeFlavour =
|
const activeFlavour =
|
||||||
flavour ?? result?.execution.engine.flavour ?? "ecmascript";
|
flavour ?? result?.execution.engine.flavour ?? "ecmascript";
|
||||||
const engineLabel = TEMPLATE_FLAVOUR_LABELS[activeFlavour];
|
const engineLabel = TEMPLATE_FLAVOUR_LABELS[activeFlavour];
|
||||||
const retainedTokens = syntax?.tokens ?? [];
|
const presentedEngineLabel =
|
||||||
const editorTokens = retainedTokens.slice(
|
TEMPLATE_FLAVOUR_LABELS[presentedFlavour ?? activeFlavour];
|
||||||
|
const resultEngineLabel =
|
||||||
|
TEMPLATE_FLAVOUR_LABELS[
|
||||||
|
resultFlavour ?? result?.execution.engine.flavour ?? activeFlavour
|
||||||
|
];
|
||||||
|
const retainedSyntax = presentedSyntax ?? syntax;
|
||||||
|
const retainedTokens = retainedSyntax?.tokens ?? [];
|
||||||
|
const currentTokens = syntax?.tokens ?? [];
|
||||||
|
const editorTokens = currentTokens.slice(
|
||||||
0,
|
0,
|
||||||
TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks,
|
TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks,
|
||||||
);
|
);
|
||||||
@@ -129,7 +145,7 @@ export function ReplacementPanel({
|
|||||||
maximumLengthUtf16={
|
maximumLengthUtf16={
|
||||||
DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16
|
DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16
|
||||||
}
|
}
|
||||||
selectedRange={selectedToken?.range}
|
selectedRange={syntaxStale ? undefined : selectedToken?.range}
|
||||||
marks={[
|
marks={[
|
||||||
...editorTokens.map((token) => ({
|
...editorTokens.map((token) => ({
|
||||||
id: token.id,
|
id: token.id,
|
||||||
@@ -137,7 +153,7 @@ export function ReplacementPanel({
|
|||||||
className: `cm-replacement-token cm-replacement-${token.kind}`,
|
className: `cm-replacement-token cm-replacement-${token.kind}`,
|
||||||
label: token.explanation,
|
label: token.explanation,
|
||||||
})),
|
})),
|
||||||
...(selectedToken
|
...(!syntaxStale && selectedToken
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id: "selected-replacement-token",
|
id: "selected-replacement-token",
|
||||||
@@ -163,7 +179,7 @@ export function ReplacementPanel({
|
|||||||
unit limit.
|
unit limit.
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{retainedTokens.length >
|
{currentTokens.length >
|
||||||
TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks ? (
|
TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks ? (
|
||||||
<p
|
<p
|
||||||
className="render-limit-banner"
|
className="render-limit-banner"
|
||||||
@@ -171,7 +187,7 @@ export function ReplacementPanel({
|
|||||||
>
|
>
|
||||||
<strong>Editor decorations limited.</strong> Showing the first{" "}
|
<strong>Editor decorations limited.</strong> Showing the first{" "}
|
||||||
{TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks.toLocaleString()}{" "}
|
{TEMPLATE_PRESENTATION_LIMITS.replacementEditorMarks.toLocaleString()}{" "}
|
||||||
of {retainedTokens.length.toLocaleString()} complete replacement
|
of {currentTokens.length.toLocaleString()} complete replacement
|
||||||
tokens.
|
tokens.
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -182,9 +198,24 @@ export function ReplacementPanel({
|
|||||||
>
|
>
|
||||||
<header className="panel-heading">
|
<header className="panel-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Derived token explanation</p>
|
<p className="eyebrow">
|
||||||
|
{presentedEngineLabel} derived token explanation
|
||||||
|
</p>
|
||||||
<h2 id="replacement-tree-heading">Replacement tokens</h2>
|
<h2 id="replacement-tree-heading">Replacement tokens</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
className={`provenance-badge ${syntaxStale ? "is-stale" : ""}`}
|
||||||
|
data-testid={syntaxStale ? "stale-replacement-syntax" : undefined}
|
||||||
|
title={
|
||||||
|
syntaxStale
|
||||||
|
? "These are the last parsed template tokens. Their ranges are not applied to the current editor."
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{syntaxStale
|
||||||
|
? "Stale snapshot · checking current template"
|
||||||
|
: "Current template"}
|
||||||
|
</span>
|
||||||
</header>
|
</header>
|
||||||
{renderedTokens.length > 0 ? (
|
{renderedTokens.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
@@ -222,12 +253,12 @@ export function ReplacementPanel({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="empty-state">
|
<p className="empty-state">
|
||||||
{syntax?.accepted === false
|
{retainedSyntax?.accepted === false
|
||||||
? "Token parsing is unavailable until the template is within the input limit."
|
? "Token parsing is unavailable until the template is within the input limit."
|
||||||
: "Enter a replacement template."}
|
: "Enter a replacement template."}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{syntax?.diagnosticsTruncated ? (
|
{retainedSyntax?.diagnosticsTruncated ? (
|
||||||
<p
|
<p
|
||||||
className="render-limit-banner"
|
className="render-limit-banner"
|
||||||
data-testid="replacement-diagnostic-render-limit"
|
data-testid="replacement-diagnostic-render-limit"
|
||||||
@@ -237,7 +268,8 @@ export function ReplacementPanel({
|
|||||||
{TEMPLATE_PRESENTATION_LIMITS.replacementDiagnostics.toLocaleString()}{" "}
|
{TEMPLATE_PRESENTATION_LIMITS.replacementDiagnostics.toLocaleString()}{" "}
|
||||||
entries, including a summary, for{" "}
|
entries, including a summary, for{" "}
|
||||||
{(
|
{(
|
||||||
syntax.totalDiagnostics ?? syntax.diagnostics.length
|
retainedSyntax.totalDiagnostics ??
|
||||||
|
retainedSyntax.diagnostics.length
|
||||||
).toLocaleString()}{" "}
|
).toLocaleString()}{" "}
|
||||||
findings.
|
findings.
|
||||||
</p>
|
</p>
|
||||||
@@ -252,7 +284,7 @@ export function ReplacementPanel({
|
|||||||
<header className="panel-heading">
|
<header className="panel-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">
|
<p className="eyebrow">
|
||||||
Native matches · bounded {engineLabel} substitution
|
Native matches · bounded {resultEngineLabel} substitution
|
||||||
</p>
|
</p>
|
||||||
<h2>Replacement output</h2>
|
<h2>Replacement output</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const syntaxHarness = vi.hoisted(() => ({
|
|||||||
|
|
||||||
const engineHarness = vi.hoisted(() => ({
|
const engineHarness = vi.hoisted(() => ({
|
||||||
requests: [] as RegexExecutionRequest[],
|
requests: [] as RegexExecutionRequest[],
|
||||||
|
emitWholeFirstCharacterMatch: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../editors/CodeEditor", () => ({
|
vi.mock("../editors/CodeEditor", () => ({
|
||||||
@@ -45,18 +46,35 @@ vi.mock("../editors/CodeEditor", () => ({
|
|||||||
onChange,
|
onChange,
|
||||||
label,
|
label,
|
||||||
readOnly,
|
readOnly,
|
||||||
|
marks,
|
||||||
|
selectedRange,
|
||||||
|
onSelectionChange,
|
||||||
}: {
|
}: {
|
||||||
readonly value: string;
|
readonly value: string;
|
||||||
readonly onChange?: (value: string) => void;
|
readonly onChange?: (value: string) => void;
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
readonly readOnly?: boolean;
|
readonly readOnly?: boolean;
|
||||||
|
readonly marks?: readonly unknown[];
|
||||||
|
readonly selectedRange?: { readonly startUtf16: number };
|
||||||
|
readonly onSelectionChange?: (range: {
|
||||||
|
readonly startUtf16: number;
|
||||||
|
readonly endUtf16: number;
|
||||||
|
}) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
value={value}
|
value={value}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
data-mark-count={marks?.length ?? 0}
|
||||||
|
data-selected-start={selectedRange?.startUtf16}
|
||||||
onChange={(event) => onChange?.(event.target.value)}
|
onChange={(event) => onChange?.(event.target.value)}
|
||||||
|
onSelect={(event) =>
|
||||||
|
onSelectionChange?.({
|
||||||
|
startUtf16: event.currentTarget.selectionStart,
|
||||||
|
endUtf16: event.currentTarget.selectionEnd,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -115,7 +133,18 @@ vi.mock("../regex/execution/EngineSupervisor", () => ({
|
|||||||
internallyAddedIndicesFlag: false,
|
internallyAddedIndicesFlag: false,
|
||||||
internallyAddedGlobalFlag: false,
|
internallyAddedGlobalFlag: false,
|
||||||
},
|
},
|
||||||
matches: [],
|
matches: engineHarness.emitWholeFirstCharacterMatch
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
matchNumber: 1,
|
||||||
|
value: request.subject.slice(0, 1),
|
||||||
|
valueStatus: "complete" as const,
|
||||||
|
range: { startUtf16: 0, endUtf16: 1 },
|
||||||
|
nativeRange: { start: 0, end: 1, unit: "utf16" as const },
|
||||||
|
captures: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
elapsedMs: 1,
|
elapsedMs: 1,
|
||||||
truncated: false,
|
truncated: false,
|
||||||
@@ -231,6 +260,7 @@ afterEach(() => {
|
|||||||
syntaxHarness.patterns.length = 0;
|
syntaxHarness.patterns.length = 0;
|
||||||
syntaxHarness.replacements.length = 0;
|
syntaxHarness.replacements.length = 0;
|
||||||
engineHarness.requests.length = 0;
|
engineHarness.requests.length = 0;
|
||||||
|
engineHarness.emitWholeFirstCharacterMatch = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Workbench syntax coordination", () => {
|
describe("Workbench syntax coordination", () => {
|
||||||
@@ -346,6 +376,79 @@ describe("Workbench syntax coordination", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not revive an old syntax snapshot when rapid edits return to the same text", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
render(<Workbench />);
|
||||||
|
fireEvent.click(screen.getByRole("checkbox", { name: "Live" }));
|
||||||
|
|
||||||
|
await advanceParseDebounce();
|
||||||
|
await resolvePattern(0, []);
|
||||||
|
const patternInput = screen.getByRole("textbox", {
|
||||||
|
name: "Regular expression pattern",
|
||||||
|
});
|
||||||
|
const initialPattern = (patternInput as HTMLTextAreaElement).value;
|
||||||
|
const runButton = screen.getByRole("button", { name: "Run" });
|
||||||
|
expect(runButton).toBeEnabled();
|
||||||
|
|
||||||
|
fireEvent.change(patternInput, { target: { value: "temporary" } });
|
||||||
|
fireEvent.change(patternInput, { target: { value: initialPattern } });
|
||||||
|
|
||||||
|
expect(runButton).toBeDisabled();
|
||||||
|
fireEvent.click(screen.getByRole("treeitem"));
|
||||||
|
expect(patternInput).toHaveAttribute("data-mark-count", "0");
|
||||||
|
await advanceParseDebounce();
|
||||||
|
await resolvePattern(1, []);
|
||||||
|
expect(patternInput).toHaveAttribute("data-mark-count", "0");
|
||||||
|
expect(runButton).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retains one exact result frame until the edited subject has a replacement frame", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
engineHarness.emitWholeFirstCharacterMatch = true;
|
||||||
|
render(<Workbench />);
|
||||||
|
fireEvent.click(screen.getByRole("checkbox", { name: "Live" }));
|
||||||
|
|
||||||
|
await advanceParseDebounce();
|
||||||
|
await resolvePattern(0, []);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Run" }));
|
||||||
|
await act(async () => {
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const subjectInput = screen.getByRole("textbox", { name: "Test text" });
|
||||||
|
const originalSubject = (subjectInput as HTMLTextAreaElement).value;
|
||||||
|
expect(subjectInput).toHaveAttribute("data-mark-count", "1");
|
||||||
|
expect(document.querySelector(".extraction-row")).toHaveTextContent("2");
|
||||||
|
|
||||||
|
fireEvent.change(subjectInput, { target: { value: "Z new subject" } });
|
||||||
|
|
||||||
|
expect(screen.getByTestId("stale-result-frame")).toHaveTextContent(
|
||||||
|
"bound to the last completed inputs",
|
||||||
|
);
|
||||||
|
expect(subjectInput).toHaveAttribute("data-mark-count", "0");
|
||||||
|
expect(document.querySelector(".extraction-row")).toHaveTextContent("2");
|
||||||
|
|
||||||
|
fireEvent.change(subjectInput, { target: { value: originalSubject } });
|
||||||
|
expect(screen.getByTestId("stale-result-frame")).toBeInTheDocument();
|
||||||
|
expect(subjectInput).toHaveAttribute("data-mark-count", "0");
|
||||||
|
|
||||||
|
fireEvent.change(subjectInput, { target: { value: "Z new subject" } });
|
||||||
|
(subjectInput as HTMLTextAreaElement).setSelectionRange(0, 1);
|
||||||
|
fireEvent.select(subjectInput);
|
||||||
|
expect(
|
||||||
|
document.querySelector(".extraction-row.is-selected"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Run" }));
|
||||||
|
await act(async () => {
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("stale-result-frame")).not.toBeInTheDocument();
|
||||||
|
expect(subjectInput).toHaveAttribute("data-mark-count", "1");
|
||||||
|
expect(document.querySelector(".extraction-row")).toHaveTextContent("Z");
|
||||||
|
});
|
||||||
|
|
||||||
it("captures a complete result and reruns only the selected saved test", async () => {
|
it("captures a complete result and reruns only the selected saved test", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
render(<Workbench />);
|
render(<Workbench />);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
} from "../regex/model/match";
|
} from "../regex/model/match";
|
||||||
import type {
|
import type {
|
||||||
NormalizedRegexNode,
|
NormalizedRegexNode,
|
||||||
|
RegexSyntaxRequest,
|
||||||
RegexSyntaxResult,
|
RegexSyntaxResult,
|
||||||
ReplacementSyntaxResult,
|
ReplacementSyntaxResult,
|
||||||
ReplacementToken,
|
ReplacementToken,
|
||||||
@@ -105,6 +106,79 @@ type RunState =
|
|||||||
| { readonly status: "timeout"; readonly message: string }
|
| { readonly status: "timeout"; readonly message: string }
|
||||||
| { readonly status: "error"; readonly message: string };
|
| { readonly status: "error"; readonly message: string };
|
||||||
|
|
||||||
|
interface ReplacementSyntaxSnapshot {
|
||||||
|
readonly revision: number;
|
||||||
|
readonly flavour: RegexFlavourId;
|
||||||
|
readonly replacement: string;
|
||||||
|
readonly patternSyntax: RegexSyntaxResult;
|
||||||
|
readonly result: ReplacementSyntaxResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResultFrame {
|
||||||
|
readonly revision: number;
|
||||||
|
readonly flavour: RegexFlavourId;
|
||||||
|
readonly flavourVersion: string;
|
||||||
|
readonly options: RegexEngineOptions;
|
||||||
|
readonly pattern: string;
|
||||||
|
readonly flags: readonly string[];
|
||||||
|
readonly subject: string;
|
||||||
|
readonly replacement: string;
|
||||||
|
readonly mode: WorkspaceMode;
|
||||||
|
readonly scanAll: boolean;
|
||||||
|
readonly syntaxRequest: RegexSyntaxRequest;
|
||||||
|
readonly syntax: RegexSyntaxResult;
|
||||||
|
readonly replacementSyntax?: ReplacementSyntaxResult;
|
||||||
|
readonly execution: RegexExecutionResult;
|
||||||
|
readonly replacementResult?: RegexReplacementResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
function engineOptionsEqual(
|
||||||
|
left: RegexEngineOptions,
|
||||||
|
right: RegexEngineOptions,
|
||||||
|
): boolean {
|
||||||
|
const entries = Object.entries(left);
|
||||||
|
return (
|
||||||
|
entries.length === Object.keys(right).length &&
|
||||||
|
entries.every(([name, value]) => right[name] === value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultFrameMatchesInput(
|
||||||
|
frame: ResultFrame | undefined,
|
||||||
|
revision: number,
|
||||||
|
input: {
|
||||||
|
readonly flavour: RegexFlavourId;
|
||||||
|
readonly flavourVersion: string;
|
||||||
|
readonly options: RegexEngineOptions;
|
||||||
|
readonly pattern: string;
|
||||||
|
readonly flags: readonly string[];
|
||||||
|
readonly subject: string;
|
||||||
|
readonly replacement: string;
|
||||||
|
readonly mode: WorkspaceMode;
|
||||||
|
readonly scanAll: boolean;
|
||||||
|
readonly syntax: RegexSyntaxResult | undefined;
|
||||||
|
readonly replacementSyntax: ReplacementSyntaxResult | undefined;
|
||||||
|
},
|
||||||
|
): frame is ResultFrame {
|
||||||
|
return (
|
||||||
|
frame !== undefined &&
|
||||||
|
frame.revision === revision &&
|
||||||
|
frame.flavour === input.flavour &&
|
||||||
|
frame.flavourVersion === input.flavourVersion &&
|
||||||
|
frame.pattern === input.pattern &&
|
||||||
|
frame.subject === input.subject &&
|
||||||
|
frame.replacement === input.replacement &&
|
||||||
|
frame.mode === input.mode &&
|
||||||
|
frame.scanAll === input.scanAll &&
|
||||||
|
frame.syntax === input.syntax &&
|
||||||
|
frame.flags.length === input.flags.length &&
|
||||||
|
frame.flags.every((flag, index) => flag === input.flags[index]) &&
|
||||||
|
engineOptionsEqual(frame.options, input.options) &&
|
||||||
|
(frame.mode !== "replace" ||
|
||||||
|
frame.replacementSyntax === input.replacementSyntax)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function findSmallestNode(
|
function findSmallestNode(
|
||||||
node: NormalizedRegexNode,
|
node: NormalizedRegexNode,
|
||||||
range: SourceRange,
|
range: SourceRange,
|
||||||
@@ -401,11 +475,9 @@ export function Workbench() {
|
|||||||
);
|
);
|
||||||
const [syntaxSnapshot, setSyntaxSnapshot] =
|
const [syntaxSnapshot, setSyntaxSnapshot] =
|
||||||
useState<PatternSyntaxSnapshot<RegexSyntaxResult>>();
|
useState<PatternSyntaxSnapshot<RegexSyntaxResult>>();
|
||||||
const [replacementSyntax, setReplacementSyntax] =
|
const [replacementSyntaxSnapshot, setReplacementSyntaxSnapshot] =
|
||||||
useState<ReplacementSyntaxResult>();
|
useState<ReplacementSyntaxSnapshot>();
|
||||||
const [execution, setExecution] = useState<RegexExecutionResult>();
|
const [resultFrame, setResultFrame] = useState<ResultFrame>();
|
||||||
const [replacementResult, setReplacementResult] =
|
|
||||||
useState<RegexReplacementResult>();
|
|
||||||
const [selectedSyntax, setSelectedSyntax] = useState<NormalizedRegexNode>();
|
const [selectedSyntax, setSelectedSyntax] = useState<NormalizedRegexNode>();
|
||||||
const [requestedPatternRange, setRequestedPatternRange] =
|
const [requestedPatternRange, setRequestedPatternRange] =
|
||||||
useState<SourceRange>();
|
useState<SourceRange>();
|
||||||
@@ -443,6 +515,10 @@ export function Workbench() {
|
|||||||
const [showSubjectWhitespace, setShowSubjectWhitespace] = useState(false);
|
const [showSubjectWhitespace, setShowSubjectWhitespace] = useState(false);
|
||||||
const [corpusSession, setCorpusSession] = useState(0);
|
const [corpusSession, setCorpusSession] = useState(0);
|
||||||
const [subjectCursor, setSubjectCursor] = useState({ line: 1, column: 1 });
|
const [subjectCursor, setSubjectCursor] = useState({ line: 1, column: 1 });
|
||||||
|
const [syntaxGeneration, setSyntaxGeneration] = useState(0);
|
||||||
|
const [replacementSyntaxGeneration, setReplacementSyntaxGeneration] =
|
||||||
|
useState(0);
|
||||||
|
const [executionGeneration, setExecutionGeneration] = useState(0);
|
||||||
const [projectIdentity, setProjectIdentity] = useState(() => ({
|
const [projectIdentity, setProjectIdentity] = useState(() => ({
|
||||||
id: projectId(),
|
id: projectId(),
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@@ -490,9 +566,7 @@ export function Workbench() {
|
|||||||
return testEngineSupervisor.current;
|
return testEngineSupervisor.current;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearExecutionResults = useCallback(() => {
|
const clearResultSelections = useCallback(() => {
|
||||||
setExecution(undefined);
|
|
||||||
setReplacementResult(undefined);
|
|
||||||
setSelectedExtraction(undefined);
|
setSelectedExtraction(undefined);
|
||||||
setSelectedCaptureRow(undefined);
|
setSelectedCaptureRow(undefined);
|
||||||
setRequestedSubjectRange(undefined);
|
setRequestedSubjectRange(undefined);
|
||||||
@@ -502,22 +576,24 @@ export function Workbench() {
|
|||||||
|
|
||||||
const invalidateExecution = (preserveCurrentResult = false) => {
|
const invalidateExecution = (preserveCurrentResult = false) => {
|
||||||
executionRevision.current += 1;
|
executionRevision.current += 1;
|
||||||
|
setExecutionGeneration(executionRevision.current);
|
||||||
engineSupervisor.current?.cancel();
|
engineSupervisor.current?.cancel();
|
||||||
clearExecutionResults();
|
clearResultSelections();
|
||||||
if (!preserveCurrentResult) setCurrentResultDraft(undefined);
|
if (!preserveCurrentResult) setCurrentResultDraft(undefined);
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "idle",
|
status: "idle",
|
||||||
message: "Inputs changed; previous results were cleared.",
|
message:
|
||||||
|
"Inputs changed. The last completed result remains visible until the current inputs complete.",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const invalidatePatternSyntax = () => {
|
const invalidatePatternSyntax = () => {
|
||||||
syntaxRevision.current += 1;
|
syntaxRevision.current += 1;
|
||||||
replacementSyntaxRevision.current += 1;
|
replacementSyntaxRevision.current += 1;
|
||||||
|
setSyntaxGeneration(syntaxRevision.current);
|
||||||
|
setReplacementSyntaxGeneration(replacementSyntaxRevision.current);
|
||||||
patternSyntaxSupervisor.current?.cancel();
|
patternSyntaxSupervisor.current?.cancel();
|
||||||
replacementSyntaxSupervisor.current?.cancel();
|
replacementSyntaxSupervisor.current?.cancel();
|
||||||
setSyntaxSnapshot(undefined);
|
|
||||||
setReplacementSyntax(undefined);
|
|
||||||
setSelectedSyntax(undefined);
|
setSelectedSyntax(undefined);
|
||||||
setSelectedReplacementToken(undefined);
|
setSelectedReplacementToken(undefined);
|
||||||
setSelectedReplacementContribution(undefined);
|
setSelectedReplacementContribution(undefined);
|
||||||
@@ -539,8 +615,8 @@ export function Workbench() {
|
|||||||
const changeReplacement = (value: string) => {
|
const changeReplacement = (value: string) => {
|
||||||
invalidateExecution();
|
invalidateExecution();
|
||||||
replacementSyntaxRevision.current += 1;
|
replacementSyntaxRevision.current += 1;
|
||||||
|
setReplacementSyntaxGeneration(replacementSyntaxRevision.current);
|
||||||
replacementSyntaxSupervisor.current?.cancel();
|
replacementSyntaxSupervisor.current?.cancel();
|
||||||
setReplacementSyntax(undefined);
|
|
||||||
setSelectedReplacementToken(undefined);
|
setSelectedReplacementToken(undefined);
|
||||||
setSelectedReplacementContribution(undefined);
|
setSelectedReplacementContribution(undefined);
|
||||||
setRequestedOutputRange(undefined);
|
setRequestedOutputRange(undefined);
|
||||||
@@ -582,16 +658,20 @@ export function Workbench() {
|
|||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
}, [flags, flavour, flavourVersion, options, pattern]);
|
}, [flags, flavour, flavourVersion, options, pattern]);
|
||||||
const syntax = isPatternSyntaxForInput(syntaxSnapshot, syntaxRequest)
|
const syntax =
|
||||||
|
syntaxSnapshot?.revision === syntaxGeneration &&
|
||||||
|
isPatternSyntaxForInput(syntaxSnapshot, syntaxRequest)
|
||||||
? syntaxSnapshot.result
|
? syntaxSnapshot.result
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const presentedSyntax = syntax ?? syntaxSnapshot?.result;
|
||||||
|
const presentedSyntaxIsStale =
|
||||||
|
presentedSyntax !== undefined && syntax === undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentRevision = syntaxRevision.current;
|
const currentRevision = syntaxRevision.current;
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
if (pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
|
if (pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
|
||||||
if (syntaxRevision.current !== currentRevision) return;
|
if (syntaxRevision.current !== currentRevision) return;
|
||||||
setSyntaxSnapshot(undefined);
|
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: `Pattern exceeds the ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} UTF-16 unit limit.`,
|
message: `Pattern exceeds the ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} UTF-16 unit limit.`,
|
||||||
@@ -613,7 +693,6 @@ export function Workbench() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
if (!result.accepted) {
|
if (!result.accepted) {
|
||||||
clearExecutionResults();
|
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
message:
|
message:
|
||||||
@@ -622,7 +701,6 @@ export function Workbench() {
|
|||||||
} else if (
|
} else if (
|
||||||
result.captures.length > DEFAULT_REGEX_LIMITS.maximumCaptureGroups
|
result.captures.length > DEFAULT_REGEX_LIMITS.maximumCaptureGroups
|
||||||
) {
|
) {
|
||||||
clearExecutionResults();
|
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: captureGroupLimitMessage(result.captures.length),
|
message: captureGroupLimitMessage(result.captures.length),
|
||||||
@@ -637,7 +715,6 @@ export function Workbench() {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearExecutionResults();
|
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: error instanceof Error ? error.message : String(error),
|
message: error instanceof Error ? error.message : String(error),
|
||||||
@@ -645,20 +722,30 @@ export function Workbench() {
|
|||||||
});
|
});
|
||||||
}, DEFAULT_REGEX_LIMITS.liveParseDebounceMs);
|
}, DEFAULT_REGEX_LIMITS.liveParseDebounceMs);
|
||||||
return () => window.clearTimeout(timer);
|
return () => window.clearTimeout(timer);
|
||||||
}, [clearExecutionResults, pattern, syntaxRequest]);
|
}, [pattern, syntaxRequest]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!syntax) return;
|
if (!syntax) return;
|
||||||
const currentRevision = ++replacementSyntaxRevision.current;
|
const currentRevision = replacementSyntaxRevision.current;
|
||||||
|
const patternSyntax = syntax;
|
||||||
void replacementSyntaxEngine()
|
void replacementSyntaxEngine()
|
||||||
.parseReplacement({
|
.parseReplacement({
|
||||||
flavour,
|
flavour,
|
||||||
replacement,
|
replacement,
|
||||||
captureMetadata: syntax.captures,
|
captureMetadata: patternSyntax.captures,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (replacementSyntaxRevision.current !== currentRevision) return;
|
if (replacementSyntaxRevision.current !== currentRevision) return;
|
||||||
setReplacementSyntax(result);
|
setReplacementSyntaxSnapshot({
|
||||||
|
revision: currentRevision,
|
||||||
|
flavour,
|
||||||
|
replacement,
|
||||||
|
patternSyntax,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
setSelectedReplacementToken(undefined);
|
||||||
|
setSelectedReplacementContribution(undefined);
|
||||||
|
setRequestedOutputRange(undefined);
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
if (replacementSyntaxRevision.current !== currentRevision) return;
|
if (replacementSyntaxRevision.current !== currentRevision) return;
|
||||||
@@ -671,6 +758,22 @@ export function Workbench() {
|
|||||||
});
|
});
|
||||||
}, [flavour, replacement, syntax]);
|
}, [flavour, replacement, syntax]);
|
||||||
|
|
||||||
|
const replacementSyntax =
|
||||||
|
replacementSyntaxSnapshot?.revision === replacementSyntaxGeneration &&
|
||||||
|
replacementSyntaxSnapshot.flavour === flavour &&
|
||||||
|
replacementSyntaxSnapshot.replacement === replacement &&
|
||||||
|
replacementSyntaxSnapshot.patternSyntax === syntax
|
||||||
|
? replacementSyntaxSnapshot.result
|
||||||
|
: undefined;
|
||||||
|
const presentedReplacementSyntax =
|
||||||
|
replacementSyntax ?? replacementSyntaxSnapshot?.result;
|
||||||
|
const presentedReplacementSyntaxIsStale =
|
||||||
|
presentedReplacementSyntax !== undefined && replacementSyntax === undefined;
|
||||||
|
const presentedReplacementFlavour =
|
||||||
|
replacementSyntax !== undefined
|
||||||
|
? flavour
|
||||||
|
: replacementSyntaxSnapshot?.flavour;
|
||||||
|
|
||||||
const subjectBytes = useMemo(() => utf8ByteLength(subject), [subject]);
|
const subjectBytes = useMemo(() => utf8ByteLength(subject), [subject]);
|
||||||
|
|
||||||
const makeRequest = useCallback(
|
const makeRequest = useCallback(
|
||||||
@@ -704,7 +807,6 @@ export function Workbench() {
|
|||||||
window.clearTimeout(liveExecutionTimer.current);
|
window.clearTimeout(liveExecutionTimer.current);
|
||||||
liveExecutionTimer.current = undefined;
|
liveExecutionTimer.current = undefined;
|
||||||
}
|
}
|
||||||
clearExecutionResults();
|
|
||||||
setCurrentResultDraft(undefined);
|
setCurrentResultDraft(undefined);
|
||||||
if (
|
if (
|
||||||
!syntax?.accepted ||
|
!syntax?.accepted ||
|
||||||
@@ -744,6 +846,23 @@ export function Workbench() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const requestRevision = ++executionRevision.current;
|
const requestRevision = ++executionRevision.current;
|
||||||
|
setExecutionGeneration(requestRevision);
|
||||||
|
const currentSyntax = syntax;
|
||||||
|
const currentReplacementSyntax = replacementSyntax;
|
||||||
|
const frameIdentity = {
|
||||||
|
revision: requestRevision,
|
||||||
|
flavour,
|
||||||
|
flavourVersion,
|
||||||
|
options: { ...options },
|
||||||
|
pattern,
|
||||||
|
flags: [...flags],
|
||||||
|
subject,
|
||||||
|
replacement,
|
||||||
|
mode,
|
||||||
|
scanAll,
|
||||||
|
syntaxRequest,
|
||||||
|
syntax: currentSyntax,
|
||||||
|
} as const;
|
||||||
const limit =
|
const limit =
|
||||||
kind === "live"
|
kind === "live"
|
||||||
? DEFAULT_REGEX_LIMITS.liveExecutionTimeoutMs
|
? DEFAULT_REGEX_LIMITS.liveExecutionTimeoutMs
|
||||||
@@ -766,16 +885,13 @@ export function Workbench() {
|
|||||||
limit,
|
limit,
|
||||||
);
|
);
|
||||||
if (executionRevision.current !== requestRevision) return;
|
if (executionRevision.current !== requestRevision) return;
|
||||||
setExecution(result.execution);
|
|
||||||
if (!result.execution.accepted) {
|
if (!result.execution.accepted) {
|
||||||
setReplacementResult(undefined);
|
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: engineRejectionMessage(result.execution),
|
message: engineRejectionMessage(result.execution),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setReplacementResult(result);
|
|
||||||
const commonDraft = {
|
const commonDraft = {
|
||||||
name: "Current replacement result",
|
name: "Current replacement result",
|
||||||
pattern,
|
pattern,
|
||||||
@@ -825,11 +941,17 @@ export function Workbench() {
|
|||||||
: undefined;
|
: undefined;
|
||||||
resultDraftNotice = decision.notice;
|
resultDraftNotice = decision.notice;
|
||||||
setCurrentResultDraft(resultDraft);
|
setCurrentResultDraft(resultDraft);
|
||||||
|
setResultFrame({
|
||||||
|
...frameIdentity,
|
||||||
|
replacementSyntax: currentReplacementSyntax,
|
||||||
|
execution: result.execution,
|
||||||
|
replacementResult: result,
|
||||||
|
});
|
||||||
|
clearResultSelections();
|
||||||
elapsedMs = result.execution.elapsedMs;
|
elapsedMs = result.execution.elapsedMs;
|
||||||
} else {
|
} else {
|
||||||
const result = await executionEngine().execute(makeRequest(), limit);
|
const result = await executionEngine().execute(makeRequest(), limit);
|
||||||
if (executionRevision.current !== requestRevision) return;
|
if (executionRevision.current !== requestRevision) return;
|
||||||
setExecution(result);
|
|
||||||
if (!result.accepted) {
|
if (!result.accepted) {
|
||||||
setRunState({
|
setRunState({
|
||||||
status: "error",
|
status: "error",
|
||||||
@@ -861,6 +983,11 @@ export function Workbench() {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
|
setResultFrame({
|
||||||
|
...frameIdentity,
|
||||||
|
execution: result,
|
||||||
|
});
|
||||||
|
clearResultSelections();
|
||||||
elapsedMs = result.elapsedMs;
|
elapsedMs = result.elapsedMs;
|
||||||
}
|
}
|
||||||
setRunState({
|
setRunState({
|
||||||
@@ -871,7 +998,6 @@ export function Workbench() {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (executionRevision.current !== requestRevision) return;
|
if (executionRevision.current !== requestRevision) return;
|
||||||
clearExecutionResults();
|
|
||||||
const timedOut =
|
const timedOut =
|
||||||
error instanceof WorkerRequestError && error.kind === "timeout";
|
error instanceof WorkerRequestError && error.kind === "timeout";
|
||||||
setRunState({
|
setRunState({
|
||||||
@@ -886,20 +1012,22 @@ export function Workbench() {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
activeTestConfiguration,
|
activeTestConfiguration,
|
||||||
clearExecutionResults,
|
clearResultSelections,
|
||||||
|
flags,
|
||||||
|
flavour,
|
||||||
|
flavourVersion,
|
||||||
makeRequest,
|
makeRequest,
|
||||||
mode,
|
mode,
|
||||||
|
options,
|
||||||
|
pattern,
|
||||||
replacement,
|
replacement,
|
||||||
replacementSyntax?.accepted,
|
replacementSyntax,
|
||||||
scanAll,
|
scanAll,
|
||||||
subjectBytes,
|
|
||||||
subject,
|
subject,
|
||||||
syntax?.accepted,
|
subjectBytes,
|
||||||
syntax?.captures.length,
|
syntax,
|
||||||
syntaxSnapshot,
|
syntaxSnapshot,
|
||||||
syntaxRequest,
|
syntaxRequest,
|
||||||
flags,
|
|
||||||
pattern,
|
|
||||||
tests,
|
tests,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
],
|
],
|
||||||
@@ -943,22 +1071,49 @@ export function Workbench() {
|
|||||||
syntax?.accepted,
|
syntax?.accepted,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const resultFrameIsCurrent = resultFrameMatchesInput(
|
||||||
|
resultFrame,
|
||||||
|
executionGeneration,
|
||||||
|
{
|
||||||
|
flavour,
|
||||||
|
flavourVersion,
|
||||||
|
options,
|
||||||
|
pattern,
|
||||||
|
flags,
|
||||||
|
subject,
|
||||||
|
replacement,
|
||||||
|
mode,
|
||||||
|
scanAll,
|
||||||
|
syntax,
|
||||||
|
replacementSyntax,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const resultFrameIsStale = resultFrame !== undefined && !resultFrameIsCurrent;
|
||||||
|
const execution = resultFrame?.execution;
|
||||||
|
const replacementResult = resultFrame?.replacementResult;
|
||||||
|
const resultSubject = resultFrame?.subject ?? subject;
|
||||||
|
const resultSyntax = resultFrame?.syntax;
|
||||||
|
|
||||||
const extraction = useMemo(
|
const extraction = useMemo(
|
||||||
() => buildExtractionTree(execution?.matches ?? [], syntax?.captures ?? []),
|
() =>
|
||||||
[execution?.matches, syntax?.captures],
|
buildExtractionTree(
|
||||||
|
execution?.matches ?? [],
|
||||||
|
resultSyntax?.captures ?? [],
|
||||||
|
),
|
||||||
|
[execution?.matches, resultSyntax?.captures],
|
||||||
);
|
);
|
||||||
const captureRows = useMemo(
|
const captureRows = useMemo(
|
||||||
() => buildCaptureRows(execution?.matches ?? [], subject),
|
() => buildCaptureRows(execution?.matches ?? [], resultSubject),
|
||||||
[execution?.matches, subject],
|
[execution?.matches, resultSubject],
|
||||||
);
|
);
|
||||||
const repeatedCaptures = useMemo(
|
const repeatedCaptures = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Set(
|
new Set(
|
||||||
syntax?.captures
|
resultSyntax?.captures
|
||||||
.filter((capture) => capture.repeated)
|
.filter((capture) => capture.repeated)
|
||||||
.map((capture) => capture.number) ?? [],
|
.map((capture) => capture.number) ?? [],
|
||||||
),
|
),
|
||||||
[syntax?.captures],
|
[resultSyntax?.captures],
|
||||||
);
|
);
|
||||||
const selectedSubjectRange = useMemo(
|
const selectedSubjectRange = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -971,8 +1126,12 @@ export function Workbench() {
|
|||||||
[selectedCaptureRow, selectedExtraction?.range],
|
[selectedCaptureRow, selectedExtraction?.range],
|
||||||
);
|
);
|
||||||
const subjectMarkSummary = useMemo(
|
const subjectMarkSummary = useMemo(
|
||||||
() => subjectMarks(execution, selectedSubjectRange),
|
() =>
|
||||||
[execution, selectedSubjectRange],
|
subjectMarks(
|
||||||
|
resultFrameIsCurrent ? execution : undefined,
|
||||||
|
resultFrameIsCurrent ? selectedSubjectRange : undefined,
|
||||||
|
),
|
||||||
|
[execution, resultFrameIsCurrent, selectedSubjectRange],
|
||||||
);
|
);
|
||||||
const patternMarkSummary = useMemo(
|
const patternMarkSummary = useMemo(
|
||||||
() => syntaxMarks(syntax, selectedSyntax),
|
() => syntaxMarks(syntax, selectedSyntax),
|
||||||
@@ -980,15 +1139,19 @@ export function Workbench() {
|
|||||||
);
|
);
|
||||||
const replacementPreview = useMemo(
|
const replacementPreview = useMemo(
|
||||||
() =>
|
() =>
|
||||||
replacementSyntax?.accepted === true && replacementResult
|
resultFrame?.replacementSyntax?.accepted === true && replacementResult
|
||||||
? buildReplacementPreview(replacementSyntax, replacementResult, subject)
|
? buildReplacementPreview(
|
||||||
|
resultFrame.replacementSyntax,
|
||||||
|
replacementResult,
|
||||||
|
resultSubject,
|
||||||
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
[replacementResult, replacementSyntax, subject],
|
[replacementResult, resultFrame, resultSubject],
|
||||||
);
|
);
|
||||||
const diagnostics = [
|
const diagnostics = [
|
||||||
...(syntax?.diagnostics ?? []),
|
...(syntax?.diagnostics ?? []),
|
||||||
...(replacementSyntax?.diagnostics ?? []),
|
...(replacementSyntax?.diagnostics ?? []),
|
||||||
...(execution?.diagnostics ?? []),
|
...(resultFrameIsCurrent ? (execution?.diagnostics ?? []) : []),
|
||||||
];
|
];
|
||||||
const currentProject: RegexProjectV1 = {
|
const currentProject: RegexProjectV1 = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
@@ -1029,14 +1192,14 @@ export function Workbench() {
|
|||||||
);
|
);
|
||||||
setSelectedCaptureRow(captureRow);
|
setSelectedCaptureRow(captureRow);
|
||||||
setRequestedSubjectRange(
|
setRequestedSubjectRange(
|
||||||
requestEditorSelection && node.range
|
resultFrameIsCurrent && requestEditorSelection && node.range
|
||||||
? {
|
? {
|
||||||
startUtf16: node.range.startUtf16,
|
startUtf16: node.range.startUtf16,
|
||||||
endUtf16: node.range.endUtf16,
|
endUtf16: node.range.endUtf16,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
if (node.groupNumber !== undefined) {
|
if (resultFrameIsCurrent && node.groupNumber !== undefined) {
|
||||||
const definition = syntax?.captures.find(
|
const definition = syntax?.captures.find(
|
||||||
(capture) => capture.number === node.groupNumber,
|
(capture) => capture.number === node.groupNumber,
|
||||||
);
|
);
|
||||||
@@ -1052,16 +1215,29 @@ export function Workbench() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (resultFrameIsCurrent) {
|
||||||
setSelectedSyntax(syntax?.root);
|
setSelectedSyntax(syntax?.root);
|
||||||
setRequestedPatternRange(syntax?.root.range);
|
setRequestedPatternRange(syntax?.root.range);
|
||||||
|
} else {
|
||||||
|
setSelectedSyntax(undefined);
|
||||||
|
setRequestedPatternRange(undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectSyntaxNode = (
|
const selectSyntaxNode = (
|
||||||
node: NormalizedRegexNode,
|
node: NormalizedRegexNode,
|
||||||
requestEditorSelection = true,
|
requestEditorSelection = true,
|
||||||
|
mapsToCurrentPattern = true,
|
||||||
) => {
|
) => {
|
||||||
|
if (!mapsToCurrentPattern) {
|
||||||
|
setSelectedSyntax(undefined);
|
||||||
|
setRequestedPatternRange(undefined);
|
||||||
|
setSelectedExtraction(undefined);
|
||||||
|
setSelectedCaptureRow(undefined);
|
||||||
|
setRequestedSubjectRange(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedSyntax(node);
|
setSelectedSyntax(node);
|
||||||
setRequestedPatternRange(
|
setRequestedPatternRange(
|
||||||
requestEditorSelection
|
requestEditorSelection
|
||||||
@@ -1072,6 +1248,13 @@ export function Workbench() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!resultFrameIsCurrent) {
|
||||||
|
setSelectedExtraction(undefined);
|
||||||
|
setSelectedCaptureRow(undefined);
|
||||||
|
setRequestedSubjectRange(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const enclosingCapture = syntax?.captures
|
const enclosingCapture = syntax?.captures
|
||||||
.filter(
|
.filter(
|
||||||
(capture) =>
|
(capture) =>
|
||||||
@@ -1129,7 +1312,9 @@ export function Workbench() {
|
|||||||
contribution: ReplacementContributionPreview,
|
contribution: ReplacementContributionPreview,
|
||||||
) => {
|
) => {
|
||||||
setSelectedReplacementContribution(contribution);
|
setSelectedReplacementContribution(contribution);
|
||||||
setSelectedReplacementToken(contribution.token);
|
setSelectedReplacementToken(
|
||||||
|
resultFrameIsCurrent ? contribution.token : undefined,
|
||||||
|
);
|
||||||
setRequestedOutputRange(contribution.outputRange);
|
setRequestedOutputRange(contribution.outputRange);
|
||||||
|
|
||||||
const match = execution?.matches.find(
|
const match = execution?.matches.find(
|
||||||
@@ -1163,7 +1348,7 @@ export function Workbench() {
|
|||||||
} else {
|
} else {
|
||||||
setSelectedExtraction(undefined);
|
setSelectedExtraction(undefined);
|
||||||
setSelectedCaptureRow(undefined);
|
setSelectedCaptureRow(undefined);
|
||||||
setRequestedSubjectRange(match?.range);
|
setRequestedSubjectRange(resultFrameIsCurrent ? match?.range : undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1416,6 +1601,7 @@ export function Workbench() {
|
|||||||
const importedFlavour = AVAILABLE_REGEX_FLAVOURS.require(project.flavour);
|
const importedFlavour = AVAILABLE_REGEX_FLAVOURS.require(project.flavour);
|
||||||
invalidatePatternSyntax();
|
invalidatePatternSyntax();
|
||||||
executionRevision.current += 1;
|
executionRevision.current += 1;
|
||||||
|
setExecutionGeneration(executionRevision.current);
|
||||||
engineSupervisor.current?.cancel();
|
engineSupervisor.current?.cancel();
|
||||||
cancelTestRun();
|
cancelTestRun();
|
||||||
setFlavour(importedFlavour.id);
|
setFlavour(importedFlavour.id);
|
||||||
@@ -1442,7 +1628,10 @@ export function Workbench() {
|
|||||||
setTestResults(new Map());
|
setTestResults(new Map());
|
||||||
setLive(false);
|
setLive(false);
|
||||||
setScanAll(project.ui?.scanAll ?? false);
|
setScanAll(project.ui?.scanAll ?? false);
|
||||||
clearExecutionResults();
|
setResultFrame(undefined);
|
||||||
|
setSyntaxSnapshot(undefined);
|
||||||
|
setReplacementSyntaxSnapshot(undefined);
|
||||||
|
clearResultSelections();
|
||||||
setSelectedSyntax(undefined);
|
setSelectedSyntax(undefined);
|
||||||
setRequestedPatternRange(undefined);
|
setRequestedPatternRange(undefined);
|
||||||
setSelectedReplacementToken(undefined);
|
setSelectedReplacementToken(undefined);
|
||||||
@@ -1973,7 +2162,7 @@ export function Workbench() {
|
|||||||
>
|
>
|
||||||
<CapabilityPanel
|
<CapabilityPanel
|
||||||
syntax={syntax}
|
syntax={syntax}
|
||||||
execution={execution}
|
execution={resultFrameIsCurrent ? execution : undefined}
|
||||||
onClose={() => setShowCapabilities(false)}
|
onClose={() => setShowCapabilities(false)}
|
||||||
/>
|
/>
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
@@ -2041,10 +2230,17 @@ export function Workbench() {
|
|||||||
<span>{runState.status}</span>
|
<span>{runState.status}</span>
|
||||||
<strong>{runState.message}</strong>
|
<strong>{runState.message}</strong>
|
||||||
{execution?.accepted ? (
|
{execution?.accepted ? (
|
||||||
<small>
|
<small
|
||||||
|
className={resultFrameIsStale ? "is-stale" : undefined}
|
||||||
|
data-testid={resultFrameIsStale ? "stale-result-frame" : undefined}
|
||||||
|
>
|
||||||
|
{resultFrameIsStale ? "Stale result · " : ""}
|
||||||
{execution.matches.length.toLocaleString()} matches ·{" "}
|
{execution.matches.length.toLocaleString()} matches ·{" "}
|
||||||
{execution.elapsedMs.toFixed(2)} ms · native offsets{" "}
|
{execution.elapsedMs.toFixed(2)} ms · native offsets{" "}
|
||||||
{execution.engine.offsetUnit}
|
{execution.engine.offsetUnit}
|
||||||
|
{resultFrameIsStale
|
||||||
|
? " · bound to the last completed inputs; no ranges are applied to current editors"
|
||||||
|
: ""}
|
||||||
</small>
|
</small>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -2097,10 +2293,17 @@ export function Workbench() {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<ExplanationTree
|
<ExplanationTree
|
||||||
root={syntax?.root}
|
root={presentedSyntax?.root}
|
||||||
selectedId={selectedSyntax?.id}
|
selectedId={selectedSyntax?.id}
|
||||||
flags={flags}
|
flags={
|
||||||
onSelect={(node) => selectSyntaxNode(node)}
|
presentedSyntaxIsStale
|
||||||
|
? (syntaxSnapshot?.request.flags ?? [])
|
||||||
|
: flags
|
||||||
|
}
|
||||||
|
stale={presentedSyntaxIsStale}
|
||||||
|
onSelect={(node) =>
|
||||||
|
selectSyntaxNode(node, true, !presentedSyntaxIsStale)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{mode !== "tests" && mode !== "corpus" ? (
|
{mode !== "tests" && mode !== "corpus" ? (
|
||||||
@@ -2146,6 +2349,11 @@ export function Workbench() {
|
|||||||
onChange={changeSubject}
|
onChange={changeSubject}
|
||||||
onSelectionChange={(range) => {
|
onSelectionChange={(range) => {
|
||||||
setRequestedSubjectRange(undefined);
|
setRequestedSubjectRange(undefined);
|
||||||
|
if (!resultFrameIsCurrent) {
|
||||||
|
setSelectedExtraction(undefined);
|
||||||
|
setSelectedCaptureRow(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const node = findSmallestExtractionNode(extraction, range);
|
const node = findSmallestExtractionNode(extraction, range);
|
||||||
if (node) {
|
if (node) {
|
||||||
selectExtraction(node, false);
|
selectExtraction(node, false);
|
||||||
@@ -2180,7 +2388,7 @@ export function Workbench() {
|
|||||||
{mode === "match" ? (
|
{mode === "match" ? (
|
||||||
<CaptureTable
|
<CaptureTable
|
||||||
rows={captureRows}
|
rows={captureRows}
|
||||||
subject={subject}
|
subject={resultSubject}
|
||||||
sourceResultTruncated={execution?.truncated ?? false}
|
sourceResultTruncated={execution?.truncated ?? false}
|
||||||
selectedId={selectedCaptureRow?.id}
|
selectedId={selectedCaptureRow?.id}
|
||||||
onSelect={(row) => {
|
onSelect={(row) => {
|
||||||
@@ -2194,7 +2402,7 @@ export function Workbench() {
|
|||||||
setSelectedCaptureRow(row);
|
setSelectedCaptureRow(row);
|
||||||
setSelectedExtraction(undefined);
|
setSelectedExtraction(undefined);
|
||||||
setRequestedSubjectRange(
|
setRequestedSubjectRange(
|
||||||
row.start === undefined
|
!resultFrameIsCurrent || row.start === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
startUtf16: row.start,
|
startUtf16: row.start,
|
||||||
@@ -2202,9 +2410,11 @@ export function Workbench() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const definition = syntax?.captures.find(
|
const definition = resultFrameIsCurrent
|
||||||
|
? syntax?.captures.find(
|
||||||
(capture) => capture.number === row.groupNumber,
|
(capture) => capture.number === row.groupNumber,
|
||||||
);
|
)
|
||||||
|
: undefined;
|
||||||
if (definition && syntax) {
|
if (definition && syntax) {
|
||||||
const selected = findSmallestNode(syntax.root, definition.range);
|
const selected = findSmallestNode(syntax.root, definition.range);
|
||||||
setSelectedSyntax(selected);
|
setSelectedSyntax(selected);
|
||||||
@@ -2226,7 +2436,11 @@ export function Workbench() {
|
|||||||
replacement={replacement}
|
replacement={replacement}
|
||||||
onReplacementChange={changeReplacement}
|
onReplacementChange={changeReplacement}
|
||||||
syntax={replacementSyntax}
|
syntax={replacementSyntax}
|
||||||
|
presentedSyntax={presentedReplacementSyntax}
|
||||||
|
syntaxStale={presentedReplacementSyntaxIsStale}
|
||||||
|
presentedFlavour={presentedReplacementFlavour}
|
||||||
result={replacementResult}
|
result={replacementResult}
|
||||||
|
resultFlavour={resultFrame?.flavour}
|
||||||
preview={replacementPreview}
|
preview={replacementPreview}
|
||||||
selectedToken={selectedReplacementToken}
|
selectedToken={selectedReplacementToken}
|
||||||
selectedContributionId={selectedReplacementContribution?.id}
|
selectedContributionId={selectedReplacementContribution?.id}
|
||||||
@@ -2244,7 +2458,7 @@ export function Workbench() {
|
|||||||
templateSource={listTemplate}
|
templateSource={listTemplate}
|
||||||
onTemplateChange={setListTemplate}
|
onTemplateChange={setListTemplate}
|
||||||
matches={execution?.matches ?? []}
|
matches={execution?.matches ?? []}
|
||||||
subject={subject}
|
subject={resultSubject}
|
||||||
sourceResultTruncated={execution?.truncated ?? false}
|
sourceResultTruncated={execution?.truncated ?? false}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -161,6 +161,24 @@ textarea:focus-visible,
|
|||||||
color: color-mix(in srgb, var(--regex-mint) 70%, var(--toolbox-text));
|
color: color-mix(in srgb, var(--regex-mint) 70%, var(--toolbox-text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.provenance-badge.is-stale,
|
||||||
|
.run-status > small.is-stale {
|
||||||
|
color: color-mix(in srgb, var(--regex-warning) 78%, var(--toolbox-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.provenance-badge.is-stale {
|
||||||
|
border-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--regex-warning) 45%,
|
||||||
|
var(--toolbox-border)
|
||||||
|
);
|
||||||
|
background: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--regex-warning) 9%,
|
||||||
|
var(--toolbox-surface)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.primary-button,
|
.primary-button,
|
||||||
.secondary-button,
|
.secondary-button,
|
||||||
.icon-button {
|
.icon-button {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.regex-tools",
|
"id": "de.add-ideas.regex-tools",
|
||||||
"name": "Regex Tools",
|
"name": "Regex Tools",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "Develop, explain, test and apply regular expressions locally in the browser.",
|
"description": "Develop, explain, test and apply regular expressions locally in the browser.",
|
||||||
"entry": "./",
|
"entry": "./",
|
||||||
"icon": "./favicon.svg",
|
"icon": "./favicon.svg",
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export const APPLICATION_VERSION = "0.4.0";
|
export const APPLICATION_VERSION = "0.4.1";
|
||||||
export const SYNTAX_PROFILE = "community";
|
export const SYNTAX_PROFILE = "community";
|
||||||
export const REGEXPP_VERSION = "4.12.2";
|
export const REGEXPP_VERSION = "4.12.2";
|
||||||
|
|||||||
@@ -446,6 +446,7 @@ test("rapid pattern and replacement edits cannot run with stale syntax", async (
|
|||||||
await page.goto("./");
|
await page.goto("./");
|
||||||
await waitForReady(page);
|
await waitForReady(page);
|
||||||
await page.getByLabel("Live").uncheck();
|
await page.getByLabel("Live").uncheck();
|
||||||
|
const initialExtractionCount = await page.locator(".extraction-row").count();
|
||||||
await page.getByRole("button", { name: "Replace", exact: true }).click();
|
await page.getByRole("button", { name: "Replace", exact: true }).click();
|
||||||
|
|
||||||
await setEditor(page, "editor-regular-expression-pattern", "(?<stale>\\w+)");
|
await setEditor(page, "editor-regular-expression-pattern", "(?<stale>\\w+)");
|
||||||
@@ -462,7 +463,10 @@ test("rapid pattern and replacement edits cannot run with stale syntax", async (
|
|||||||
await expect(runButton).toBeDisabled();
|
await expect(runButton).toBeDisabled();
|
||||||
await runButton.evaluate((button: HTMLButtonElement) => button.click());
|
await runButton.evaluate((button: HTMLButtonElement) => button.click());
|
||||||
await expect(page.locator(".run-status")).not.toHaveClass(/status-running/u);
|
await expect(page.locator(".run-status")).not.toHaveClass(/status-running/u);
|
||||||
await expect(page.locator(".extraction-row")).toHaveCount(0);
|
await expect(page.locator(".extraction-row")).toHaveCount(
|
||||||
|
initialExtractionCount,
|
||||||
|
);
|
||||||
|
await expect(page.getByTestId("stale-result-frame")).toBeVisible();
|
||||||
|
|
||||||
await expect(runButton).toBeEnabled();
|
await expect(runButton).toBeEnabled();
|
||||||
const currentNamedToken = page.locator(".token-row").filter({
|
const currentNamedToken = page.locator(".token-row").filter({
|
||||||
@@ -484,22 +488,31 @@ test("rapid pattern and replacement edits cannot run with stale syntax", async (
|
|||||||
).toHaveText("[A] [B]");
|
).toHaveText("[A] [B]");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("manual editing clears results that no longer describe the inputs", async ({
|
test("manual editing retains an explicitly stale result without applying its ranges", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto("./");
|
await page.goto("./");
|
||||||
await waitForReady(page);
|
await waitForReady(page);
|
||||||
await page.getByLabel("Live").uncheck();
|
await page.getByLabel("Live").uncheck();
|
||||||
|
|
||||||
|
const extractionRows = page.locator(".extraction-row");
|
||||||
|
const captureRows = page.locator(".capture-table-scroll tbody tr");
|
||||||
|
const initialExtractionCount = await extractionRows.count();
|
||||||
|
const initialCaptureCount = await captureRows.count();
|
||||||
|
expect(initialExtractionCount).toBeGreaterThan(0);
|
||||||
|
expect(initialCaptureCount).toBeGreaterThan(0);
|
||||||
|
|
||||||
await setEditor(page, "editor-regular-expression-pattern", "nomatch");
|
await setEditor(page, "editor-regular-expression-pattern", "nomatch");
|
||||||
|
|
||||||
await expect(page.locator(".run-status")).toHaveClass(/status-idle/u);
|
await expect(page.locator(".run-status")).toHaveClass(/status-idle/u);
|
||||||
await expect(page.locator(".run-status")).toContainText(
|
await expect(page.locator(".run-status")).toContainText(
|
||||||
"previous results were cleared",
|
"last completed result remains visible until the current inputs complete",
|
||||||
);
|
);
|
||||||
await expect(page.locator(".run-status small")).toHaveCount(0);
|
await expect(page.getByTestId("stale-result-frame")).toContainText(
|
||||||
await expect(page.locator(".extraction-row")).toHaveCount(0);
|
"no ranges are applied to current editors",
|
||||||
await expect(page.locator(".capture-table-scroll tbody tr")).toHaveCount(0);
|
);
|
||||||
|
await expect(extractionRows).toHaveCount(initialExtractionCount);
|
||||||
|
await expect(captureRows).toHaveCount(initialCaptureCount);
|
||||||
await expect(page.locator(".cm-subject-match")).toHaveCount(0);
|
await expect(page.locator(".cm-subject-match")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -509,6 +522,10 @@ test("capture-group resource limits block execution visibly", async ({
|
|||||||
await page.goto("./");
|
await page.goto("./");
|
||||||
await waitForReady(page);
|
await waitForReady(page);
|
||||||
await page.getByLabel("Live").uncheck();
|
await page.getByLabel("Live").uncheck();
|
||||||
|
const initialExtractionCount = await page.locator(".extraction-row").count();
|
||||||
|
const initialCaptureCount = await page
|
||||||
|
.locator(".capture-table-scroll tbody tr")
|
||||||
|
.count();
|
||||||
|
|
||||||
await setEditor(
|
await setEditor(
|
||||||
page,
|
page,
|
||||||
@@ -520,8 +537,14 @@ test("capture-group resource limits block execution visibly", async ({
|
|||||||
await expect(page.locator(".run-status")).toContainText(
|
await expect(page.locator(".run-status")).toContainText(
|
||||||
"above the 1,000 group execution limit",
|
"above the 1,000 group execution limit",
|
||||||
);
|
);
|
||||||
await expect(page.locator(".run-status small")).toHaveCount(0);
|
await expect(page.getByTestId("stale-result-frame")).toBeVisible();
|
||||||
await expect(page.locator(".extraction-row")).toHaveCount(0);
|
await expect(page.locator(".extraction-row")).toHaveCount(
|
||||||
|
initialExtractionCount,
|
||||||
|
);
|
||||||
|
await expect(page.locator(".capture-table-scroll tbody tr")).toHaveCount(
|
||||||
|
initialCaptureCount,
|
||||||
|
);
|
||||||
|
await expect(page.locator(".cm-subject-match")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("large valid patterns bound syntax tree nodes and editor decorations", async ({
|
test("large valid patterns bound syntax tree nodes and editor decorations", async ({
|
||||||
@@ -779,6 +802,25 @@ test("replacement and list modes use native and typed templates", async ({
|
|||||||
page.getByTestId("editor-replacement-output").locator(".cm-selected-range"),
|
page.getByTestId("editor-replacement-output").locator(".cm-selected-range"),
|
||||||
).toHaveCount(1);
|
).toHaveCount(1);
|
||||||
|
|
||||||
|
await page.getByLabel("Live").uncheck();
|
||||||
|
await setEditor(page, "editor-replacement-template", "[$<user> — $<date>]");
|
||||||
|
await expect(page.getByTestId("stale-replacement-syntax")).not.toBeVisible();
|
||||||
|
await expect(page.getByTestId("stale-result-frame")).toBeVisible();
|
||||||
|
await userContribution.click();
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByTestId("editor-replacement-template")
|
||||||
|
.locator(".cm-selected-range"),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByTestId("editor-regular-expression-pattern")
|
||||||
|
.locator(".cm-selected-range"),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("editor-test-text").locator(".cm-selected-range"),
|
||||||
|
).toHaveCount(0);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "List", exact: true }).click();
|
await page.getByRole("button", { name: "List", exact: true }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("heading", { name: "Generated rows" }),
|
page.getByRole("heading", { name: "Generated rows" }),
|
||||||
@@ -1117,6 +1159,10 @@ test("a timeout kills the worker and the next request uses a fresh engine", asyn
|
|||||||
await page.goto("./");
|
await page.goto("./");
|
||||||
await waitForReady(page);
|
await waitForReady(page);
|
||||||
await page.getByLabel("Live").uncheck();
|
await page.getByLabel("Live").uncheck();
|
||||||
|
const initialExtractionCount = await page.locator(".extraction-row").count();
|
||||||
|
const initialCaptureCount = await page
|
||||||
|
.locator(".capture-table-scroll tbody tr")
|
||||||
|
.count();
|
||||||
await page.getByLabel("Manual limit").selectOption("250");
|
await page.getByLabel("Manual limit").selectOption("250");
|
||||||
await setEditor(page, "editor-regular-expression-pattern", "^(a|aa)+$");
|
await setEditor(page, "editor-regular-expression-pattern", "^(a|aa)+$");
|
||||||
await setEditor(page, "editor-test-text", `${"a".repeat(48)}!`);
|
await setEditor(page, "editor-test-text", `${"a".repeat(48)}!`);
|
||||||
@@ -1127,9 +1173,13 @@ test("a timeout kills the worker and the next request uses a fresh engine", asyn
|
|||||||
await expect(page.locator(".run-status")).toContainText(
|
await expect(page.locator(".run-status")).toContainText(
|
||||||
"worker was terminated",
|
"worker was terminated",
|
||||||
);
|
);
|
||||||
await expect(page.locator(".run-status small")).toHaveCount(0);
|
await expect(page.getByTestId("stale-result-frame")).toBeVisible();
|
||||||
await expect(page.locator(".extraction-row")).toHaveCount(0);
|
await expect(page.locator(".extraction-row")).toHaveCount(
|
||||||
await expect(page.locator(".capture-table-scroll tbody tr")).toHaveCount(0);
|
initialExtractionCount,
|
||||||
|
);
|
||||||
|
await expect(page.locator(".capture-table-scroll tbody tr")).toHaveCount(
|
||||||
|
initialCaptureCount,
|
||||||
|
);
|
||||||
await expect(page.locator(".cm-subject-match")).toHaveCount(0);
|
await expect(page.locator(".cm-subject-match")).toHaveCount(0);
|
||||||
|
|
||||||
await setEditor(page, "editor-regular-expression-pattern", "^ok$");
|
await setEditor(page, "editor-regular-expression-pattern", "^ok$");
|
||||||
|
|||||||
Reference in New Issue
Block a user