@opensumi/ide-workspace-edit 2.21.13 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ import { observable, action } from 'mobx';
2
+ import React from 'react';
3
+
4
+ import { Injector, Injectable, Autowired, INJECTOR_TOKEN } from '@opensumi/di';
5
+ import { Deferred, localize, MessageType } from '@opensumi/ide-core-common';
6
+ import { IMainLayoutService } from '@opensumi/ide-main-layout';
7
+ import { IDialogService } from '@opensumi/ide-overlay';
8
+ import { ResourceEdit } from '@opensumi/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService';
9
+ import type {
10
+ IWorkspaceFileEdit,
11
+ IWorkspaceTextEdit,
12
+ } from '@opensumi/monaco-editor-core/esm/vs/editor/common/languages';
13
+
14
+ import { RefactorPreview } from './refactor-preview';
15
+
16
+ export const PreviewViewId = 'RefactorPreview';
17
+
18
+ export interface IRefactorPreviewService {
19
+ edits: Array<IWorkspaceTextEdit | IWorkspaceFileEdit>;
20
+ selectedFileOrTextEdits: Set<IWorkspaceTextEdit | IWorkspaceFileEdit>;
21
+
22
+ previewEdits(edit: ResourceEdit[]): Promise<ResourceEdit[]>;
23
+
24
+ filterEdit(edit: IWorkspaceTextEdit | IWorkspaceFileEdit, checked: boolean): void;
25
+
26
+ applyEdits(): void;
27
+
28
+ clearAllEdits(): void;
29
+ }
30
+
31
+ export const IRefactorPreviewService = Symbol('IRefactorPreviewService');
32
+
33
+ @Injectable()
34
+ export class RefactorPreviewServiceImpl implements IRefactorPreviewService {
35
+ @observable.shallow
36
+ public edits: Array<IWorkspaceTextEdit | IWorkspaceFileEdit> = [];
37
+
38
+ public selectedFileOrTextEdits = observable.set<IWorkspaceTextEdit | IWorkspaceFileEdit>([], { deep: false });
39
+
40
+ @Autowired(IMainLayoutService)
41
+ protected readonly mainLayout: IMainLayoutService;
42
+
43
+ @Autowired(IDialogService)
44
+ protected readonly dialogService: IDialogService;
45
+
46
+ @Autowired(INJECTOR_TOKEN)
47
+ protected readonly injector: Injector;
48
+
49
+ private previewDeferred: Deferred<Array<IWorkspaceTextEdit | IWorkspaceFileEdit>> | null;
50
+
51
+ private clear() {
52
+ this.togglePreviewView(false);
53
+ this.edits = [];
54
+ this.previewDeferred = null;
55
+ this.selectedFileOrTextEdits.clear();
56
+ }
57
+
58
+ private registerRefactorPreviewView() {
59
+ const handler = this.mainLayout.getTabbarHandler(PreviewViewId);
60
+ if (handler) {
61
+ return;
62
+ }
63
+
64
+ this.mainLayout.collectTabbarComponent(
65
+ [
66
+ {
67
+ component: RefactorPreview,
68
+ id: PreviewViewId,
69
+ },
70
+ ],
71
+ {
72
+ title: localize('refactor-preview.title', 'REFACTOR PREVIEW'),
73
+ containerId: PreviewViewId,
74
+ hidden: true,
75
+ },
76
+ 'bottom',
77
+ );
78
+ }
79
+
80
+ private togglePreviewView(show: boolean) {
81
+ const handler = this.mainLayout.getTabbarHandler(PreviewViewId);
82
+ if (show) {
83
+ handler?.show();
84
+ handler?.activate();
85
+ } else {
86
+ handler?.hide();
87
+ handler?.deactivate();
88
+ }
89
+ }
90
+
91
+ async previewEdits(edits: ResourceEdit[]): Promise<ResourceEdit[]> {
92
+ this.registerRefactorPreviewView();
93
+
94
+ if (this.previewDeferred) {
95
+ const continued = await this.dialogService.open(
96
+ <div>
97
+ {localize('refactor-preview.overlay.title')}
98
+ <p>{localize('refactor-preview.overlay.detail')}</p>
99
+ </div>,
100
+ MessageType.Warning,
101
+ [localize('refactor-preview.overlay.cancel'), localize('refactor-preview.overlay.continue')],
102
+ );
103
+
104
+ if (continued === localize('refactor-preview.overlay.cancel')) {
105
+ return [];
106
+ }
107
+ }
108
+
109
+ this.togglePreviewView(true);
110
+
111
+ this.edits = edits;
112
+ // 默认全选
113
+ edits.forEach((edit) => {
114
+ this.selectedFileOrTextEdits.add(edit);
115
+ });
116
+
117
+ this.previewDeferred = new Deferred();
118
+
119
+ return this.previewDeferred.promise;
120
+ }
121
+
122
+ @action
123
+ filterEdit(edit: IWorkspaceTextEdit | IWorkspaceFileEdit, checked: boolean) {
124
+ if (checked) {
125
+ this.selectedFileOrTextEdits.add(edit);
126
+ } else {
127
+ this.selectedFileOrTextEdits.delete(edit);
128
+ }
129
+ }
130
+
131
+ applyEdits(): void {
132
+ if (!this.previewDeferred) {
133
+ // it's can not be happened
134
+ return;
135
+ }
136
+
137
+ const candidate = this.edits.filter((edit: IWorkspaceTextEdit) => this.selectedFileOrTextEdits.has(edit));
138
+
139
+ this.previewDeferred.resolve(candidate);
140
+ this.clear();
141
+ }
142
+
143
+ clearAllEdits(): void {
144
+ if (!this.previewDeferred) {
145
+ return;
146
+ }
147
+
148
+ this.previewDeferred.resolve([]);
149
+ this.clear();
150
+ }
151
+ }
@@ -0,0 +1,194 @@
1
+ import { observer } from 'mobx-react-lite';
2
+ import React from 'react';
3
+
4
+ import { CheckBox } from '@opensumi/ide-components/lib/checkbox';
5
+ import { RecycleList } from '@opensumi/ide-components/lib/recycle-list';
6
+ import { ViewState } from '@opensumi/ide-core-browser/lib/layout';
7
+ import { useInjectable } from '@opensumi/ide-core-browser/lib/react-hooks';
8
+ import { LabelService } from '@opensumi/ide-core-browser/lib/services/label-service';
9
+ import { URI, Uri, path } from '@opensumi/ide-core-common';
10
+ import { localize } from '@opensumi/ide-core-common/lib/localize';
11
+ import { IEditorDocumentModelService } from '@opensumi/ide-editor/lib/browser/doc-model/types';
12
+ import { ITextModel } from '@opensumi/ide-monaco/lib/browser/monaco-api/types';
13
+ import type {
14
+ ResourceFileEdit,
15
+ ResourceTextEdit,
16
+ } from '@opensumi/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService';
17
+ import type { IRange } from '@opensumi/monaco-editor-core/esm/vs/editor/common/core/range';
18
+ import * as monaco from '@opensumi/monaco-editor-core/esm/vs/editor/editor.api';
19
+
20
+ import { IRefactorPreviewService } from './refactor-preview.service';
21
+ import styles from './refactor_preview.module.less';
22
+ import { isResourceFileEdit } from './utils';
23
+
24
+ interface IRefactorNodeProps {
25
+ data: ResourceTextEdit | ResourceFileEdit;
26
+ onClick: (item: ResourceTextEdit | ResourceFileEdit) => void;
27
+ }
28
+
29
+ interface ITextEditNodeProps {
30
+ data: ResourceTextEdit;
31
+ onClick: (item: ResourceTextEdit) => void;
32
+ }
33
+
34
+ interface IFileEditNodeProps {
35
+ data: ResourceFileEdit;
36
+ onClick: (item: ResourceFileEdit) => void;
37
+ }
38
+
39
+ /**
40
+ * @param range
41
+ * @param textModel
42
+ * @example
43
+ * ```ts
44
+ * const str = `import path from 'path'`;
45
+ *
46
+ * const {
47
+ * leftPad, // import * as
48
+ * base, // path
49
+ * rightPad, // from 'path';
50
+ * } = splitLeftAndRightPadInTextModel(range\/** path range *\/, model);
51
+ * ```
52
+ */
53
+ function splitLeftAndRightPadInTextModel(range: IRange, textModel: ITextModel) {
54
+ const lineContent = textModel.getLineContent(range.startLineNumber);
55
+ const base = textModel.getValueInRange(range);
56
+
57
+ const leftPadRange = new monaco.Range(range.startLineNumber, 0, range.startLineNumber, range.startColumn);
58
+ const rightPadRange = new monaco.Range(
59
+ range.endLineNumber,
60
+ range.startColumn + base.length,
61
+ range.endLineNumber,
62
+ lineContent.length + 1,
63
+ );
64
+
65
+ const leftPad = textModel.getValueInRange(leftPadRange);
66
+ const rightPad = textModel.getValueInRange(rightPadRange);
67
+
68
+ return {
69
+ leftPad,
70
+ rightPad,
71
+ base,
72
+ };
73
+ }
74
+
75
+ const ResourceIcon: React.FC<{ uri: Uri }> = (props) => {
76
+ const labelService = useInjectable<LabelService>(LabelService);
77
+ const iconClass = labelService.getIcon(URI.from(props.uri));
78
+ return <span className={`file-icon ${iconClass}`} />;
79
+ };
80
+
81
+ const TextEditNode = observer<ITextEditNodeProps>(({ data: item }) => {
82
+ const modelService = useInjectable<IEditorDocumentModelService>(IEditorDocumentModelService);
83
+ const refactorPreviewService = useInjectable<IRefactorPreviewService>(IRefactorPreviewService);
84
+
85
+ const renderTextEditDiff = () => {
86
+ const model = modelService.getModelReference(URI.from(item.resource));
87
+ if (!model) {
88
+ return <div className={styles.refactor_preview_node_wrapper}>{item.textEdit.text}</div>;
89
+ }
90
+
91
+ const textModel = model.instance.getMonacoModel();
92
+ const { leftPad, base, rightPad } = splitLeftAndRightPadInTextModel(item.textEdit.range, textModel);
93
+
94
+ return (
95
+ <div className={styles.refactor_preview_node_wrapper}>
96
+ {leftPad}
97
+ <span className={styles.refactor_preview_node_base}>{base}</span>
98
+ <span className={styles.refactor_preview_node_new}>{item.textEdit.text}</span>
99
+ {rightPad}
100
+ </div>
101
+ );
102
+ };
103
+
104
+ return (
105
+ <div className={styles.resource_node} data-workspace-edit-type='text'>
106
+ <CheckBox
107
+ checked={refactorPreviewService.selectedFileOrTextEdits.has(item)}
108
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
109
+ refactorPreviewService.filterEdit(item, event.target.checked);
110
+ }}
111
+ />
112
+ <ResourceIcon uri={item.resource as unknown as Uri /* monaco#Uri */} />
113
+ {renderTextEditDiff()}
114
+ <span className={styles.resource_node_path}>{item.resource.path}</span>
115
+ </div>
116
+ );
117
+ });
118
+
119
+ function mapDescForFileEdit(edit: ResourceFileEdit) {
120
+ if (edit.newResource && edit.oldResource) {
121
+ // rename
122
+ return {
123
+ uri: edit.newResource,
124
+ desc: localize('refactor-preview.file.move'),
125
+ };
126
+ }
127
+ if (edit.newResource && !edit.oldResource) {
128
+ // create
129
+ return {
130
+ uri: edit.newResource,
131
+ desc: localize('refactor-preview.file.create'),
132
+ };
133
+ }
134
+ if (!edit.newResource && edit.oldResource) {
135
+ return {
136
+ uri: edit.oldResource,
137
+ desc: localize('refactor-preview.file.delete'),
138
+ };
139
+ }
140
+ return undefined;
141
+ }
142
+
143
+ const FileEditNode = observer<IFileEditNodeProps>(({ data: item }) => {
144
+ const refactorPreviewService = useInjectable<IRefactorPreviewService>(IRefactorPreviewService);
145
+
146
+ const editDesc = mapDescForFileEdit(item);
147
+ if (editDesc === undefined) {
148
+ return null;
149
+ }
150
+
151
+ const filename = path.basename(editDesc.uri.fsPath);
152
+ const dirname = path.dirname(editDesc.uri.fsPath);
153
+
154
+ return (
155
+ <div className={styles.resource_node} data-workspace-edit-type='file'>
156
+ <CheckBox
157
+ checked={refactorPreviewService.selectedFileOrTextEdits.has(item)}
158
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
159
+ refactorPreviewService.filterEdit(item, event.target.checked);
160
+ }}
161
+ />
162
+ <ResourceIcon uri={editDesc.uri as unknown as Uri /* monaco#Uri */} />
163
+ <span className={styles.refactor_preview_node_wrapper}>{filename}</span>
164
+ <span className={styles.resource_node_path}>
165
+ {dirname} ({editDesc.desc})
166
+ </span>
167
+ </div>
168
+ );
169
+ });
170
+
171
+ const RefactorNode = observer<IRefactorNodeProps>(({ data, ...restProps }) => {
172
+ if (isResourceFileEdit(data)) {
173
+ return <FileEditNode data={data} {...restProps} />;
174
+ }
175
+ return <TextEditNode data={data} {...restProps} />;
176
+ });
177
+
178
+ export const RefactorPreview = observer(({ viewState }: React.PropsWithChildren<{ viewState: ViewState }>) => {
179
+ const refactorPreviewService = useInjectable<IRefactorPreviewService>(IRefactorPreviewService);
180
+
181
+ return (
182
+ <div>
183
+ {refactorPreviewService.edits.length > 0 && (
184
+ <RecycleList
185
+ itemHeight={23}
186
+ width={viewState.width}
187
+ height={viewState.height}
188
+ data={refactorPreviewService.edits}
189
+ template={RefactorNode}
190
+ />
191
+ )}
192
+ </div>
193
+ );
194
+ });
@@ -0,0 +1,49 @@
1
+ .refactor_preview {
2
+ display: flex;
3
+ }
4
+
5
+ .resource_node {
6
+ display: flex;
7
+ height: 23px;
8
+ line-height: 23px;
9
+ padding: 0px 10px;
10
+ cursor: pointer;
11
+
12
+ .resource_clickable {
13
+ display: flex;
14
+ height: 23px;
15
+ line-height: 23px;
16
+ }
17
+
18
+ &:hover {
19
+ background-color: var(--editor-selectionBackground);
20
+ }
21
+
22
+ .resource_node_path {
23
+ font-size: 12px;
24
+ overflow: hidden;
25
+ text-overflow: ellipsis;
26
+ white-space: nowrap;
27
+ color: var(--descriptionForeground);
28
+ }
29
+ }
30
+
31
+ .refactor_preview_node_wrapper {
32
+ display: inline-block;
33
+ padding: 0px;
34
+ margin: 0px 10px 0px 10px;
35
+ overflow: hidden;
36
+ text-overflow: ellipsis;
37
+ white-space: nowrap;
38
+
39
+ .refactor_preview_node_base {
40
+ height: 16px;
41
+ text-decoration: line-through;
42
+ background-color: var(--diffEditor-removedTextBackground);
43
+ }
44
+
45
+ .refactor_preview_node_new {
46
+ height: 16px;
47
+ background-color: var(--diffEditor-insertedTextBackground);
48
+ }
49
+ }
@@ -0,0 +1,16 @@
1
+ import { isObject } from '@opensumi/ide-core-common';
2
+ import type {
3
+ ResourceFileEdit,
4
+ ResourceTextEdit,
5
+ } from '@opensumi/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService';
6
+ import { Uri } from '@opensumi/monaco-editor-core/esm/vs/editor/editor.api';
7
+
8
+ export function isResourceFileEdit(thing: any): thing is ResourceFileEdit {
9
+ return isObject(thing) && (Boolean(thing.newResource) || Boolean(thing.oldResource));
10
+ }
11
+
12
+ export function isResourceTextEdit(thing: any): thing is ResourceTextEdit {
13
+ // 重写的原因是因为直接用 monaco 存在 Uri.isUri 情况
14
+ // 我们的 Uri 并未 monaco 的 Uri 的 instance
15
+ return isObject(thing) && Uri.isUri(thing.resource) && isObject(thing.textEdit);
16
+ }