@opensumi/ide-addons 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.
Files changed (39) hide show
  1. package/lib/browser/chrome-devtools.contribution.js.map +1 -1
  2. package/lib/browser/connection-rtt-contribution.js.map +1 -1
  3. package/lib/browser/connection-rtt-service.js.map +1 -1
  4. package/lib/browser/file-content-update-time.contribution.js +4 -4
  5. package/lib/browser/file-content-update-time.contribution.js.map +1 -1
  6. package/lib/browser/file-drop.contribution.js.map +1 -1
  7. package/lib/browser/file-drop.service.js.map +1 -1
  8. package/lib/browser/file-search.contribution.d.ts.map +1 -1
  9. package/lib/browser/file-search.contribution.js +15 -6
  10. package/lib/browser/file-search.contribution.js.map +1 -1
  11. package/lib/browser/index.js.map +1 -1
  12. package/lib/browser/language-change.contribution.js +1 -1
  13. package/lib/browser/language-change.contribution.js.map +1 -1
  14. package/lib/browser/status-bar-contribution.js.map +1 -1
  15. package/lib/browser/toolbar-customize/toolbar-customize.contribution.js.map +1 -1
  16. package/lib/browser/toolbar-customize/toolbar-customize.js +1 -1
  17. package/lib/browser/toolbar-customize/toolbar-customize.js.map +1 -1
  18. package/lib/node/connection-rtt-service.js.map +1 -1
  19. package/lib/node/file-drop.service.js.map +1 -1
  20. package/lib/node/index.js.map +1 -1
  21. package/package.json +17 -16
  22. package/src/browser/chrome-devtools.contribution.ts +66 -0
  23. package/src/browser/connection-rtt-contribution.ts +82 -0
  24. package/src/browser/connection-rtt-service.ts +15 -0
  25. package/src/browser/file-content-update-time.contribution.ts +256 -0
  26. package/src/browser/file-drop.contribution.ts +17 -0
  27. package/src/browser/file-drop.service.ts +165 -0
  28. package/src/browser/file-search.contribution.ts +612 -0
  29. package/src/browser/index.ts +44 -0
  30. package/src/browser/language-change.contribution.ts +45 -0
  31. package/src/browser/status-bar-contribution.ts +28 -0
  32. package/src/browser/toolbar-customize/style.module.less +52 -0
  33. package/src/browser/toolbar-customize/toolbar-customize.contribution.ts +59 -0
  34. package/src/browser/toolbar-customize/toolbar-customize.tsx +147 -0
  35. package/src/common/index.ts +46 -0
  36. package/src/index.ts +1 -0
  37. package/src/node/connection-rtt-service.ts +10 -0
  38. package/src/node/file-drop.service.ts +34 -0
  39. package/src/node/index.ts +37 -0
@@ -0,0 +1,256 @@
1
+ import { Injectable, Autowired } from '@opensumi/di';
2
+ import { ClientAppContribution, Domain } from '@opensumi/ide-core-browser';
3
+ import { PreferenceSchema, PreferenceSchemaProvider, PreferenceService } from '@opensumi/ide-core-browser';
4
+ import {
5
+ debounce,
6
+ IReporterService,
7
+ StaleLRUMap,
8
+ OnEvent,
9
+ URI,
10
+ WithEventBus,
11
+ Schemes,
12
+ } from '@opensumi/ide-core-common';
13
+ import { EditorDocumentModelSavedEvent, EditorDocumentModelWillSaveEvent } from '@opensumi/ide-editor/lib/browser';
14
+ import { IWorkspaceService } from '@opensumi/ide-workspace';
15
+ import {
16
+ FileOperation,
17
+ WorkspaceFileEvent,
18
+ IWorkspaceFileOperationParticipant,
19
+ IWorkspaceFileService,
20
+ } from '@opensumi/ide-workspace-edit';
21
+
22
+ enum ContentUpdateOperation {
23
+ /**
24
+ * 文本内容更新
25
+ */
26
+ Update = -100,
27
+ }
28
+
29
+ type FileAndContentOperation = FileOperation | ContentUpdateOperation;
30
+
31
+ interface FileChangeMarker {
32
+ /**
33
+ * 开始的时间戳
34
+ */
35
+ start: number;
36
+ /**
37
+ * 标记操作种类
38
+ */
39
+ operation: FileAndContentOperation;
40
+ }
41
+
42
+ enum FileOperationResultEnum {
43
+ SUCCESS = 'success',
44
+ FAIL = 'fail',
45
+ }
46
+
47
+ const FileOperationMsgMapStr = {
48
+ [ContentUpdateOperation.Update]: 'update',
49
+ [FileOperation.COPY]: 'copy',
50
+ [FileOperation.CREATE]: 'create',
51
+ [FileOperation.DELETE]: 'delete',
52
+ [FileOperation.MOVE]: 'move',
53
+ };
54
+
55
+ const TRACE_LOG_FLAG = 'trace.file-content.update.time';
56
+
57
+ const configurationSchema: PreferenceSchema = {
58
+ title: 'Addons',
59
+ properties: {
60
+ [TRACE_LOG_FLAG]: {
61
+ type: 'boolean',
62
+ default: false,
63
+ description: 'flag for tracing file/content update time consuming',
64
+ },
65
+ },
66
+ };
67
+
68
+ /**
69
+ * 本模块默认不加载,集成侧需要自定引入
70
+ * 监听文件及内容变更的耗时
71
+ * 命中条件:
72
+ * * file 类型文件
73
+ * * 在当前项目的 workspace root 下的文件
74
+ * * 打开了 trace log 配置 (具体字段见 `TRACE_LOG_FLAG`)
75
+ * 记录的文件操作类型:
76
+ * * 文件的创建/删除/复制/移动
77
+ * * 文件内容变更 (增加 debounce 500ms 避免自动保存/批量替换/LSP重命名 触发的大量变更导致上报数量过多)
78
+ * 记录的字段:
79
+ * * name: `FileAndContentOperation`
80
+ * * msg: 文件操作的类型
81
+ * * extra.uri: 文件 uri string
82
+ * * extra.time: 耗时
83
+ * * extra.result: 0 为成功 1 为失败
84
+ * 其他备注:
85
+ * * time 的实际意义:
86
+ * - 记录了文件操作的从 js 代码开始执行到创建成功后前端可以收到通知的过程时长
87
+ * - 但是不涵盖异步事件通知文件树进行节点变幻到前端渲染完成的耗时部分
88
+ * - 当然对于文件内容更新的操作,这个时间就是完整的时间
89
+ */
90
+ @Injectable()
91
+ @Domain(ClientAppContribution)
92
+ export class FileAndContentUpdateTimeContribution extends WithEventBus {
93
+ @Autowired(IWorkspaceFileService)
94
+ private readonly workspaceFileService: IWorkspaceFileService;
95
+
96
+ @Autowired(PreferenceService)
97
+ private readonly preferenceService: PreferenceService;
98
+
99
+ @Autowired(IWorkspaceService)
100
+ private readonly workspaceService: IWorkspaceService;
101
+
102
+ @Autowired(IReporterService)
103
+ private readonly reporterService: IReporterService;
104
+
105
+ @Autowired(PreferenceSchemaProvider)
106
+ private readonly preferenceSchemaProvider: PreferenceSchemaProvider;
107
+
108
+ private _traceConfig = false;
109
+
110
+ private _markedFileUris = new StaleLRUMap<string, FileChangeMarker>(100, 50, 10 * 60 * 1000 /* 十分钟超时清理 */);
111
+
112
+ constructor() {
113
+ super();
114
+ }
115
+
116
+ onDidStart() {
117
+ // Set configuration schema
118
+ this.preferenceSchemaProvider.setSchema(configurationSchema);
119
+ // Do somethings when file operation is happening
120
+ this._participateFileOperation();
121
+ // Init listener for showPreview
122
+ this._initTraceConfig();
123
+ }
124
+
125
+ // 增加一个 debounce 避免 editor.autoSave 导致频繁变更触发
126
+ // 只监听 file 协议
127
+ @debounce(500)
128
+ @OnEvent(EditorDocumentModelWillSaveEvent)
129
+ public handleEditorDocModelWillSave(e: EditorDocumentModelWillSaveEvent) {
130
+ const { uri } = e.payload;
131
+ // 手动保存的立刻上报
132
+ // 标记文件链接
133
+ this._markFileUri(uri, ContentUpdateOperation.Update);
134
+ }
135
+
136
+ @debounce(500)
137
+ @OnEvent(EditorDocumentModelSavedEvent)
138
+ public handleEditorDocModelDidSave(e: EditorDocumentModelSavedEvent) {
139
+ const uri = e.payload;
140
+ this._reportFileOperation(uri, ContentUpdateOperation.Update, FileOperationResultEnum.SUCCESS);
141
+ }
142
+
143
+ private _participateFileOperation() {
144
+ // BEFORE file operation
145
+ this.addDispose(
146
+ this.workspaceFileService.registerFileOperationParticipant({
147
+ participate: this.fileOperationParticipant.bind(this),
148
+ }),
149
+ );
150
+
151
+ // AFTER file operation SUCCEED
152
+ this.addDispose(
153
+ this.workspaceFileService.onDidRunWorkspaceFileOperation(this._handleFileOperationDidRun.bind(this)),
154
+ );
155
+
156
+ // AFTER file operation FAILED
157
+ this.addDispose(
158
+ this.workspaceFileService.onDidFailWorkspaceFileOperation(this._handleFileOperationDidFail.bind(this)),
159
+ );
160
+ }
161
+
162
+ private async fileOperationParticipant(...args: Parameters<IWorkspaceFileOperationParticipant['participate']>) {
163
+ const [files, operation] = args;
164
+ for (const file of files) {
165
+ this._markFileUri(new URI(file.target), operation);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * refactoring changes 带来的文件增删也会带进来,但是一般来说比较少见
171
+ * 出于性能考虑,删除文件的设计是异步操作的,因此删除文件可能耗时比肉眼可见的要长一些
172
+ */
173
+ private _handleFileOperationDidRun(e: WorkspaceFileEvent) {
174
+ const { files, operation } = e;
175
+ for (const file of files) {
176
+ this._reportFileOperation(new URI(file.target), operation, FileOperationResultEnum.SUCCESS);
177
+ }
178
+ }
179
+
180
+ private _handleFileOperationDidFail(e: WorkspaceFileEvent) {
181
+ const { files, operation } = e;
182
+ for (const file of files) {
183
+ this._reportFileOperation(new URI(file.target), operation, FileOperationResultEnum.FAIL);
184
+ }
185
+ }
186
+
187
+ private async _markFileUri(uri: URI, operation: FileAndContentOperation) {
188
+ const shouldReport = await this._shouldReport(uri);
189
+ if (!shouldReport) {
190
+ return;
191
+ }
192
+
193
+ const uriStr = uri.toString(true);
194
+ this._markedFileUris.set(uriStr, {
195
+ start: Date.now(),
196
+ operation,
197
+ });
198
+ }
199
+
200
+ private async _reportFileOperation(uri: URI, operation: FileAndContentOperation, result: FileOperationResultEnum) {
201
+ const shouldReport = await this._shouldReport(uri);
202
+ if (!shouldReport) {
203
+ return;
204
+ }
205
+
206
+ const uriStr = uri.toString(true);
207
+ const existedMarker = this._markedFileUris.get(uriStr);
208
+ // 不存在标记 | 标记的行为不匹配的 均过滤掉
209
+ if (!existedMarker || existedMarker.operation !== operation) {
210
+ return;
211
+ }
212
+ // report here
213
+ this.reporterService.point(
214
+ 'FileAndContentOperation',
215
+ FileOperationMsgMapStr[existedMarker.operation] || 'unknown',
216
+ {
217
+ uri: uriStr,
218
+ time: Date.now() - existedMarker.start,
219
+ result: result === FileOperationResultEnum.SUCCESS ? 0 : 1,
220
+ },
221
+ );
222
+ // delete cached
223
+ this._markedFileUris.delete(uriStr);
224
+ }
225
+
226
+ private _initTraceConfig() {
227
+ // additional edits for file-participants
228
+ this._traceConfig = !!this.preferenceService.get<boolean>(TRACE_LOG_FLAG);
229
+ this.addDispose(
230
+ this.preferenceService.onPreferenceChanged((e) => {
231
+ if (e.preferenceName === TRACE_LOG_FLAG) {
232
+ this._traceConfig = !!e.newValue;
233
+ }
234
+ }),
235
+ );
236
+ }
237
+
238
+ /**
239
+ * 不符合要求的文件直接跳过
240
+ */
241
+ private async _shouldReport(uri: URI): Promise<boolean> {
242
+ // 配置项关闭则直接跳过记录
243
+ if (!this._traceConfig) {
244
+ return false;
245
+ }
246
+
247
+ // 非当前 workspace 的文件不统计 | 非 file 协议不统计
248
+ if (uri.scheme !== Schemes.file) {
249
+ return false;
250
+ }
251
+
252
+ // 文件是当前 workspace 下的
253
+ const roots = await this.workspaceService.roots;
254
+ return roots.some((fileStat) => !!new URI(fileStat.uri).relative(uri));
255
+ }
256
+ }
@@ -0,0 +1,17 @@
1
+ import { Injectable, Autowired } from '@opensumi/di';
2
+ import { ClientAppContribution, Domain } from '@opensumi/ide-core-browser';
3
+ import { OnEvent, FileTreeDropEvent, WithEventBus } from '@opensumi/ide-core-common';
4
+
5
+ import { IFileDropFrontendService, IFileDropFrontendServiceToken } from '../common';
6
+
7
+ @Injectable()
8
+ @Domain(ClientAppContribution)
9
+ export class FileDropContribution extends WithEventBus {
10
+ @Autowired(IFileDropFrontendServiceToken)
11
+ protected readonly dropService: IFileDropFrontendService;
12
+
13
+ @OnEvent(FileTreeDropEvent)
14
+ onDidDropFile(e: FileTreeDropEvent) {
15
+ this.dropService.onDidDropFile(e);
16
+ }
17
+ }
@@ -0,0 +1,165 @@
1
+ import { Injectable, Autowired } from '@opensumi/di';
2
+ import { formatLocalize } from '@opensumi/ide-core-browser';
3
+ import { IStatusBarService, StatusBarAlignment, StatusBarEntryAccessor } from '@opensumi/ide-core-browser/lib/services';
4
+ import { WithEventBus, Uri, path } from '@opensumi/ide-core-common';
5
+ import { FileTreeDropEvent } from '@opensumi/ide-core-common/lib/types/dnd';
6
+ import { IFileServiceClient } from '@opensumi/ide-file-service/lib/common';
7
+
8
+ const { Path } = path;
9
+
10
+ import {
11
+ IFileDropFrontendService,
12
+ IFileDropBackendService,
13
+ FileDropServicePath,
14
+ IWebkitDataTransfer,
15
+ IWebkitDataTransferItemEntry,
16
+ } from '../common';
17
+
18
+ @Injectable()
19
+ export class FileDropService extends WithEventBus implements IFileDropFrontendService {
20
+ private pending: Set<string> = new Set();
21
+
22
+ @Autowired(IFileServiceClient)
23
+ protected readonly fs: IFileServiceClient;
24
+
25
+ @Autowired(FileDropServicePath)
26
+ protected readonly dropService: IFileDropBackendService;
27
+
28
+ @Autowired(IStatusBarService)
29
+ protected readonly statusBarService: IStatusBarService;
30
+
31
+ private uploadStatus?: StatusBarEntryAccessor;
32
+
33
+ private onDidUploadFileStart(fullPath: string) {
34
+ this.pending.add(fullPath);
35
+ this.createOrUpdateStatusBar();
36
+ }
37
+
38
+ private onDidUploadFileEnd(fullPath: string) {
39
+ this.pending.delete(fullPath);
40
+ this.createOrUpdateStatusBar();
41
+ }
42
+
43
+ private createOrUpdateStatusBar(speed?: string) {
44
+ if (this.pending.size === 0) {
45
+ if (this.uploadStatus) {
46
+ this.uploadStatus.dispose();
47
+ this.uploadStatus = undefined;
48
+ }
49
+ return;
50
+ }
51
+
52
+ const entryId = 'sumi-upload-file-status';
53
+ const message = formatLocalize('workbench.uploadingFiles', this.pending.size, speed || '0 MB');
54
+ const entry = {
55
+ text: message,
56
+ alignment: StatusBarAlignment.RIGHT,
57
+ tooltip: message,
58
+ iconClass: 'kaitian-icon kticon-cloud-server',
59
+ };
60
+
61
+ if (!this.uploadStatus) {
62
+ this.uploadStatus = this.statusBarService.addElement(entryId, entry);
63
+ } else {
64
+ this.uploadStatus.update({ id: entryId, ...entry });
65
+ }
66
+ }
67
+
68
+ onDidDropFile(e: FileTreeDropEvent) {
69
+ const {
70
+ payload: { event, targetDir },
71
+ } = e;
72
+ if (!targetDir || !event.dataTransfer?.files || event.dataTransfer.files.length === 0) {
73
+ return;
74
+ }
75
+
76
+ const items = (event.dataTransfer as unknown as IWebkitDataTransfer).items;
77
+
78
+ let uploadedBytes = 0;
79
+ const startTime = Date.now();
80
+ const reporter = (uploaded: number) => {
81
+ uploadedBytes += uploaded;
82
+ const now = Date.now();
83
+ const bytesUploadedPerSecond = uploadedBytes / ((now - startTime) / 1000);
84
+ const speed = `${(bytesUploadedPerSecond / 1024 / 1024).toFixed(2)} MB`;
85
+ this.createOrUpdateStatusBar(speed);
86
+ };
87
+
88
+ for (const item of items) {
89
+ const entry = item.webkitGetAsEntry();
90
+ this.processFilesEntry(targetDir, entry, reporter);
91
+ }
92
+ }
93
+
94
+ private async processFilesEntry(
95
+ targetDir: string,
96
+ entry: IWebkitDataTransferItemEntry,
97
+ reporter: (uploadedByteLength: number) => void,
98
+ ) {
99
+ if (entry.isFile) {
100
+ this.processFileEntry(entry, targetDir, reporter);
101
+ } else {
102
+ const folder = Uri.file(new Path(targetDir).join(entry.fullPath).toString()).toString();
103
+ await this.fs.createFolder(folder);
104
+ this.processDirEntry(entry, targetDir, reporter);
105
+ }
106
+ }
107
+
108
+ private toBinaryString(uint8Arr: Uint8Array): string {
109
+ let i;
110
+ const length = uint8Arr.length;
111
+ let resultString = '';
112
+ for (i = 0; i < length; i += 1) {
113
+ resultString += String.fromCharCode(uint8Arr[i]);
114
+ }
115
+ return resultString;
116
+ }
117
+
118
+ private async doUploadFile(file: File, targetDir: string, reporter: (uploadedByteLength: number) => void) {
119
+ const filePath = new Path(targetDir).join(file.name).toString();
120
+ this.onDidUploadFileStart(filePath.toString());
121
+ await this.fs.createFile(Uri.file(filePath.toString()).toString());
122
+ await this.dropService.$ensureFileExist(file.name, targetDir);
123
+ const reader = file.stream().getReader();
124
+ let res: ReadableStreamReadResult<Uint8Array> = await reader.read();
125
+ while (!res.done) {
126
+ await this.dropService.$writeStream(this.toBinaryString(res.value), file.name, targetDir, res.done);
127
+ reporter(res.value?.byteLength);
128
+ res = await reader.read();
129
+ }
130
+
131
+ if (res.done) {
132
+ this.onDidUploadFileEnd(filePath.toString());
133
+ }
134
+ }
135
+
136
+ private processFileEntry(
137
+ fileEntry: IWebkitDataTransferItemEntry,
138
+ targetDir: string,
139
+ reporter: (uploadedByteLength: number) => void,
140
+ ): void {
141
+ fileEntry.file(
142
+ (fileChunk) => {
143
+ const file = new File([fileChunk], fileEntry.fullPath!, { type: fileEntry.type! });
144
+ this.doUploadFile(file, targetDir, reporter);
145
+ },
146
+ () => {},
147
+ );
148
+ }
149
+
150
+ private processDirEntry(
151
+ entry: IWebkitDataTransferItemEntry,
152
+ targetDir: string,
153
+ reporter: (uploadedByteLength: number) => void,
154
+ ) {
155
+ const dirReader = entry.createReader();
156
+ dirReader.readEntries(
157
+ (entries) => {
158
+ entries.forEach(async (fileOrDirEntry) => {
159
+ this.processFilesEntry(targetDir, fileOrDirEntry, reporter);
160
+ });
161
+ },
162
+ () => {},
163
+ );
164
+ }
165
+ }