Reveal method in editor view

This will reveal a method for which "Review in editor" is clicked in the
model editor view: it will expand the group (library/package) in which
the method is located, scroll to the method, and highlight the method.

If the user clicks anywhere on the page, the highlight will be removed,
but the group will remain expanded.
This commit is contained in:
Koen Vlaswinkel
2023-10-02 15:32:22 +02:00
parent 79ea901a52
commit 487a753ede
11 changed files with 177 additions and 83 deletions

View File

@@ -575,12 +575,18 @@ interface SetModeledMethodMessage {
method: ModeledMethod;
}
interface RevealMethodMessage {
t: "revealMethod";
method: Method;
}
export type ToModelEditorMessage =
| SetExtensionPackStateMessage
| SetMethodsMessage
| SetModeledMethodsMessage
| SetModifiedMethodsMessage
| SetInProgressMethodsMessage;
| SetInProgressMethodsMessage
| RevealMethodMessage;
export type FromModelEditorMessage =
| ViewLoadedMsg

View File

@@ -347,7 +347,12 @@ export class ModelEditorView extends AbstractWebview<
}
public async revealMethod(method: Method): Promise<void> {
void this.app.logger.log(`Revealing method ${JSON.stringify(method)}`);
this.panel?.reveal();
await this.postMessage({
t: "revealMethod",
method,
});
}
private async setViewState(): Promise<void> {

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { styled } from "styled-components";
import { Method } from "../../model-editor/method";
import { ModeledMethod } from "../../model-editor/modeled-method";
@@ -76,6 +76,7 @@ export type LibraryRowProps = {
inProgressMethods: InProgressMethods;
viewState: ModelEditorViewState;
hideModeledMethods: boolean;
revealedMethodSignature: string | null;
onChange: (modeledMethod: ModeledMethod) => void;
onSaveModelClick: (
methods: Method[],
@@ -100,6 +101,7 @@ export const LibraryRow = ({
inProgressMethods,
viewState,
hideModeledMethods,
revealedMethodSignature,
onChange,
onSaveModelClick,
onGenerateFromLlmClick,
@@ -117,6 +119,14 @@ export const LibraryRow = ({
setExpanded((oldIsExpanded) => !oldIsExpanded);
}, []);
useEffect(() => {
// If any of the methods in this group is the one that should be revealed, we should expand
// this group so the method can highlight itself.
if (methods.some((m) => m.signature === revealedMethodSignature)) {
setExpanded(true);
}
}, [methods, revealedMethodSignature]);
const handleModelWithAI = useCallback(
async (e: React.MouseEvent) => {
onGenerateFromLlmClick(title, methods, modeledMethods);
@@ -227,6 +237,7 @@ export const LibraryRow = ({
inProgressMethods={inProgressMethods}
mode={viewState.mode}
hideModeledMethods={hideModeledMethods}
revealedMethodSignature={revealedMethodSignature}
onChange={onChange}
/>
<SectionDivider />

View File

@@ -5,7 +5,7 @@ import {
VSCodeProgressRing,
} from "@vscode/webview-ui-toolkit/react";
import * as React from "react";
import { useCallback } from "react";
import { forwardRef, useCallback, useEffect, useRef } from "react";
import { styled } from "styled-components";
import { vscode } from "../vscode-api";
@@ -47,6 +47,11 @@ const ProgressRing = styled(VSCodeProgressRing)`
margin-left: auto;
`;
const DataGridRow = styled(VSCodeDataGridRow)<{ focused?: boolean }>`
outline: ${(props) =>
props.focused ? "1px solid var(--vscode-focusBorder)" : "none"};
`;
export type MethodRowProps = {
method: Method;
methodCanBeModeled: boolean;
@@ -54,97 +59,126 @@ export type MethodRowProps = {
methodIsUnsaved: boolean;
modelingInProgress: boolean;
mode: Mode;
revealedMethodSignature: string | null;
onChange: (modeledMethod: ModeledMethod) => void;
};
export const MethodRow = (props: MethodRowProps) => {
const { methodCanBeModeled } = props;
const { method, methodCanBeModeled, revealedMethodSignature } = props;
const ref = useRef<HTMLElement>();
useEffect(() => {
if (method.signature === revealedMethodSignature) {
ref.current?.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
}, [method, revealedMethodSignature]);
if (methodCanBeModeled) {
return <ModelableMethodRow {...props} />;
return <ModelableMethodRow {...props} ref={ref} />;
} else {
return <UnmodelableMethodRow {...props} />;
return <UnmodelableMethodRow {...props} ref={ref} />;
}
};
function ModelableMethodRow(props: MethodRowProps) {
const { method, modeledMethod, methodIsUnsaved, mode, onChange } = props;
const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
(props, ref) => {
const {
method,
modeledMethod,
methodIsUnsaved,
mode,
revealedMethodSignature,
onChange,
} = props;
const jumpToUsage = useCallback(
() => sendJumpToUsageMessage(method),
[method],
);
const jumpToUsage = useCallback(
() => sendJumpToUsageMessage(method),
[method],
);
const modelingStatus = getModelingStatus(modeledMethod, methodIsUnsaved);
const modelingStatus = getModelingStatus(modeledMethod, methodIsUnsaved);
return (
<VSCodeDataGridRow data-testid="modelable-method-row">
<ApiOrMethodCell gridColumn={1}>
<ModelingStatusIndicator status={modelingStatus} />
<MethodClassifications method={method} />
<MethodName {...props.method} />
{mode === Mode.Application && (
<UsagesButton onClick={jumpToUsage}>
{method.usages.length}
</UsagesButton>
return (
<DataGridRow
data-testid="modelable-method-row"
ref={ref}
focused={revealedMethodSignature === method.signature}
>
<ApiOrMethodCell gridColumn={1}>
<ModelingStatusIndicator status={modelingStatus} />
<MethodClassifications method={method} />
<MethodName {...props.method} />
{mode === Mode.Application && (
<UsagesButton onClick={jumpToUsage}>
{method.usages.length}
</UsagesButton>
)}
<ViewLink onClick={jumpToUsage}>View</ViewLink>
{props.modelingInProgress && <ProgressRing />}
</ApiOrMethodCell>
{props.modelingInProgress && (
<>
<VSCodeDataGridCell gridColumn={2}>
<InProgressDropdown />
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={3}>
<InProgressDropdown />
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={4}>
<InProgressDropdown />
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={5}>
<InProgressDropdown />
</VSCodeDataGridCell>
</>
)}
<ViewLink onClick={jumpToUsage}>View</ViewLink>
{props.modelingInProgress && <ProgressRing />}
</ApiOrMethodCell>
{props.modelingInProgress && (
<>
<VSCodeDataGridCell gridColumn={2}>
<InProgressDropdown />
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={3}>
<InProgressDropdown />
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={4}>
<InProgressDropdown />
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={5}>
<InProgressDropdown />
</VSCodeDataGridCell>
</>
)}
{!props.modelingInProgress && (
<>
<VSCodeDataGridCell gridColumn={2}>
<ModelTypeDropdown
method={method}
modeledMethod={modeledMethod}
onChange={onChange}
/>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={3}>
<ModelInputDropdown
method={method}
modeledMethod={modeledMethod}
onChange={onChange}
/>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={4}>
<ModelOutputDropdown
method={method}
modeledMethod={modeledMethod}
onChange={onChange}
/>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={5}>
<ModelKindDropdown
method={method}
modeledMethod={modeledMethod}
onChange={onChange}
/>
</VSCodeDataGridCell>
</>
)}
</VSCodeDataGridRow>
);
}
{!props.modelingInProgress && (
<>
<VSCodeDataGridCell gridColumn={2}>
<ModelTypeDropdown
method={method}
modeledMethod={modeledMethod}
onChange={onChange}
/>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={3}>
<ModelInputDropdown
method={method}
modeledMethod={modeledMethod}
onChange={onChange}
/>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={4}>
<ModelOutputDropdown
method={method}
modeledMethod={modeledMethod}
onChange={onChange}
/>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={5}>
<ModelKindDropdown
method={method}
modeledMethod={modeledMethod}
onChange={onChange}
/>
</VSCodeDataGridCell>
</>
)}
</DataGridRow>
);
},
);
ModelableMethodRow.displayName = "ModelableMethodRow";
function UnmodelableMethodRow(props: MethodRowProps) {
const { method, mode } = props;
const UnmodelableMethodRow = forwardRef<
HTMLElement | undefined,
MethodRowProps
>((props, ref) => {
const { method, mode, revealedMethodSignature } = props;
const jumpToUsage = useCallback(
() => sendJumpToUsageMessage(method),
@@ -152,7 +186,11 @@ function UnmodelableMethodRow(props: MethodRowProps) {
);
return (
<VSCodeDataGridRow data-testid="unmodelable-method-row">
<DataGridRow
data-testid="unmodelable-method-row"
ref={ref}
focused={revealedMethodSignature === method.signature}
>
<ApiOrMethodCell gridColumn={1}>
<ModelingStatusIndicator status="saved" />
<MethodName {...props.method} />
@@ -167,9 +205,10 @@ function UnmodelableMethodRow(props: MethodRowProps) {
<VSCodeDataGridCell gridColumn="span 4">
Method already modeled
</VSCodeDataGridCell>
</VSCodeDataGridRow>
</DataGridRow>
);
}
});
UnmodelableMethodRow.displayName = "UnmodelableMethodRow";
function sendJumpToUsageMessage(method: Method) {
vscode.postMessage({

View File

@@ -101,6 +101,10 @@ export function ModelEditor({
initialHideModeledMethods,
);
const [revealedMethodSignature, setRevealedMethodSignature] = useState<
string | null
>(null);
useEffect(() => {
vscode.postMessage({
t: "hideModeledMethods",
@@ -136,6 +140,10 @@ export function ModelEditor({
new Set(msg.inProgressMethods),
),
);
break;
case "revealMethod":
setRevealedMethodSignature(msg.method.signature);
break;
default:
assertNever(msg);
@@ -153,6 +161,20 @@ export function ModelEditor({
};
}, []);
useEffect(() => {
// If there is a revealed method signature, hide it when the user clicks anywhere. In this case, we do need to
// show the user where the method is anymore and they should have seen it.
const listener = () => {
setRevealedMethodSignature(null);
};
window.addEventListener("click", listener);
return () => {
window.removeEventListener("click", listener);
};
}, []);
const modeledPercentage = useMemo(
() => calculateModeledPercentage(methods),
[methods],
@@ -323,6 +345,7 @@ export function ModelEditor({
inProgressMethods={inProgressMethods}
viewState={viewState}
hideModeledMethods={hideModeledMethods}
revealedMethodSignature={revealedMethodSignature}
onChange={onChange}
onSaveModelClick={onSaveModelClick}
onGenerateFromLlmClick={onGenerateFromLlmClick}

View File

@@ -23,6 +23,7 @@ export type ModeledMethodDataGridProps = {
inProgressMethods: InProgressMethods;
mode: Mode;
hideModeledMethods: boolean;
revealedMethodSignature: string | null;
onChange: (modeledMethod: ModeledMethod) => void;
};
@@ -34,6 +35,7 @@ export const ModeledMethodDataGrid = ({
inProgressMethods,
mode,
hideModeledMethods,
revealedMethodSignature,
onChange,
}: ModeledMethodDataGridProps) => {
const [methodsWithModelability, numHiddenMethods]: [
@@ -94,6 +96,7 @@ export const ModeledMethodDataGrid = ({
method.signature,
)}
mode={mode}
revealedMethodSignature={revealedMethodSignature}
onChange={onChange}
/>
))}

View File

@@ -16,6 +16,7 @@ export type ModeledMethodsListProps = {
modeledMethods: Record<string, ModeledMethod>;
modifiedSignatures: Set<string>;
inProgressMethods: InProgressMethods;
revealedMethodSignature: string | null;
viewState: ModelEditorViewState;
hideModeledMethods: boolean;
onChange: (modeledMethod: ModeledMethod) => void;
@@ -44,6 +45,7 @@ export const ModeledMethodsList = ({
inProgressMethods,
viewState,
hideModeledMethods,
revealedMethodSignature,
onChange,
onSaveModelClick,
onGenerateFromLlmClick,
@@ -89,6 +91,7 @@ export const ModeledMethodsList = ({
inProgressMethods={inProgressMethods}
viewState={viewState}
hideModeledMethods={hideModeledMethods}
revealedMethodSignature={revealedMethodSignature}
onChange={onChange}
onSaveModelClick={onSaveModelClick}
onGenerateFromLlmClick={onGenerateFromLlmClick}

View File

@@ -43,6 +43,7 @@ describe(LibraryRow.name, () => {
inProgressMethods={new InProgressMethods()}
viewState={viewState}
hideModeledMethods={false}
revealedMethodSignature={null}
onChange={onChange}
onSaveModelClick={onSaveModelClick}
onGenerateFromLlmClick={onGenerateFromLlmClick}

View File

@@ -39,6 +39,7 @@ describe(MethodRow.name, () => {
modeledMethod={modeledMethod}
methodIsUnsaved={false}
modelingInProgress={false}
revealedMethodSignature={null}
mode={Mode.Application}
onChange={onChange}
{...props}

View File

@@ -60,6 +60,7 @@ describe(ModeledMethodDataGrid.name, () => {
inProgressMethods={new InProgressMethods()}
mode={Mode.Application}
hideModeledMethods={false}
revealedMethodSignature={null}
onChange={onChange}
{...props}
/>,

View File

@@ -69,6 +69,7 @@ describe(ModeledMethodsList.name, () => {
inProgressMethods={new InProgressMethods()}
viewState={viewState}
hideModeledMethods={false}
revealedMethodSignature={null}
onChange={onChange}
onSaveModelClick={onSaveModelClick}
onGenerateFromLlmClick={onGenerateFromLlmClick}