Skip to content

Commit 487a753

Browse files
committed
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.
1 parent 79ea901 commit 487a753

File tree

11 files changed

+181
-87
lines changed

11 files changed

+181
-87
lines changed

extensions/ql-vscode/src/common/interface-types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,12 +575,18 @@ interface SetModeledMethodMessage {
575575
method: ModeledMethod;
576576
}
577577

578+
interface RevealMethodMessage {
579+
t: "revealMethod";
580+
method: Method;
581+
}
582+
578583
export type ToModelEditorMessage =
579584
| SetExtensionPackStateMessage
580585
| SetMethodsMessage
581586
| SetModeledMethodsMessage
582587
| SetModifiedMethodsMessage
583-
| SetInProgressMethodsMessage;
588+
| SetInProgressMethodsMessage
589+
| RevealMethodMessage;
584590

585591
export type FromModelEditorMessage =
586592
| ViewLoadedMsg

extensions/ql-vscode/src/model-editor/model-editor-view.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,12 @@ export class ModelEditorView extends AbstractWebview<
347347
}
348348

349349
public async revealMethod(method: Method): Promise<void> {
350-
void this.app.logger.log(`Revealing method ${JSON.stringify(method)}`);
350+
this.panel?.reveal();
351+
352+
await this.postMessage({
353+
t: "revealMethod",
354+
method,
355+
});
351356
}
352357

353358
private async setViewState(): Promise<void> {

extensions/ql-vscode/src/view/model-editor/LibraryRow.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from "react";
2-
import { useCallback, useMemo, useState } from "react";
2+
import { useCallback, useEffect, useMemo, useState } from "react";
33
import { styled } from "styled-components";
44
import { Method } from "../../model-editor/method";
55
import { ModeledMethod } from "../../model-editor/modeled-method";
@@ -76,6 +76,7 @@ export type LibraryRowProps = {
7676
inProgressMethods: InProgressMethods;
7777
viewState: ModelEditorViewState;
7878
hideModeledMethods: boolean;
79+
revealedMethodSignature: string | null;
7980
onChange: (modeledMethod: ModeledMethod) => void;
8081
onSaveModelClick: (
8182
methods: Method[],
@@ -100,6 +101,7 @@ export const LibraryRow = ({
100101
inProgressMethods,
101102
viewState,
102103
hideModeledMethods,
104+
revealedMethodSignature,
103105
onChange,
104106
onSaveModelClick,
105107
onGenerateFromLlmClick,
@@ -117,6 +119,14 @@ export const LibraryRow = ({
117119
setExpanded((oldIsExpanded) => !oldIsExpanded);
118120
}, []);
119121

122+
useEffect(() => {
123+
// If any of the methods in this group is the one that should be revealed, we should expand
124+
// this group so the method can highlight itself.
125+
if (methods.some((m) => m.signature === revealedMethodSignature)) {
126+
setExpanded(true);
127+
}
128+
}, [methods, revealedMethodSignature]);
129+
120130
const handleModelWithAI = useCallback(
121131
async (e: React.MouseEvent) => {
122132
onGenerateFromLlmClick(title, methods, modeledMethods);
@@ -227,6 +237,7 @@ export const LibraryRow = ({
227237
inProgressMethods={inProgressMethods}
228238
mode={viewState.mode}
229239
hideModeledMethods={hideModeledMethods}
240+
revealedMethodSignature={revealedMethodSignature}
230241
onChange={onChange}
231242
/>
232243
<SectionDivider />

extensions/ql-vscode/src/view/model-editor/MethodRow.tsx

Lines changed: 123 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
VSCodeProgressRing,
66
} from "@vscode/webview-ui-toolkit/react";
77
import * as React from "react";
8-
import { useCallback } from "react";
8+
import { forwardRef, useCallback, useEffect, useRef } from "react";
99
import { styled } from "styled-components";
1010
import { vscode } from "../vscode-api";
1111

@@ -47,112 +47,150 @@ const ProgressRing = styled(VSCodeProgressRing)`
4747
margin-left: auto;
4848
`;
4949

50+
const DataGridRow = styled(VSCodeDataGridRow)<{ focused?: boolean }>`
51+
outline: ${(props) =>
52+
props.focused ? "1px solid var(--vscode-focusBorder)" : "none"};
53+
`;
54+
5055
export type MethodRowProps = {
5156
method: Method;
5257
methodCanBeModeled: boolean;
5358
modeledMethod: ModeledMethod | undefined;
5459
methodIsUnsaved: boolean;
5560
modelingInProgress: boolean;
5661
mode: Mode;
62+
revealedMethodSignature: string | null;
5763
onChange: (modeledMethod: ModeledMethod) => void;
5864
};
5965

6066
export const MethodRow = (props: MethodRowProps) => {
61-
const { methodCanBeModeled } = props;
67+
const { method, methodCanBeModeled, revealedMethodSignature } = props;
68+
69+
const ref = useRef<HTMLElement>();
70+
71+
useEffect(() => {
72+
if (method.signature === revealedMethodSignature) {
73+
ref.current?.scrollIntoView({
74+
behavior: "smooth",
75+
block: "center",
76+
});
77+
}
78+
}, [method, revealedMethodSignature]);
6279

6380
if (methodCanBeModeled) {
64-
return <ModelableMethodRow {...props} />;
81+
return <ModelableMethodRow {...props} ref={ref} />;
6582
} else {
66-
return <UnmodelableMethodRow {...props} />;
83+
return <UnmodelableMethodRow {...props} ref={ref} />;
6784
}
6885
};
6986

70-
function ModelableMethodRow(props: MethodRowProps) {
71-
const { method, modeledMethod, methodIsUnsaved, mode, onChange } = props;
72-
73-
const jumpToUsage = useCallback(
74-
() => sendJumpToUsageMessage(method),
75-
[method],
76-
);
77-
78-
const modelingStatus = getModelingStatus(modeledMethod, methodIsUnsaved);
79-
80-
return (
81-
<VSCodeDataGridRow data-testid="modelable-method-row">
82-
<ApiOrMethodCell gridColumn={1}>
83-
<ModelingStatusIndicator status={modelingStatus} />
84-
<MethodClassifications method={method} />
85-
<MethodName {...props.method} />
86-
{mode === Mode.Application && (
87-
<UsagesButton onClick={jumpToUsage}>
88-
{method.usages.length}
89-
</UsagesButton>
87+
const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
88+
(props, ref) => {
89+
const {
90+
method,
91+
modeledMethod,
92+
methodIsUnsaved,
93+
mode,
94+
revealedMethodSignature,
95+
onChange,
96+
} = props;
97+
98+
const jumpToUsage = useCallback(
99+
() => sendJumpToUsageMessage(method),
100+
[method],
101+
);
102+
103+
const modelingStatus = getModelingStatus(modeledMethod, methodIsUnsaved);
104+
105+
return (
106+
<DataGridRow
107+
data-testid="modelable-method-row"
108+
ref={ref}
109+
focused={revealedMethodSignature === method.signature}
110+
>
111+
<ApiOrMethodCell gridColumn={1}>
112+
<ModelingStatusIndicator status={modelingStatus} />
113+
<MethodClassifications method={method} />
114+
<MethodName {...props.method} />
115+
{mode === Mode.Application && (
116+
<UsagesButton onClick={jumpToUsage}>
117+
{method.usages.length}
118+
</UsagesButton>
119+
)}
120+
<ViewLink onClick={jumpToUsage}>View</ViewLink>
121+
{props.modelingInProgress && <ProgressRing />}
122+
</ApiOrMethodCell>
123+
{props.modelingInProgress && (
124+
<>
125+
<VSCodeDataGridCell gridColumn={2}>
126+
<InProgressDropdown />
127+
</VSCodeDataGridCell>
128+
<VSCodeDataGridCell gridColumn={3}>
129+
<InProgressDropdown />
130+
</VSCodeDataGridCell>
131+
<VSCodeDataGridCell gridColumn={4}>
132+
<InProgressDropdown />
133+
</VSCodeDataGridCell>
134+
<VSCodeDataGridCell gridColumn={5}>
135+
<InProgressDropdown />
136+
</VSCodeDataGridCell>
137+
</>
90138
)}
91-
<ViewLink onClick={jumpToUsage}>View</ViewLink>
92-
{props.modelingInProgress && <ProgressRing />}
93-
</ApiOrMethodCell>
94-
{props.modelingInProgress && (
95-
<>
96-
<VSCodeDataGridCell gridColumn={2}>
97-
<InProgressDropdown />
98-
</VSCodeDataGridCell>
99-
<VSCodeDataGridCell gridColumn={3}>
100-
<InProgressDropdown />
101-
</VSCodeDataGridCell>
102-
<VSCodeDataGridCell gridColumn={4}>
103-
<InProgressDropdown />
104-
</VSCodeDataGridCell>
105-
<VSCodeDataGridCell gridColumn={5}>
106-
<InProgressDropdown />
107-
</VSCodeDataGridCell>
108-
</>
109-
)}
110-
{!props.modelingInProgress && (
111-
<>
112-
<VSCodeDataGridCell gridColumn={2}>
113-
<ModelTypeDropdown
114-
method={method}
115-
modeledMethod={modeledMethod}
116-
onChange={onChange}
117-
/>
118-
</VSCodeDataGridCell>
119-
<VSCodeDataGridCell gridColumn={3}>
120-
<ModelInputDropdown
121-
method={method}
122-
modeledMethod={modeledMethod}
123-
onChange={onChange}
124-
/>
125-
</VSCodeDataGridCell>
126-
<VSCodeDataGridCell gridColumn={4}>
127-
<ModelOutputDropdown
128-
method={method}
129-
modeledMethod={modeledMethod}
130-
onChange={onChange}
131-
/>
132-
</VSCodeDataGridCell>
133-
<VSCodeDataGridCell gridColumn={5}>
134-
<ModelKindDropdown
135-
method={method}
136-
modeledMethod={modeledMethod}
137-
onChange={onChange}
138-
/>
139-
</VSCodeDataGridCell>
140-
</>
141-
)}
142-
</VSCodeDataGridRow>
143-
);
144-
}
145-
146-
function UnmodelableMethodRow(props: MethodRowProps) {
147-
const { method, mode } = props;
139+
{!props.modelingInProgress && (
140+
<>
141+
<VSCodeDataGridCell gridColumn={2}>
142+
<ModelTypeDropdown
143+
method={method}
144+
modeledMethod={modeledMethod}
145+
onChange={onChange}
146+
/>
147+
</VSCodeDataGridCell>
148+
<VSCodeDataGridCell gridColumn={3}>
149+
<ModelInputDropdown
150+
method={method}
151+
modeledMethod={modeledMethod}
152+
onChange={onChange}
153+
/>
154+
</VSCodeDataGridCell>
155+
<VSCodeDataGridCell gridColumn={4}>
156+
<ModelOutputDropdown
157+
method={method}
158+
modeledMethod={modeledMethod}
159+
onChange={onChange}
160+
/>
161+
</VSCodeDataGridCell>
162+
<VSCodeDataGridCell gridColumn={5}>
163+
<ModelKindDropdown
164+
method={method}
165+
modeledMethod={modeledMethod}
166+
onChange={onChange}
167+
/>
168+
</VSCodeDataGridCell>
169+
</>
170+
)}
171+
</DataGridRow>
172+
);
173+
},
174+
);
175+
ModelableMethodRow.displayName = "ModelableMethodRow";
176+
177+
const UnmodelableMethodRow = forwardRef<
178+
HTMLElement | undefined,
179+
MethodRowProps
180+
>((props, ref) => {
181+
const { method, mode, revealedMethodSignature } = props;
148182

149183
const jumpToUsage = useCallback(
150184
() => sendJumpToUsageMessage(method),
151185
[method],
152186
);
153187

154188
return (
155-
<VSCodeDataGridRow data-testid="unmodelable-method-row">
189+
<DataGridRow
190+
data-testid="unmodelable-method-row"
191+
ref={ref}
192+
focused={revealedMethodSignature === method.signature}
193+
>
156194
<ApiOrMethodCell gridColumn={1}>
157195
<ModelingStatusIndicator status="saved" />
158196
<MethodName {...props.method} />
@@ -167,9 +205,10 @@ function UnmodelableMethodRow(props: MethodRowProps) {
167205
<VSCodeDataGridCell gridColumn="span 4">
168206
Method already modeled
169207
</VSCodeDataGridCell>
170-
</VSCodeDataGridRow>
208+
</DataGridRow>
171209
);
172-
}
210+
});
211+
UnmodelableMethodRow.displayName = "UnmodelableMethodRow";
173212

174213
function sendJumpToUsageMessage(method: Method) {
175214
vscode.postMessage({

extensions/ql-vscode/src/view/model-editor/ModelEditor.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ export function ModelEditor({
101101
initialHideModeledMethods,
102102
);
103103

104+
const [revealedMethodSignature, setRevealedMethodSignature] = useState<
105+
string | null
106+
>(null);
107+
104108
useEffect(() => {
105109
vscode.postMessage({
106110
t: "hideModeledMethods",
@@ -136,6 +140,10 @@ export function ModelEditor({
136140
new Set(msg.inProgressMethods),
137141
),
138142
);
143+
break;
144+
case "revealMethod":
145+
setRevealedMethodSignature(msg.method.signature);
146+
139147
break;
140148
default:
141149
assertNever(msg);
@@ -153,6 +161,20 @@ export function ModelEditor({
153161
};
154162
}, []);
155163

164+
useEffect(() => {
165+
// If there is a revealed method signature, hide it when the user clicks anywhere. In this case, we do need to
166+
// show the user where the method is anymore and they should have seen it.
167+
const listener = () => {
168+
setRevealedMethodSignature(null);
169+
};
170+
171+
window.addEventListener("click", listener);
172+
173+
return () => {
174+
window.removeEventListener("click", listener);
175+
};
176+
}, []);
177+
156178
const modeledPercentage = useMemo(
157179
() => calculateModeledPercentage(methods),
158180
[methods],
@@ -323,6 +345,7 @@ export function ModelEditor({
323345
inProgressMethods={inProgressMethods}
324346
viewState={viewState}
325347
hideModeledMethods={hideModeledMethods}
348+
revealedMethodSignature={revealedMethodSignature}
326349
onChange={onChange}
327350
onSaveModelClick={onSaveModelClick}
328351
onGenerateFromLlmClick={onGenerateFromLlmClick}

0 commit comments

Comments
 (0)