diff --git a/extensions/ql-vscode/src/view/common/useMessageFromExtension.ts b/extensions/ql-vscode/src/view/common/useMessageFromExtension.ts new file mode 100644 index 00000000000..2ba13a99858 --- /dev/null +++ b/extensions/ql-vscode/src/view/common/useMessageFromExtension.ts @@ -0,0 +1,27 @@ +import { useEffect } from "react"; + +/** + * Invokes the given callback when a message is received from the extension. + */ +export function useMessageFromExtension( + onEvent: (event: T) => void, + onEventDependencies: unknown[], +): void { + useEffect(() => { + const listener = (evt: MessageEvent) => { + if (evt.origin === window.origin) { + onEvent(evt.data as T); + } else { + // sanitize origin + const origin = evt.origin.replace(/\n|\r/g, ""); + console.error(`Invalid event origin ${origin}`); + } + }; + window.addEventListener("message", listener); + + return () => { + window.removeEventListener("message", listener); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, onEventDependencies); +} diff --git a/extensions/ql-vscode/src/view/compare/Compare.tsx b/extensions/ql-vscode/src/view/compare/Compare.tsx index b64b39b33d8..18412bac5f2 100644 --- a/extensions/ql-vscode/src/view/compare/Compare.tsx +++ b/extensions/ql-vscode/src/view/compare/Compare.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useRef } from "react"; import { styled } from "styled-components"; import type { @@ -16,6 +16,7 @@ import CompareTable from "./CompareTable"; import "../results/resultsView.css"; import { assertNever } from "../../common/helpers-pure"; +import { useMessageFromExtension } from "../common/useMessageFromExtension"; const Header = styled.div` display: flex; @@ -50,115 +51,101 @@ export function Compare(_: Record): React.JSX.Element { comparison?.result && (comparison.result.to.length || comparison.result.from.length); - useEffect(() => { - const listener = (evt: MessageEvent) => { - if (evt.origin === window.origin) { - const msg: ToCompareViewMessage = evt.data; - switch (msg.t) { - case "setComparisonQueryInfo": - setQueryInfo(msg); - break; - case "setComparisons": - setComparison(msg); - break; - case "streamingComparisonSetup": - setComparison(null); - streamingComparisonRef.current = msg; - break; - case "streamingComparisonAddResults": { - const prev = streamingComparisonRef.current; - if (prev === null) { - console.warn( - 'Received "streamingComparisonAddResults" before "streamingComparisonSetup"', - ); - break; - } + useMessageFromExtension((msg) => { + switch (msg.t) { + case "setComparisonQueryInfo": + setQueryInfo(msg); + break; + case "setComparisons": + setComparison(msg); + break; + case "streamingComparisonSetup": + setComparison(null); + streamingComparisonRef.current = msg; + break; + case "streamingComparisonAddResults": { + const prev = streamingComparisonRef.current; + if (prev === null) { + console.warn( + 'Received "streamingComparisonAddResults" before "streamingComparisonSetup"', + ); + break; + } - if (prev.id !== msg.id) { - console.warn( - 'Received "streamingComparisonAddResults" with different id, ignoring', - ); - break; - } + if (prev.id !== msg.id) { + console.warn( + 'Received "streamingComparisonAddResults" with different id, ignoring', + ); + break; + } - let result: QueryCompareResult; - switch (prev.result.kind) { - case "raw": - if (msg.result.kind !== "raw") { - throw new Error( - "Streaming comparison: expected raw results, got interpreted results", - ); - } - - result = { - ...prev.result, - from: [...prev.result.from, ...msg.result.from], - to: [...prev.result.to, ...msg.result.to], - }; - break; - case "interpreted": - if (msg.result.kind !== "interpreted") { - throw new Error( - "Streaming comparison: expected interpreted results, got raw results", - ); - } - - result = { - ...prev.result, - from: [...prev.result.from, ...msg.result.from], - to: [...prev.result.to, ...msg.result.to], - }; - break; - default: - throw new Error("Unexpected comparison result kind"); + let result: QueryCompareResult; + switch (prev.result.kind) { + case "raw": + if (msg.result.kind !== "raw") { + throw new Error( + "Streaming comparison: expected raw results, got interpreted results", + ); } - streamingComparisonRef.current = { - ...prev, - result, + result = { + ...prev.result, + from: [...prev.result.from, ...msg.result.from], + to: [...prev.result.to, ...msg.result.to], }; - break; - } - case "streamingComparisonComplete": - if (streamingComparisonRef.current === null) { - console.warn( - 'Received "streamingComparisonComplete" before "streamingComparisonSetup"', + case "interpreted": + if (msg.result.kind !== "interpreted") { + throw new Error( + "Streaming comparison: expected interpreted results, got raw results", ); - setComparison(null); - break; } - if (streamingComparisonRef.current.id !== msg.id) { - console.warn( - 'Received "streamingComparisonComplete" with different id, ignoring', - ); - break; - } - - setComparison({ - ...streamingComparisonRef.current, - t: "setComparisons", - }); - streamingComparisonRef.current = null; - break; - case "setUserSettings": - setUserSettings(msg.userSettings); + result = { + ...prev.result, + from: [...prev.result.from, ...msg.result.from], + to: [...prev.result.to, ...msg.result.to], + }; break; default: - assertNever(msg); + throw new Error("Unexpected comparison result kind"); } - } else { - // sanitize origin - const origin = evt.origin.replace(/\n|\r/g, ""); - console.error(`Invalid event origin ${origin}`); + + streamingComparisonRef.current = { + ...prev, + result, + }; + + break; } - }; - window.addEventListener("message", listener); + case "streamingComparisonComplete": + if (streamingComparisonRef.current === null) { + console.warn( + 'Received "streamingComparisonComplete" before "streamingComparisonSetup"', + ); + setComparison(null); + break; + } + + if (streamingComparisonRef.current.id !== msg.id) { + console.warn( + 'Received "streamingComparisonComplete" with different id, ignoring', + ); + break; + } - return () => { - window.removeEventListener("message", listener); - }; + setComparison({ + ...streamingComparisonRef.current, + t: "setComparisons", + }); + streamingComparisonRef.current = null; + break; + case "setUserSettings": + setUserSettings(msg.userSettings); + break; + default: + assertNever(msg); + } }, []); if (!queryInfo || !comparison) { diff --git a/extensions/ql-vscode/src/view/data-flow-paths/DataFlowPathsView.tsx b/extensions/ql-vscode/src/view/data-flow-paths/DataFlowPathsView.tsx index 9267b81f053..6fc2b3b4ba7 100644 --- a/extensions/ql-vscode/src/view/data-flow-paths/DataFlowPathsView.tsx +++ b/extensions/ql-vscode/src/view/data-flow-paths/DataFlowPathsView.tsx @@ -1,7 +1,8 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import type { ToDataFlowPathsMessage } from "../../common/interface-types"; import type { DataFlowPaths as DataFlowPathsDomainModel } from "../../variant-analysis/shared/data-flow-paths"; import { DataFlowPaths } from "./DataFlowPaths"; +import { useMessageFromExtension } from "../common/useMessageFromExtension"; export type DataFlowPathsViewProps = { dataFlowPaths?: DataFlowPathsDomainModel; @@ -14,28 +15,12 @@ export function DataFlowPathsView({ DataFlowPathsDomainModel | undefined >(initialDataFlowPaths); - useEffect(() => { - const listener = (evt: MessageEvent) => { - if (evt.origin === window.origin) { - const msg: ToDataFlowPathsMessage = evt.data; - if (msg.t === "setDataFlowPaths") { - setDataFlowPaths(msg.dataFlowPaths); + useMessageFromExtension((msg) => { + setDataFlowPaths(msg.dataFlowPaths); - // Scroll to the top of the page when we're rendering - // new data flow paths. - window.scrollTo(0, 0); - } - } else { - // sanitize origin - const origin = evt.origin.replace(/\n|\r/g, ""); - console.error(`Invalid event origin ${origin}`); - } - }; - window.addEventListener("message", listener); - - return () => { - window.removeEventListener("message", listener); - }; + // Scroll to the top of the page when we're rendering + // new data flow paths. + window.scrollTo(0, 0); }, []); if (!dataFlowPaths) { diff --git a/extensions/ql-vscode/src/view/method-modeling/MethodModelingView.tsx b/extensions/ql-vscode/src/view/method-modeling/MethodModelingView.tsx index a37c209b913..53ec8d1101a 100644 --- a/extensions/ql-vscode/src/view/method-modeling/MethodModelingView.tsx +++ b/extensions/ql-vscode/src/view/method-modeling/MethodModelingView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { MethodModeling } from "./MethodModeling"; import { getModelingStatus } from "../../model-editor/shared/modeling-status"; import type { Method } from "../../model-editor/method"; @@ -12,6 +12,7 @@ import { NoMethodSelected } from "./NoMethodSelected"; import type { MethodModelingPanelViewState } from "../../model-editor/shared/view-state"; import { MethodAlreadyModeled } from "./MethodAlreadyModeled"; import { defaultModelConfig } from "../../model-editor/languages"; +import { useMessageFromExtension } from "../common/useMessageFromExtension"; type Props = { initialViewState?: MethodModelingPanelViewState; @@ -36,47 +37,33 @@ export function MethodModelingView({ [modeledMethods, isMethodModified], ); - useEffect(() => { - const listener = (evt: MessageEvent) => { - if (evt.origin === window.origin) { - const msg: ToMethodModelingMessage = evt.data; - switch (msg.t) { - case "setMethodModelingPanelViewState": - setViewState(msg.viewState); - break; - case "setInModelingMode": - setInModelingMode(msg.inModelingMode); - break; - case "setMultipleModeledMethods": - setModeledMethods(msg.modeledMethods); - break; - case "setMethodModified": - setIsMethodModified(msg.isModified); - break; - case "setNoMethodSelected": - setMethod(undefined); - setModeledMethods([]); - setIsMethodModified(false); - break; - case "setSelectedMethod": - setMethod(msg.method); - setModeledMethods(msg.modeledMethods); - setIsMethodModified(msg.isModified); - break; - default: - assertNever(msg); - } - } else { - // sanitize origin - const origin = evt.origin.replace(/\n|\r/g, ""); - console.error(`Invalid event origin ${origin}`); - } - }; - window.addEventListener("message", listener); - - return () => { - window.removeEventListener("message", listener); - }; + useMessageFromExtension((msg) => { + switch (msg.t) { + case "setMethodModelingPanelViewState": + setViewState(msg.viewState); + break; + case "setInModelingMode": + setInModelingMode(msg.inModelingMode); + break; + case "setMultipleModeledMethods": + setModeledMethods(msg.modeledMethods); + break; + case "setMethodModified": + setIsMethodModified(msg.isModified); + break; + case "setNoMethodSelected": + setMethod(undefined); + setModeledMethods([]); + setIsMethodModified(false); + break; + case "setSelectedMethod": + setMethod(msg.method); + setModeledMethods(msg.modeledMethods); + setIsMethodModified(msg.isModified); + break; + default: + assertNever(msg); + } }, []); if (!inModelingMode || !viewState?.language) { diff --git a/extensions/ql-vscode/src/view/model-alerts/ModelAlerts.tsx b/extensions/ql-vscode/src/view/model-alerts/ModelAlerts.tsx index 6f065286336..70169f0a9f4 100644 --- a/extensions/ql-vscode/src/view/model-alerts/ModelAlerts.tsx +++ b/extensions/ql-vscode/src/view/model-alerts/ModelAlerts.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { styled } from "styled-components"; import { ModelAlertsHeader } from "./ModelAlertsHeader"; import type { ModelAlertsViewState } from "../../model-editor/shared/view-state"; @@ -18,6 +18,7 @@ import { } from "../../model-editor/shared/model-alerts-filter-sort"; import type { ModelAlertsFilterSortState } from "../../model-editor/shared/model-alerts-filter-sort"; import type { ModeledMethod } from "../../model-editor/modeled-method"; +import { useMessageFromExtension } from "../common/useMessageFromExtension"; type Props = { initialViewState?: ModelAlertsViewState; @@ -67,47 +68,33 @@ export function ModelAlerts({ null, ); - useEffect(() => { - const listener = (evt: MessageEvent) => { - if (evt.origin === window.origin) { - const msg: ToModelAlertsMessage = evt.data; - switch (msg.t) { - case "setModelAlertsViewState": { - setViewState(msg.viewState); - break; - } - case "setVariantAnalysis": { - setVariantAnalysis(msg.variantAnalysis); - break; - } - case "setRepoResults": { - setRepoResults((oldRepoResults) => { - const newRepoIds = msg.repoResults.map((r) => r.repositoryId); - return [ - ...oldRepoResults.filter( - (v) => !newRepoIds.includes(v.repositoryId), - ), - ...msg.repoResults, - ]; - }); - break; - } - case "revealModel": { - setRevealedModel(msg.modeledMethod); - break; - } - } - } else { - // sanitize origin - const origin = evt.origin.replace(/\n|\r/g, ""); - console.error(`Invalid event origin ${origin}`); + useMessageFromExtension((msg) => { + switch (msg.t) { + case "setModelAlertsViewState": { + setViewState(msg.viewState); + break; } - }; - window.addEventListener("message", listener); - - return () => { - window.removeEventListener("message", listener); - }; + case "setVariantAnalysis": { + setVariantAnalysis(msg.variantAnalysis); + break; + } + case "setRepoResults": { + setRepoResults((oldRepoResults) => { + const newRepoIds = msg.repoResults.map((r) => r.repositoryId); + return [ + ...oldRepoResults.filter( + (v) => !newRepoIds.includes(v.repositoryId), + ), + ...msg.repoResults, + ]; + }); + break; + } + case "revealModel": { + setRevealedModel(msg.modeledMethod); + break; + } + } }, []); const modelAlerts = useMemo(() => { diff --git a/extensions/ql-vscode/src/view/model-editor/ModelEditor.tsx b/extensions/ql-vscode/src/view/model-editor/ModelEditor.tsx index 3eef361da16..02c235d5029 100644 --- a/extensions/ql-vscode/src/view/model-editor/ModelEditor.tsx +++ b/extensions/ql-vscode/src/view/model-editor/ModelEditor.tsx @@ -21,6 +21,7 @@ import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "../../model-editor/shared/hi import type { AccessPathSuggestionOptions } from "../../model-editor/suggestions"; import type { ModelEvaluationRunState } from "../../model-editor/shared/model-evaluation-run-state"; import { ModelEvaluation } from "./ModelEvaluation"; +import { useMessageFromExtension } from "../common/useMessageFromExtension"; const LoadingContainer = styled.div` text-align: center; @@ -129,47 +130,33 @@ export function ModelEditor({ AccessPathSuggestionOptions | undefined >(undefined); - useEffect(() => { - const listener = (evt: MessageEvent) => { - if (evt.origin === window.origin) { - const msg: ToModelEditorMessage = evt.data; - switch (msg.t) { - case "setModelEditorViewState": - setViewState(msg.viewState); - break; - case "setMethods": - setMethods(msg.methods); - break; - case "setModeledAndModifiedMethods": - setModeledMethods(msg.methods); - setModifiedSignatures(new Set(msg.modifiedMethodSignatures)); - break; - case "setModifiedMethods": - setModifiedSignatures(new Set(msg.methodSignatures)); - break; - case "revealMethod": - setRevealedMethodSignature(msg.methodSignature); - break; - case "setAccessPathSuggestions": - setAccessPathSuggestions(msg.accessPathSuggestions); - break; - case "setModelEvaluationRun": - setEvaluationRun(msg.run); - break; - default: - assertNever(msg); - } - } else { - // sanitize origin - const origin = evt.origin.replace(/\n|\r/g, ""); - console.error(`Invalid event origin ${origin}`); - } - }; - window.addEventListener("message", listener); - - return () => { - window.removeEventListener("message", listener); - }; + useMessageFromExtension((msg) => { + switch (msg.t) { + case "setModelEditorViewState": + setViewState(msg.viewState); + break; + case "setMethods": + setMethods(msg.methods); + break; + case "setModeledAndModifiedMethods": + setModeledMethods(msg.methods); + setModifiedSignatures(new Set(msg.modifiedMethodSignatures)); + break; + case "setModifiedMethods": + setModifiedSignatures(new Set(msg.methodSignatures)); + break; + case "revealMethod": + setRevealedMethodSignature(msg.methodSignature); + break; + case "setAccessPathSuggestions": + setAccessPathSuggestions(msg.accessPathSuggestions); + break; + case "setModelEvaluationRun": + setEvaluationRun(msg.run); + break; + default: + assertNever(msg); + } }, []); useEffect(() => { diff --git a/extensions/ql-vscode/src/view/results/ResultsApp.tsx b/extensions/ql-vscode/src/view/results/ResultsApp.tsx index 63f5e060dec..f0ff4e2cd3c 100644 --- a/extensions/ql-vscode/src/view/results/ResultsApp.tsx +++ b/extensions/ql-vscode/src/view/results/ResultsApp.tsx @@ -16,11 +16,12 @@ import { DEFAULT_USER_SETTINGS, GRAPH_TABLE_NAME, } from "../../common/interface-types"; +import { useMessageFromExtension } from "../common/useMessageFromExtension"; import { ResultTables } from "./ResultTables"; import { onNavigation } from "./navigation"; import "./resultsView.css"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; /** * ResultsApp.tsx @@ -113,8 +114,8 @@ export function ResultsApp() { [], ); - const handleMessage = useCallback( - (msg: IntoResultsViewMsg): void => { + useMessageFromExtension( + (msg) => { switch (msg.t) { case "setUserSettings": setUserSettings(msg.userSettings); @@ -189,26 +190,6 @@ export function ResultsApp() { [updateStateWithNewResultsInfo], ); - const vscodeMessageHandler = useCallback( - (evt: MessageEvent) => { - // sanitize origin - const origin = evt.origin.replace(/\n|\r/g, ""); - if (evt.origin === window.origin) { - handleMessage(evt.data as IntoResultsViewMsg); - } else { - console.error(`Invalid event origin ${origin}`); - } - }, - [handleMessage], - ); - - useEffect(() => { - window.addEventListener("message", vscodeMessageHandler); - return () => { - window.removeEventListener("message", vscodeMessageHandler); - }; - }, [vscodeMessageHandler]); - const { displayedResults, nextResultsInfo, isExpectingResultsUpdate } = state; if ( displayedResults.results !== null && diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx index 3a72c3548cd..5b68d114d6f 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import type { VariantAnalysis as VariantAnalysisDomainModel, @@ -13,6 +13,7 @@ import type { ToVariantAnalysisMessage } from "../../common/interface-types"; import { vscode } from "../vscode-api"; import { defaultFilterSortState } from "../../variant-analysis/shared/variant-analysis-filter-sort"; import { sendTelemetry, useTelemetryOnChange } from "../common/telemetry"; +import { useMessageFromExtension } from "../common/useMessageFromExtension"; export type VariantAnalysisProps = { variantAnalysis?: VariantAnalysisDomainModel; @@ -77,49 +78,31 @@ export function VariantAnalysis({ debounceTimeoutMillis: 1000, }); - useEffect(() => { - const listener = (evt: MessageEvent) => { - if (evt.origin === window.origin) { - const msg: ToVariantAnalysisMessage = evt.data; - if (msg.t === "setVariantAnalysis") { - setVariantAnalysis(msg.variantAnalysis); - vscode.setState({ - variantAnalysisId: msg.variantAnalysis.id, - }); - } else if (msg.t === "setFilterSortState") { - setFilterSortState(msg.filterSortState); - } else if (msg.t === "setRepoResults") { - setRepoResults((oldRepoResults) => { - const newRepoIds = msg.repoResults.map((r) => r.repositoryId); - return [ - ...oldRepoResults.filter( - (v) => !newRepoIds.includes(v.repositoryId), - ), - ...msg.repoResults, - ]; - }); - } else if (msg.t === "setRepoStates") { - setRepoStates((oldRepoStates) => { - const newRepoIds = msg.repoStates.map((r) => r.repositoryId); - return [ - ...oldRepoStates.filter( - (v) => !newRepoIds.includes(v.repositoryId), - ), - ...msg.repoStates, - ]; - }); - } - } else { - // sanitize origin - const origin = evt.origin.replace(/\n|\r/g, ""); - console.error(`Invalid event origin ${origin}`); - } - }; - window.addEventListener("message", listener); - - return () => { - window.removeEventListener("message", listener); - }; + useMessageFromExtension((msg) => { + if (msg.t === "setVariantAnalysis") { + setVariantAnalysis(msg.variantAnalysis); + vscode.setState({ + variantAnalysisId: msg.variantAnalysis.id, + }); + } else if (msg.t === "setFilterSortState") { + setFilterSortState(msg.filterSortState); + } else if (msg.t === "setRepoResults") { + setRepoResults((oldRepoResults) => { + const newRepoIds = msg.repoResults.map((r) => r.repositoryId); + return [ + ...oldRepoResults.filter((v) => !newRepoIds.includes(v.repositoryId)), + ...msg.repoResults, + ]; + }); + } else if (msg.t === "setRepoStates") { + setRepoStates((oldRepoStates) => { + const newRepoIds = msg.repoStates.map((r) => r.repositoryId); + return [ + ...oldRepoStates.filter((v) => !newRepoIds.includes(v.repositoryId)), + ...msg.repoStates, + ]; + }); + } }, []); const copyRepositoryList = useCallback(() => {