@@ -41,6 +42,9 @@
{/each}
+ {#if instance.selectedFileStatuses.length === 0}
+ No file statuses selected; all files will be excluded.
+ {/if}
@@ -50,27 +54,27 @@
class="mb-1 flex w-full items-center gap-1"
onsubmit={(e) => {
e.preventDefault();
- if (newFileNameFilterInput === "") return;
+ if (newFilePathFilterInput === "") return;
// TODO error handling
- instance.addFileNameFilter(newFileNameFilterInput, newFileNameFilterMode);
- newFileNameFilterInput = "";
+ instance.addFilePathFilter(newFilePathFilterInput, newFilePathFilterMode);
+ newFilePathFilterInput = "";
}}
>
{
- newFileNameFilterMode = newFileNameFilterMode === "exclude" ? "include" : "exclude";
+ newFilePathFilterMode = newFilePathFilterMode === "exclude" ? "include" : "exclude";
}}
>
- {#if newFileNameFilterMode === "exclude"}
+ {#if newFilePathFilterMode === "exclude"}
{:else}
@@ -81,7 +85,7 @@
- {#each instance.reverseFileNameFilters as filter, i (i)}
+ {#each instance.reverseFilePathFilters as filter, i (i)}
-
{filter.text}
@@ -98,14 +102,14 @@
title="Delete filter"
class="flex size-6 items-center justify-center rounded-sm btn-ghost-danger"
onclick={() => {
- instance.fileNameFilters.delete(filter);
+ instance.filePathFilters.delete(filter);
}}
>
{/each}
- {#if instance.reverseFileNameFilters.length === 0}
+ {#if instance.reverseFilePathFilters.length === 0}
- No file path filters. Add one using the above form.
{/if}
diff --git a/web/src/lib/components/diff-filtering/index.svelte.ts b/web/src/lib/components/diff-filtering/index.svelte.ts
index 2a4217f..df3654b 100644
--- a/web/src/lib/components/diff-filtering/index.svelte.ts
+++ b/web/src/lib/components/diff-filtering/index.svelte.ts
@@ -2,13 +2,13 @@ import type { FileDetails } from "$lib/diff-viewer.svelte";
import { FILE_STATUSES } from "$lib/github.svelte";
import { SvelteSet } from "svelte/reactivity";
-export type FileNameFilterMode = "include" | "exclude";
-export class FileNameFilter {
+export type FilePathFilterMode = "include" | "exclude";
+export class FilePathFilter {
text: string;
regex: RegExp;
- mode: FileNameFilterMode;
+ mode: FilePathFilterMode;
- constructor(text: string, regex: RegExp, mode: FileNameFilterMode) {
+ constructor(text: string, regex: RegExp, mode: FilePathFilterMode) {
this.text = $state(text);
this.regex = $state.raw(regex);
this.mode = $state(mode);
@@ -24,23 +24,25 @@ function tryCompileRegex(pattern: string): RegExp | undefined {
}
export class DiffFilterDialogState {
- fileNameFilters = new SvelteSet();
- reverseFileNameFilters = $derived([...this.fileNameFilters].toReversed());
+ filePathFilters = new SvelteSet();
+ reverseFilePathFilters = $derived([...this.filePathFilters].toReversed());
+ filePathInclusions = $derived(this.reverseFilePathFilters.filter((f) => f.mode === "include"));
+ filePathExclusions = $derived(this.reverseFilePathFilters.filter((f) => f.mode === "exclude"));
selectedFileStatuses: string[] = $state([...FILE_STATUSES]);
- addFileNameFilter(filterString: string, mode: FileNameFilterMode): { invalidRegex: boolean } {
+ addFilePathFilter(filterString: string, mode: FilePathFilterMode): { invalidRegex: boolean } {
const compiled = tryCompileRegex(filterString);
if (!compiled) {
return { invalidRegex: true };
}
- const newFilter = new FileNameFilter(filterString, compiled, mode);
- this.fileNameFilters.add(newFilter);
+ const newFilter = new FilePathFilter(filterString, compiled, mode);
+ this.filePathFilters.add(newFilter);
return { invalidRegex: false };
}
setDefaults() {
- this.fileNameFilters.clear();
+ this.filePathFilters.clear();
this.selectedFileStatuses = [...FILE_STATUSES];
}
@@ -49,16 +51,13 @@ export class DiffFilterDialogState {
if (!statusAllowed) {
return false;
}
- const pathFilterArray = [...this.fileNameFilters];
- const includes = pathFilterArray.filter((f) => f.mode === "include");
- const excludes = pathFilterArray.filter((f) => f.mode === "exclude");
- for (const exclude of excludes) {
+ for (const exclude of this.filePathExclusions) {
if (exclude.regex.test(file.toFile) || exclude.regex.test(file.fromFile)) {
return false;
}
}
- if (includes.length > 0) {
- for (const include of includes) {
+ if (this.filePathInclusions.length > 0) {
+ for (const include of this.filePathInclusions) {
if (include.regex.test(file.toFile) || include.regex.test(file.fromFile)) {
return true;
}
diff --git a/web/src/lib/components/diff/concise-diff-view.svelte.ts b/web/src/lib/components/diff/concise-diff-view.svelte.ts
index a19de04..a8a636b 100644
--- a/web/src/lib/components/diff/concise-diff-view.svelte.ts
+++ b/web/src/lib/components/diff/concise-diff-view.svelte.ts
@@ -1076,6 +1076,7 @@ async function getTheme(theme: BundledTheme | undefined): Promise;
+ cacheKey: unknown;
syntaxHighlighting: boolean;
syntaxHighlightingTheme: BundledTheme | undefined;
omitPatchHeaderOnlyHunks: boolean;
@@ -1083,6 +1084,7 @@ export class ConciseDiffViewCachedState {
constructor(diffViewerPatch: Promise, props: ConciseDiffViewStateProps) {
this.diffViewerPatch = diffViewerPatch;
+ this.cacheKey = props.cacheKey.current;
this.syntaxHighlighting = props.syntaxHighlighting.current;
this.syntaxHighlightingTheme = props.syntaxHighlightingTheme.current;
this.omitPatchHeaderOnlyHunks = props.omitPatchHeaderOnlyHunks.current;
@@ -1188,8 +1190,8 @@ export class ConciseDiffViewState {
});
onDestroy(() => {
- if (this.props.cache.current !== undefined && this.props.cacheKey.current !== undefined && this.cachedState !== undefined) {
- this.props.cache.current.set(this.props.cacheKey.current, this.cachedState);
+ if (this.props.cache.current !== undefined && this.cachedState !== undefined) {
+ this.props.cache.current.set(this.cachedState.cacheKey as K, this.cachedState);
}
});
}
From 61d4269ed035c2cb3004fe6b4aa5ffbc0aaf92a4 Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Sun, 21 Dec 2025 11:46:34 -0800
Subject: [PATCH 3/4] Improve regex input validation
---
.../diff-filtering/DiffFilterDialog.svelte | 26 ++++++--
.../components/diff-filtering/index.svelte.ts | 59 +++++++++----------
web/src/lib/open-diff-dialog.svelte.ts | 12 ++--
web/src/lib/util.ts | 12 ++++
4 files changed, 65 insertions(+), 44 deletions(-)
diff --git a/web/src/lib/components/diff-filtering/DiffFilterDialog.svelte b/web/src/lib/components/diff-filtering/DiffFilterDialog.svelte
index a583b0c..a9956e5 100644
--- a/web/src/lib/components/diff-filtering/DiffFilterDialog.svelte
+++ b/web/src/lib/components/diff-filtering/DiffFilterDialog.svelte
@@ -1,14 +1,30 @@
@@ -54,9 +70,8 @@
class="mb-1 flex w-full items-center gap-1"
onsubmit={(e) => {
e.preventDefault();
- if (newFilePathFilterInput === "") return;
- // TODO error handling
- instance.addFilePathFilter(newFilePathFilterInput, newFilePathFilterMode);
+ if (!newFilePathFilterInputResult.success) return;
+ instance.addFilePathFilter(newFilePathFilterInputResult, newFilePathFilterMode);
newFilePathFilterInput = "";
}}
>
@@ -65,6 +80,7 @@
placeholder="Enter regular expression here..."
class="grow rounded-md border px-2 py-1 inset-shadow-xs ring-focus focus:outline-none focus-visible:ring-2"
bind:value={newFilePathFilterInput}
+ bind:this={newFilePathFilterElement}
/>
{/each}
{#if instance.reverseFilePathFilters.length === 0}
- No file path filters. Add one using the above form.
+ No file path filters. Add one using the above form.
{/if}
diff --git a/web/src/lib/components/diff-filtering/index.svelte.ts b/web/src/lib/components/diff-filtering/index.svelte.ts
index df3654b..a101ef8 100644
--- a/web/src/lib/components/diff-filtering/index.svelte.ts
+++ b/web/src/lib/components/diff-filtering/index.svelte.ts
@@ -1,5 +1,6 @@
import type { FileDetails } from "$lib/diff-viewer.svelte";
import { FILE_STATUSES } from "$lib/github.svelte";
+import type { TryCompileRegexSuccess } from "$lib/util";
import { SvelteSet } from "svelte/reactivity";
export type FilePathFilterMode = "include" | "exclude";
@@ -15,30 +16,16 @@ export class FilePathFilter {
}
}
-function tryCompileRegex(pattern: string): RegExp | undefined {
- try {
- return new RegExp(pattern);
- } catch {
- return undefined;
- }
-}
-
export class DiffFilterDialogState {
filePathFilters = new SvelteSet();
reverseFilePathFilters = $derived([...this.filePathFilters].toReversed());
- filePathInclusions = $derived(this.reverseFilePathFilters.filter((f) => f.mode === "include"));
- filePathExclusions = $derived(this.reverseFilePathFilters.filter((f) => f.mode === "exclude"));
+ filterFunction = $derived(this.createFilterFunction());
selectedFileStatuses: string[] = $state([...FILE_STATUSES]);
- addFilePathFilter(filterString: string, mode: FilePathFilterMode): { invalidRegex: boolean } {
- const compiled = tryCompileRegex(filterString);
- if (!compiled) {
- return { invalidRegex: true };
- }
- const newFilter = new FilePathFilter(filterString, compiled, mode);
+ addFilePathFilter(regex: TryCompileRegexSuccess, mode: FilePathFilterMode) {
+ const newFilter = new FilePathFilter(regex.input, regex.regex, mode);
this.filePathFilters.add(newFilter);
- return { invalidRegex: false };
}
setDefaults() {
@@ -47,23 +34,33 @@ export class DiffFilterDialogState {
}
filterFile(file: FileDetails): boolean {
- const statusAllowed = this.selectedFileStatuses.includes(file.status);
- if (!statusAllowed) {
- return false;
- }
- for (const exclude of this.filePathExclusions) {
- if (exclude.regex.test(file.toFile) || exclude.regex.test(file.fromFile)) {
+ return this.filterFunction(file);
+ }
+
+ private createFilterFunction() {
+ const filePathInclusions = this.reverseFilePathFilters.filter((f) => f.mode === "include");
+ const filePathExclusions = this.reverseFilePathFilters.filter((f) => f.mode === "exclude");
+ const selectedFileStatuses = [...this.selectedFileStatuses];
+
+ return (file: FileDetails) => {
+ const statusAllowed = selectedFileStatuses.includes(file.status);
+ if (!statusAllowed) {
return false;
}
- }
- if (this.filePathInclusions.length > 0) {
- for (const include of this.filePathInclusions) {
- if (include.regex.test(file.toFile) || include.regex.test(file.fromFile)) {
- return true;
+ for (const exclude of filePathExclusions) {
+ if (exclude.regex.test(file.toFile) || exclude.regex.test(file.fromFile)) {
+ return false;
+ }
+ }
+ if (filePathInclusions.length > 0) {
+ for (const include of filePathInclusions) {
+ if (include.regex.test(file.toFile) || include.regex.test(file.fromFile)) {
+ return true;
+ }
}
+ return false;
}
- return false;
- }
- return true;
+ return true;
+ };
}
}
diff --git a/web/src/lib/open-diff-dialog.svelte.ts b/web/src/lib/open-diff-dialog.svelte.ts
index b2bdf6b..19983bd 100644
--- a/web/src/lib/open-diff-dialog.svelte.ts
+++ b/web/src/lib/open-diff-dialog.svelte.ts
@@ -3,7 +3,7 @@ import { DirectoryEntry, FileEntry, MultimodalFileInputState, type MultimodalFil
import { SvelteSet } from "svelte/reactivity";
import { type FileStatus } from "$lib/github.svelte";
import { makeImageDetails, makeTextDetails, MultiFileDiffViewerState, type LoadPatchesOptions } from "$lib/diff-viewer.svelte";
-import { binaryFileDummyDetails, bytesEqual, isBinaryFile, isImageFile, parseMultiFilePatch } from "$lib/util";
+import { binaryFileDummyDetails, bytesEqual, isBinaryFile, isImageFile, parseMultiFilePatch, tryCompileRegex } from "$lib/util";
import { createTwoFilesPatch } from "diff";
export interface OpenDiffDialogProps {
@@ -47,13 +47,9 @@ export class OpenDiffDialogState {
}
addBlacklistEntry() {
- if (this.dirBlacklistInput === "") {
- return;
- }
- try {
- new RegExp(this.dirBlacklistInput); // Validate regex
- } catch (e) {
- alert("'" + this.dirBlacklistInput + "' is not a valid regex pattern. Error: " + e);
+ const result = tryCompileRegex(this.dirBlacklistInput);
+ if (!result.success) {
+ alert(result.error);
return;
}
this.dirBlacklist.add(this.dirBlacklistInput);
diff --git a/web/src/lib/util.ts b/web/src/lib/util.ts
index 22e337a..d66b00d 100644
--- a/web/src/lib/util.ts
+++ b/web/src/lib/util.ts
@@ -420,3 +420,15 @@ export function animationFramePromise() {
export async function yieldToBrowser() {
await new Promise((resolve) => setTimeout(resolve, 0));
}
+
+export type TryCompileRegexSuccess = { success: true; regex: RegExp; input: string };
+export type TryCompileRegexFailure = { success: false; error: string; input: string };
+export type TryCompileRegexResult = TryCompileRegexSuccess | TryCompileRegexFailure;
+
+export function tryCompileRegex(pattern: string): TryCompileRegexResult {
+ try {
+ return { success: true, regex: new RegExp(pattern), input: pattern };
+ } catch (e) {
+ return { success: false, error: e instanceof Error ? e.message : String(e), input: pattern };
+ }
+}
From aeb52e1630046db2dc2febe479c821e66306a425 Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Sun, 21 Dec 2025 11:50:58 -0800
Subject: [PATCH 4/4] Move empty pattern check
---
.../components/diff-filtering/DiffFilterDialog.svelte | 9 ++-------
web/src/lib/util.ts | 6 +++++-
2 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/web/src/lib/components/diff-filtering/DiffFilterDialog.svelte b/web/src/lib/components/diff-filtering/DiffFilterDialog.svelte
index a9956e5..a15d82c 100644
--- a/web/src/lib/components/diff-filtering/DiffFilterDialog.svelte
+++ b/web/src/lib/components/diff-filtering/DiffFilterDialog.svelte
@@ -1,7 +1,7 @@