@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.
- package/lib/browser/bulk-edit.service.js.map +1 -1
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/refactor-preview.service.js.map +1 -1
- package/lib/browser/workspace-edit.service.js +3 -3
- package/lib/browser/workspace-edit.service.js.map +1 -1
- package/lib/browser/workspace-file.service.js +6 -6
- package/lib/browser/workspace-file.service.js.map +1 -1
- package/lib/common/index.d.ts +1 -1
- package/lib/common/index.d.ts.map +1 -1
- package/package.json +14 -13
- package/src/browser/bulk-edit.service.ts +145 -0
- package/src/browser/index.ts +126 -0
- package/src/browser/refactor-preview.service.tsx +151 -0
- package/src/browser/refactor-preview.tsx +194 -0
- package/src/browser/refactor_preview.module.less +49 -0
- package/src/browser/utils.ts +16 -0
- package/src/browser/workspace-edit.service.ts +354 -0
- package/src/browser/workspace-file.service.ts +206 -0
- package/src/common/index.ts +151 -0
- package/src/index.ts +2 -0
|
@@ -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
|
+
}
|