refactor: simplify file reference interactions by removing unused validation logic
This commit is contained in:
@@ -1022,23 +1022,15 @@ const getContextDirectory = (effectiveDirectory: string, resolvedPath: string):
|
|||||||
const useFileReferenceInteractions = ({
|
const useFileReferenceInteractions = ({
|
||||||
containerRef,
|
containerRef,
|
||||||
effectiveDirectory,
|
effectiveDirectory,
|
||||||
statFile,
|
|
||||||
editor,
|
editor,
|
||||||
preferRuntimeEditor,
|
preferRuntimeEditor,
|
||||||
deferValidationUntilIdle = false,
|
|
||||||
}: {
|
}: {
|
||||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
effectiveDirectory: string;
|
effectiveDirectory: string;
|
||||||
statFile?: (path: string) => Promise<{ path: string; isFile: boolean; size: number }>;
|
|
||||||
editor?: EditorAPI;
|
editor?: EditorAPI;
|
||||||
preferRuntimeEditor?: boolean;
|
preferRuntimeEditor?: boolean;
|
||||||
deferValidationUntilIdle?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const validationCacheRef = React.useRef<Map<string, boolean>>(new Map());
|
|
||||||
const inFlightValidationsRef = React.useRef<Map<string, Promise<boolean>>>(new Map());
|
|
||||||
const annotationPassRef = React.useRef(0);
|
|
||||||
const annotationDebounceRef = React.useRef<number | null>(null);
|
const annotationDebounceRef = React.useRef<number | null>(null);
|
||||||
const isValidationSweepRunningRef = React.useRef(false);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
@@ -1046,158 +1038,47 @@ const useFileReferenceInteractions = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let disposed = false;
|
const annotateFileLinks = () => {
|
||||||
|
|
||||||
const isPathResolvable = async (resolvedPath: string): Promise<boolean> => {
|
|
||||||
const cache = validationCacheRef.current;
|
|
||||||
if (cache.has(resolvedPath)) {
|
|
||||||
return cache.get(resolvedPath) === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inFlight = inFlightValidationsRef.current.get(resolvedPath);
|
|
||||||
if (inFlight) {
|
|
||||||
return inFlight;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkPromise = (async () => {
|
|
||||||
try {
|
|
||||||
if (!statFile) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const stat = await statFile(resolvedPath);
|
|
||||||
if (!stat.isFile) {
|
|
||||||
cache.set(resolvedPath, false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
cache.set(resolvedPath, true);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
cache.set(resolvedPath, false);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
inFlightValidationsRef.current.delete(resolvedPath);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
inFlightValidationsRef.current.set(resolvedPath, checkPromise);
|
|
||||||
return checkPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearCandidateLinkAttrs = (candidate: HTMLElement) => {
|
|
||||||
candidate.removeAttribute('data-openchamber-file-link');
|
|
||||||
candidate.removeAttribute('data-openchamber-file-ref');
|
|
||||||
candidate.removeAttribute('data-openchamber-file-path');
|
|
||||||
if (candidate.getAttribute('title') === 'Open file') {
|
|
||||||
candidate.removeAttribute('title');
|
|
||||||
}
|
|
||||||
if (candidate.tagName.toLowerCase() !== 'a') {
|
|
||||||
candidate.removeAttribute('role');
|
|
||||||
candidate.removeAttribute('tabindex');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyCandidateLinkAttrs = (candidate: HTMLElement, rawCandidate: string, resolvedPath: string) => {
|
|
||||||
candidate.setAttribute('data-openchamber-file-link', 'true');
|
|
||||||
candidate.setAttribute('data-openchamber-file-ref', rawCandidate);
|
|
||||||
candidate.setAttribute('data-openchamber-file-path', resolvedPath);
|
|
||||||
candidate.setAttribute('title', 'Open file');
|
|
||||||
if (candidate.tagName.toLowerCase() !== 'a') {
|
|
||||||
candidate.setAttribute('role', 'button');
|
|
||||||
candidate.setAttribute('tabindex', '0');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const runValidationSweep = async (paths: string[], expectedPassID: number) => {
|
|
||||||
if (deferValidationUntilIdle) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isValidationSweepRunningRef.current || paths.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isValidationSweepRunningRef.current = true;
|
|
||||||
const maxConcurrent = 3;
|
|
||||||
let cursor = 0;
|
|
||||||
|
|
||||||
const worker = async () => {
|
|
||||||
while (!disposed && cursor < paths.length) {
|
|
||||||
const index = cursor;
|
|
||||||
cursor += 1;
|
|
||||||
const pathToCheck = paths[index];
|
|
||||||
if (!pathToCheck) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await isPathResolvable(pathToCheck);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(Array.from({ length: Math.min(maxConcurrent, paths.length) }, () => worker()));
|
|
||||||
} finally {
|
|
||||||
isValidationSweepRunningRef.current = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!disposed && annotationPassRef.current === expectedPassID) {
|
|
||||||
void annotateFileLinks();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const annotateFileLinks = async () => {
|
|
||||||
const passID = annotationPassRef.current + 1;
|
|
||||||
annotationPassRef.current = passID;
|
|
||||||
const candidates = container.querySelectorAll<HTMLElement>('[data-streamdown="inline-code"], a');
|
const candidates = container.querySelectorAll<HTMLElement>('[data-streamdown="inline-code"], a');
|
||||||
const unresolvedPaths = new Set<string>();
|
|
||||||
|
|
||||||
for (const candidate of Array.from(candidates)) {
|
for (const candidate of Array.from(candidates)) {
|
||||||
const rawCandidate = extractPathCandidateFromElement(candidate);
|
const rawCandidate = extractPathCandidateFromElement(candidate);
|
||||||
const resolved = getResolvedReference(rawCandidate, effectiveDirectory);
|
const resolved = getResolvedReference(rawCandidate, effectiveDirectory);
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
clearCandidateLinkAttrs(candidate);
|
candidate.removeAttribute('data-openchamber-file-link');
|
||||||
|
candidate.removeAttribute('data-openchamber-file-ref');
|
||||||
|
candidate.removeAttribute('data-openchamber-file-path');
|
||||||
|
if (candidate.getAttribute('title') === 'Open file') {
|
||||||
|
candidate.removeAttribute('title');
|
||||||
|
}
|
||||||
|
if (candidate.tagName.toLowerCase() !== 'a') {
|
||||||
|
candidate.removeAttribute('role');
|
||||||
|
candidate.removeAttribute('tabindex');
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (annotationPassRef.current !== passID) {
|
candidate.setAttribute('data-openchamber-file-link', 'true');
|
||||||
return;
|
candidate.setAttribute('data-openchamber-file-ref', rawCandidate);
|
||||||
|
candidate.setAttribute('data-openchamber-file-path', resolved.resolvedPath);
|
||||||
|
candidate.setAttribute('title', 'Open file');
|
||||||
|
if (candidate.tagName.toLowerCase() !== 'a') {
|
||||||
|
candidate.setAttribute('role', 'button');
|
||||||
|
candidate.setAttribute('tabindex', '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedResult = validationCacheRef.current.get(resolved.resolvedPath);
|
|
||||||
if (cachedResult === true) {
|
|
||||||
applyCandidateLinkAttrs(candidate, rawCandidate, resolved.resolvedPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCandidateLinkAttrs(candidate);
|
|
||||||
if (cachedResult !== false) {
|
|
||||||
unresolvedPaths.add(resolved.resolvedPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unresolvedPaths.size > 0) {
|
|
||||||
void runValidationSweep(Array.from(unresolvedPaths), passID);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openFileReference = async (sourceElement: HTMLElement): Promise<boolean> => {
|
const openFileReference = (sourceElement: HTMLElement) => {
|
||||||
const raw = sourceElement.getAttribute('data-openchamber-file-ref') || extractPathCandidateFromElement(sourceElement);
|
const raw = sourceElement.getAttribute('data-openchamber-file-ref') || extractPathCandidateFromElement(sourceElement);
|
||||||
const resolved = getResolvedReference(raw, effectiveDirectory);
|
const resolved = getResolvedReference(raw, effectiveDirectory);
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
return false;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const isResolvable = await isPathResolvable(resolved.resolvedPath);
|
|
||||||
if (!isResolvable) {
|
|
||||||
sourceElement.removeAttribute('data-openchamber-file-link');
|
|
||||||
sourceElement.removeAttribute('data-openchamber-file-ref');
|
|
||||||
sourceElement.removeAttribute('data-openchamber-file-path');
|
|
||||||
if (sourceElement.getAttribute('title') === 'Open file') {
|
|
||||||
sourceElement.removeAttribute('title');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextDirectory = getContextDirectory(effectiveDirectory, resolved.resolvedPath);
|
const contextDirectory = getContextDirectory(effectiveDirectory, resolved.resolvedPath);
|
||||||
if (preferRuntimeEditor && editor) {
|
if (preferRuntimeEditor && editor) {
|
||||||
await editor.openFile(
|
void editor.openFile(
|
||||||
resolved.resolvedPath,
|
resolved.resolvedPath,
|
||||||
Number.isFinite(resolved.line ?? Number.NaN)
|
Number.isFinite(resolved.line ?? Number.NaN)
|
||||||
? Math.max(1, Math.trunc(resolved.line as number))
|
? Math.max(1, Math.trunc(resolved.line as number))
|
||||||
@@ -1206,7 +1087,7 @@ const useFileReferenceInteractions = ({
|
|||||||
? Math.max(1, Math.trunc(resolved.column as number))
|
? Math.max(1, Math.trunc(resolved.column as number))
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiStore = useUIStore.getState();
|
const uiStore = useUIStore.getState();
|
||||||
@@ -1222,7 +1103,6 @@ const useFileReferenceInteractions = ({
|
|||||||
} else {
|
} else {
|
||||||
uiStore.openContextFile(contextDirectory, resolved.resolvedPath);
|
uiStore.openContextFile(contextDirectory, resolved.resolvedPath);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (event: MouseEvent) => {
|
const handleClick = (event: MouseEvent) => {
|
||||||
@@ -1239,7 +1119,7 @@ const useFileReferenceInteractions = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
void openFileReference(fileRefElement);
|
openFileReference(fileRefElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
@@ -1255,36 +1135,34 @@ const useFileReferenceInteractions = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
void openFileReference(target);
|
openFileReference(target);
|
||||||
};
|
};
|
||||||
|
|
||||||
void annotateFileLinks();
|
annotateFileLinks();
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
if (annotationDebounceRef.current !== null && typeof window !== 'undefined') {
|
if (annotationDebounceRef.current !== null && typeof window !== 'undefined') {
|
||||||
window.clearTimeout(annotationDebounceRef.current);
|
window.clearTimeout(annotationDebounceRef.current);
|
||||||
}
|
}
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
void annotateFileLinks();
|
annotateFileLinks();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
annotationDebounceRef.current = window.setTimeout(() => {
|
annotationDebounceRef.current = window.setTimeout(() => {
|
||||||
annotationDebounceRef.current = null;
|
annotationDebounceRef.current = null;
|
||||||
void annotateFileLinks();
|
annotateFileLinks();
|
||||||
}, deferValidationUntilIdle ? 280 : 120);
|
}, 120);
|
||||||
});
|
});
|
||||||
observer.observe(container, {
|
observer.observe(container, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
...(deferValidationUntilIdle ? {} : { characterData: true }),
|
characterData: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
container.addEventListener('click', handleClick);
|
container.addEventListener('click', handleClick);
|
||||||
container.addEventListener('keydown', handleKeyDown);
|
container.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
|
||||||
annotationPassRef.current += 1;
|
|
||||||
if (annotationDebounceRef.current !== null && typeof window !== 'undefined') {
|
if (annotationDebounceRef.current !== null && typeof window !== 'undefined') {
|
||||||
window.clearTimeout(annotationDebounceRef.current);
|
window.clearTimeout(annotationDebounceRef.current);
|
||||||
}
|
}
|
||||||
@@ -1293,7 +1171,7 @@ const useFileReferenceInteractions = ({
|
|||||||
container.removeEventListener('click', handleClick);
|
container.removeEventListener('click', handleClick);
|
||||||
container.removeEventListener('keydown', handleKeyDown);
|
container.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [containerRef, deferValidationUntilIdle, editor, effectiveDirectory, preferRuntimeEditor, statFile]);
|
}, [containerRef, editor, effectiveDirectory, preferRuntimeEditor]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useMermaidInlineInteractions = ({
|
const useMermaidInlineInteractions = ({
|
||||||
@@ -1409,10 +1287,8 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||||||
useFileReferenceInteractions({
|
useFileReferenceInteractions({
|
||||||
containerRef: streamdownContainerRef,
|
containerRef: streamdownContainerRef,
|
||||||
effectiveDirectory,
|
effectiveDirectory,
|
||||||
statFile: files.statFile,
|
|
||||||
editor,
|
editor,
|
||||||
preferRuntimeEditor: runtime.isVSCode,
|
preferRuntimeEditor: runtime.isVSCode,
|
||||||
deferValidationUntilIdle: isStreaming,
|
|
||||||
});
|
});
|
||||||
useExternalLinkInteractions({ containerRef: streamdownContainerRef });
|
useExternalLinkInteractions({ containerRef: streamdownContainerRef });
|
||||||
|
|
||||||
@@ -1495,7 +1371,6 @@ export const SimpleMarkdownRenderer: React.FC<{
|
|||||||
useFileReferenceInteractions({
|
useFileReferenceInteractions({
|
||||||
containerRef: streamdownContainerRef,
|
containerRef: streamdownContainerRef,
|
||||||
effectiveDirectory,
|
effectiveDirectory,
|
||||||
statFile: files.statFile,
|
|
||||||
editor,
|
editor,
|
||||||
preferRuntimeEditor: runtime.isVSCode,
|
preferRuntimeEditor: runtime.isVSCode,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user