Skip to content

Commit dd5a240

Browse files
committed
feat: implement launch json editor (#2598)
* feat: implement launch json schema editor * feat: implement launch json editor * chore: improve code
1 parent 8ca0d33 commit dd5a240

17 files changed

Lines changed: 650 additions & 15 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Ajv } from 'ajv';
2+
3+
let _ajv;
4+
export const acquireAjv = (): Ajv | undefined => {
5+
if (!_ajv) {
6+
const Ajv = require('ajv');
7+
_ajv = new Ajv();
8+
return _ajv;
9+
}
10+
return _ajv;
11+
};

packages/debug/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
"@opensumi/ide-file-service": "workspace:*",
2424
"@opensumi/ide-task": "workspace:*",
2525
"@opensumi/ide-terminal-next": "workspace:*",
26+
"@rjsf/core": "^5.5.2",
27+
"@rjsf/utils": "^5.5.2",
28+
"@rjsf/validator-ajv8": "^5.5.2",
2629
"anser": "^1.4.9",
2730
"btoa": "^1.2.1"
2831
},
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { WidgetProps } from '@rjsf/utils';
2+
import React, { useMemo } from 'react';
3+
4+
import { Option, Select } from '@opensumi/ide-components';
5+
6+
export const SelectWidget = (props: WidgetProps) => {
7+
const { schema, value } = props;
8+
9+
const enumValue = useMemo(() => {
10+
if (schema && schema.enum) {
11+
return schema.enum;
12+
}
13+
return [];
14+
}, [schema, schema.enum]);
15+
16+
return (
17+
<Select value={value}>
18+
{enumValue.forEach((data: string) => {
19+
<Option value={data}>{data}</Option>;
20+
})}
21+
</Select>
22+
);
23+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { WidgetProps } from '@rjsf/utils';
2+
import React from 'react';
3+
4+
import { Input } from '@opensumi/ide-components';
5+
6+
const INPUT_STYLE = {
7+
width: '100%',
8+
};
9+
10+
export const TextWidget = (props: WidgetProps) => {
11+
const { disabled, formContext, id, onBlur, onChange, onFocus, options, placeholder, readonly, schema, value } = props;
12+
const { readonlyAsDisabled = true } = formContext;
13+
14+
const handleTextChange = ({ target }: React.ChangeEvent<HTMLInputElement>) =>
15+
onChange(target.value === '' ? options.emptyValue : target.value);
16+
17+
const handleBlur = ({ target }: React.FocusEvent<HTMLInputElement>) => onBlur(id, target.value);
18+
19+
const handleFocus = ({ target }: React.FocusEvent<HTMLInputElement>) => onFocus(id, target.value);
20+
21+
return schema.type === 'string' ? (
22+
<Input
23+
disabled={disabled || (readonlyAsDisabled && readonly)}
24+
id={id}
25+
name={id}
26+
onBlur={!readonly ? handleBlur : undefined}
27+
onChange={!readonly ? handleTextChange : undefined}
28+
onFocus={!readonly ? handleFocus : undefined}
29+
placeholder={placeholder}
30+
style={INPUT_STYLE}
31+
type={(options.inputType || 'text') as string}
32+
value={value}
33+
autoComplete='off'
34+
/>
35+
) : null;
36+
};

packages/debug/src/browser/preferences/launch-preferences-contribution.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export class LaunchResourceProvider implements IResourceProvider {
2929
readonly scheme: string = LAUNCH_VIEW_SCHEME;
3030

3131
provideResource(uri: URI): MaybePromise<IResource<any>> {
32-
// 获取文件类型 getFileType: (path: string) => string
3332
return {
3433
supportsRevive: true,
3534
name: localize('menu-bar.title.debug'),
@@ -38,11 +37,11 @@ export class LaunchResourceProvider implements IResourceProvider {
3837
};
3938
}
4039

41-
provideResourceSubname(resource: IResource, groupResources: IResource[]): string | null {
40+
provideResourceSubname(): string | null {
4241
return null;
4342
}
4443

45-
async shouldCloseResource(resource: IResource, openedResources: IResource[][]): Promise<boolean> {
44+
async shouldCloseResource(): Promise<boolean> {
4645
return true;
4746
}
4847
}

packages/debug/src/browser/preferences/launch.module.less

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@
5959
}
6060
}
6161
}
62+
63+
.launch_schema_body_container {
64+
overflow: auto;
65+
height: 100%;
66+
padding: 0 16px 12px 16px;
67+
}
6268
}
6369
}
6470

packages/debug/src/browser/preferences/launch.view.tsx

Lines changed: 144 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { withTheme } from '@rjsf/core';
2+
import { RJSFSchema, StrictRJSFSchema } from '@rjsf/utils';
3+
import validator from '@rjsf/validator-ajv8';
14
import cls from 'classnames';
25
import lodashGet from 'lodash/get';
36
import React, { useCallback, useEffect, useMemo, useState } from 'react';
@@ -12,18 +15,37 @@ import {
1215
ISchemaContributions,
1316
IJSONSchema,
1417
IJSONSchemaSnippet,
18+
IJSONSchemaMap,
19+
isUndefined,
1520
} from '@opensumi/ide-core-browser';
1621
import { EDirection, SplitPanel } from '@opensumi/ide-core-browser/lib/components';
1722
import { MenuActionList } from '@opensumi/ide-core-browser/lib/components/actions/index';
1823
import { LabelMenuItemNode } from '@opensumi/ide-core-browser/lib/menu/next/menu.interface';
24+
import { acquireAjv } from '@opensumi/ide-core-browser/lib/utils/schema';
1925

2026
import { launchExtensionSchemaUri } from '../../common/debug-schema';
2127

28+
import { SelectWidget } from './components/select-widget';
29+
import { TextWidget } from './components/text-widget';
2230
import styles from './launch.module.less';
31+
import { ArrayFieldItemTemplate } from './templates/array-field-item-template';
32+
import { ArrayFieldTemplate } from './templates/array-field-template';
33+
import {
34+
MoveUpButton,
35+
MoveDownButton,
36+
RemoveButton,
37+
SubmitButton,
38+
AddButton,
39+
CopyButton,
40+
} from './templates/button-template';
41+
import { DescriptionFieldTemplate } from './templates/description-field-template';
42+
import { FieldTemplate } from './templates/field-template';
43+
import { ObjectFieldTemplate } from './templates/object-field-template';
2344

2445
export const LaunchViewContainer = () => {
2546
const schemaRegistry = useInjectable<IJSONSchemaRegistry>(IJSONSchemaRegistry);
26-
const [snippetItems, setSnippetItems] = useState<IJSONSchemaSnippet[]>([]);
47+
const [schemaContributions, setSchemaContributions] = useState<IJSONSchema>();
48+
const [currentSnippetItem, setCurrentSnippetItem] = useState<IJSONSchemaSnippet>();
2749

2850
useEffect(() => {
2951
const disposed = schemaRegistry.onDidChangeSchema((uri: string) => {
@@ -37,23 +59,41 @@ export const LaunchViewContainer = () => {
3759
return () => disposed.dispose();
3860
}, []);
3961

40-
const handleSchemaSnippets = (contributions: ISchemaContributions) => {
41-
const launchExtension = contributions.schemas[launchExtensionSchemaUri];
62+
const handleSchemaSnippets = useCallback(
63+
(contributions: ISchemaContributions) => {
64+
const launchExtension = contributions.schemas[launchExtensionSchemaUri];
4265

43-
if (!launchExtension) {
44-
return;
66+
if (!launchExtension) {
67+
return;
68+
}
69+
70+
setSchemaContributions(launchExtension);
71+
},
72+
[schemaContributions],
73+
);
74+
75+
const snippetItems = useMemo(() => {
76+
if (!schemaContributions) {
77+
return [];
4578
}
4679

47-
const snippets: IJSONSchemaSnippet[] = lodashGet(launchExtension, [
80+
const snippets: IJSONSchemaSnippet[] = lodashGet(schemaContributions, [
4881
'properties',
4982
'configurations',
5083
'items',
5184
'defaultSnippets',
5285
] as (keyof IJSONSchema)[]);
53-
setSnippetItems(snippets.filter((s) => s.label));
54-
};
86+
return snippets.filter((s) => s.label);
87+
}, [schemaContributions]);
5588

56-
const onSelectedConfiguration = (current: string) => {};
89+
const onSelectedConfiguration = (current: string) => {
90+
const findItems = snippetItems.find(({ label }) => label === current);
91+
if (!findItems) {
92+
return;
93+
}
94+
95+
setCurrentSnippetItem(findItems);
96+
};
5797

5898
return (
5999
<ComponentContextProvider value={{ getIcon, localize }}>
@@ -70,7 +110,7 @@ export const LaunchViewContainer = () => {
70110
snippetItems={snippetItems}
71111
onSelectedConfiguration={onSelectedConfiguration}
72112
/>
73-
<LaunchBody data-sp-flex={1} />
113+
<LaunchBody data-sp-flex={1} snippetItem={currentSnippetItem} schemaContributions={schemaContributions} />
74114
</SplitPanel>
75115
</div>
76116
</ComponentContextProvider>
@@ -152,4 +192,97 @@ const LaunchIndexs = ({
152192
);
153193
};
154194

155-
const LaunchBody = () => <div>body</div>;
195+
const Form = withTheme({
196+
widgets: {
197+
TextWidget,
198+
SelectWidget,
199+
},
200+
});
201+
202+
const LaunchBody = ({
203+
snippetItem,
204+
schemaContributions,
205+
}: {
206+
snippetItem?: IJSONSchemaSnippet;
207+
schemaContributions?: IJSONSchema;
208+
}) => {
209+
if (!snippetItem) {
210+
return <div>{localize('debug.action.no.configuration')}</div>;
211+
}
212+
213+
const [schemaProperties, setSchemaProperties] = useState<IJSONSchema>();
214+
215+
useEffect(() => {
216+
const ajv = acquireAjv();
217+
// 1. 先从 schema 中找出 oneOf 池
218+
const oneOfPool: IJSONSchema[] =
219+
lodashGet(schemaContributions, ['properties', 'configurations', 'items', 'oneOf'] as (keyof IJSONSchema)[]) || [];
220+
221+
// 2. 再从 snippetItem body 中找出符合条件的 oneOf(可能存在多个,如果有多个就只取第一个)
222+
const findOneOf = oneOfPool.find((oneOf) => {
223+
const { body } = snippetItem;
224+
return ajv!.validate(oneOf, body);
225+
});
226+
227+
if (findOneOf) {
228+
setSchemaProperties(findOneOf);
229+
}
230+
}, [snippetItem, schemaContributions]);
231+
232+
const schema: RJSFSchema | undefined = useMemo(() => {
233+
if (!(schemaProperties && schemaProperties.properties)) {
234+
return;
235+
}
236+
237+
const { label, body, description } = snippetItem;
238+
const { properties } = schemaProperties;
239+
240+
const snippetProperties = Object.keys(body).reduce((pre: IJSONSchemaMap, cur: string) => {
241+
if (properties![cur]?.type === 'array' && isUndefined(properties![cur].items)) {
242+
properties![cur].items = { type: 'string' };
243+
}
244+
245+
// 如果 type 是数组,则取第一个
246+
if (Array.isArray(properties![cur]?.type)) {
247+
properties![cur].type = properties![cur]?.type![0] || 'string';
248+
}
249+
250+
pre[cur] = properties![cur];
251+
return pre;
252+
}, {});
253+
254+
return {
255+
title: label,
256+
type: 'object',
257+
description,
258+
properties: snippetProperties,
259+
} as StrictRJSFSchema;
260+
}, [snippetItem, schemaProperties]);
261+
262+
return (
263+
<div className={styles.launch_schema_body_container}>
264+
{schema && (
265+
<Form
266+
formData={snippetItem.body}
267+
schema={schema}
268+
validator={validator}
269+
templates={{
270+
ArrayFieldTemplate,
271+
ArrayFieldItemTemplate,
272+
DescriptionFieldTemplate,
273+
FieldTemplate,
274+
ObjectFieldTemplate,
275+
ButtonTemplates: {
276+
MoveUpButton,
277+
MoveDownButton,
278+
RemoveButton,
279+
SubmitButton,
280+
AddButton,
281+
CopyButton,
282+
},
283+
}}
284+
/>
285+
)}
286+
</div>
287+
);
288+
};

0 commit comments

Comments
 (0)