@opensumi/ide-workspace 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/index.js.map +1 -1
- package/lib/browser/workspace-contextkey.js.map +1 -1
- package/lib/browser/workspace-contribution.js.map +1 -1
- package/lib/browser/workspace-preferences.d.ts +1 -1
- package/lib/browser/workspace-preferences.d.ts.map +1 -1
- package/lib/browser/workspace-service.d.ts +4 -1
- package/lib/browser/workspace-service.d.ts.map +1 -1
- package/lib/browser/workspace-service.js +18 -7
- package/lib/browser/workspace-service.js.map +1 -1
- package/lib/browser/workspace-storage-service.js.map +1 -1
- package/lib/browser/workspace-variable-contribution.js.map +1 -1
- package/lib/common/mocks/workspace-service.d.ts +4 -1
- package/lib/common/mocks/workspace-service.d.ts.map +1 -1
- package/lib/common/mocks/workspace-service.js.map +1 -1
- package/lib/common/workspace.interface.d.ts +5 -2
- package/lib/common/workspace.interface.d.ts.map +1 -1
- package/lib/common/workspace.interface.js.map +1 -1
- package/package.json +10 -9
- package/src/browser/index.ts +31 -0
- package/src/browser/workspace-contextkey.ts +18 -0
- package/src/browser/workspace-contribution.ts +103 -0
- package/src/browser/workspace-data.ts +144 -0
- package/src/browser/workspace-preferences.ts +45 -0
- package/src/browser/workspace-service.ts +809 -0
- package/src/browser/workspace-storage-service.ts +64 -0
- package/src/browser/workspace-variable-contribution.ts +133 -0
- package/src/common/constants.ts +4 -0
- package/src/common/index.ts +2 -0
- package/src/common/mocks/index.ts +1 -0
- package/src/common/mocks/workspace-service.ts +132 -0
- package/src/common/workspace.interface.ts +75 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
import * as jsoncparser from 'jsonc-parser';
|
|
2
|
+
|
|
3
|
+
import { Injectable, Autowired } from '@opensumi/di';
|
|
4
|
+
import {
|
|
5
|
+
Deferred,
|
|
6
|
+
ILogger,
|
|
7
|
+
PreferenceService,
|
|
8
|
+
PreferenceSchemaProvider,
|
|
9
|
+
Event,
|
|
10
|
+
Emitter,
|
|
11
|
+
DisposableCollection,
|
|
12
|
+
PreferenceScope,
|
|
13
|
+
IDisposable,
|
|
14
|
+
Disposable,
|
|
15
|
+
AppConfig,
|
|
16
|
+
IClientApp,
|
|
17
|
+
IWindowService,
|
|
18
|
+
path,
|
|
19
|
+
} from '@opensumi/ide-core-browser';
|
|
20
|
+
import {
|
|
21
|
+
URI,
|
|
22
|
+
StorageProvider,
|
|
23
|
+
IStorage,
|
|
24
|
+
STORAGE_NAMESPACE,
|
|
25
|
+
localize,
|
|
26
|
+
formatLocalize,
|
|
27
|
+
Schemes,
|
|
28
|
+
} from '@opensumi/ide-core-common';
|
|
29
|
+
import { FileStat } from '@opensumi/ide-file-service';
|
|
30
|
+
import { FileChangeEvent } from '@opensumi/ide-file-service/lib/common';
|
|
31
|
+
import { IFileServiceClient } from '@opensumi/ide-file-service/lib/common';
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
DEFAULT_WORKSPACE_SUFFIX_NAME,
|
|
35
|
+
IWorkspaceService,
|
|
36
|
+
WorkspaceInput,
|
|
37
|
+
WORKSPACE_USER_STORAGE_FOLDER_NAME,
|
|
38
|
+
UNTITLED_WORKSPACE,
|
|
39
|
+
} from '../common';
|
|
40
|
+
|
|
41
|
+
import { WorkspaceData } from './workspace-data';
|
|
42
|
+
import { WorkspacePreferences } from './workspace-preferences';
|
|
43
|
+
|
|
44
|
+
const { Path } = path;
|
|
45
|
+
|
|
46
|
+
@Injectable()
|
|
47
|
+
export class WorkspaceService implements IWorkspaceService {
|
|
48
|
+
private _workspace: FileStat | undefined;
|
|
49
|
+
|
|
50
|
+
private _roots: FileStat[] = [];
|
|
51
|
+
private deferredRoots = new Deferred<FileStat[]>();
|
|
52
|
+
|
|
53
|
+
@Autowired(IFileServiceClient)
|
|
54
|
+
protected readonly fileServiceClient: IFileServiceClient;
|
|
55
|
+
|
|
56
|
+
@Autowired(IWindowService)
|
|
57
|
+
protected readonly windowService: IWindowService;
|
|
58
|
+
|
|
59
|
+
@Autowired(ILogger)
|
|
60
|
+
protected logger: ILogger;
|
|
61
|
+
|
|
62
|
+
@Autowired(WorkspacePreferences)
|
|
63
|
+
protected preferences: WorkspacePreferences;
|
|
64
|
+
|
|
65
|
+
@Autowired(PreferenceService)
|
|
66
|
+
protected preferenceService: PreferenceService;
|
|
67
|
+
|
|
68
|
+
@Autowired(PreferenceSchemaProvider)
|
|
69
|
+
protected readonly schemaProvider: PreferenceSchemaProvider;
|
|
70
|
+
|
|
71
|
+
@Autowired(AppConfig)
|
|
72
|
+
protected readonly appConfig: AppConfig;
|
|
73
|
+
|
|
74
|
+
@Autowired(StorageProvider)
|
|
75
|
+
private readonly storageProvider: StorageProvider;
|
|
76
|
+
|
|
77
|
+
private recentGlobalStorage: IStorage;
|
|
78
|
+
|
|
79
|
+
@Autowired(IClientApp)
|
|
80
|
+
private readonly clientApp: IClientApp;
|
|
81
|
+
|
|
82
|
+
get workspaceSuffixName() {
|
|
83
|
+
return this.appConfig.workspaceSuffixName || DEFAULT_WORKSPACE_SUFFIX_NAME;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected applicationName: string;
|
|
87
|
+
|
|
88
|
+
private _whenReady: Deferred<void> = new Deferred();
|
|
89
|
+
|
|
90
|
+
protected readonly toDisposableCollection: DisposableCollection = new DisposableCollection();
|
|
91
|
+
|
|
92
|
+
// 映射工作区显示的文字信息
|
|
93
|
+
private workspaceToName = {};
|
|
94
|
+
|
|
95
|
+
public init() {
|
|
96
|
+
this.doInit();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public async initFileServiceExclude() {
|
|
100
|
+
await this.setFileServiceExcludes();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public get whenReady() {
|
|
104
|
+
return this._whenReady.promise;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected async doInit(): Promise<void> {
|
|
108
|
+
// 这里的 `appName` 存在默认值
|
|
109
|
+
this.applicationName = this.appConfig.appName!;
|
|
110
|
+
const wpUriString = this.getDefaultWorkspacePath();
|
|
111
|
+
|
|
112
|
+
this.listenPreference();
|
|
113
|
+
|
|
114
|
+
if (wpUriString) {
|
|
115
|
+
const wpStat = await this.toFileStat(wpUriString);
|
|
116
|
+
await this.setWorkspace(wpStat);
|
|
117
|
+
this.toDisposableCollection.push(
|
|
118
|
+
this.fileServiceClient.onFilesChanged((event) => {
|
|
119
|
+
if (this._workspace && FileChangeEvent.isAffected(event, new URI(this._workspace.uri))) {
|
|
120
|
+
this.updateWorkspace();
|
|
121
|
+
}
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
// 处理空工作区情况
|
|
126
|
+
this.deferredRoots.resolve([]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this._whenReady.resolve();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected getTemporaryWorkspaceFileUri(home: URI): URI {
|
|
133
|
+
return home
|
|
134
|
+
.resolve(this.appConfig.storageDirName || WORKSPACE_USER_STORAGE_FOLDER_NAME)
|
|
135
|
+
.resolve(`${UNTITLED_WORKSPACE}.${this.workspaceSuffixName}`)
|
|
136
|
+
.withScheme(Schemes.file);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
protected listenPreference() {
|
|
140
|
+
const watchExcludeName = 'files.watcherExclude';
|
|
141
|
+
const filesExcludeName = 'files.exclude';
|
|
142
|
+
const multiRootPrefName = 'workspace.supportMultiRootWorkspace';
|
|
143
|
+
|
|
144
|
+
this.toDisposableCollection.push(
|
|
145
|
+
this.preferenceService.onPreferenceChanged((e) => {
|
|
146
|
+
// 工作区切换到多工作区时,可能会触发一次所有工作区配置的 Changed
|
|
147
|
+
if (e.preferenceName === watchExcludeName) {
|
|
148
|
+
this.fileServiceClient.setWatchFileExcludes(this.getFlattenExcludes(watchExcludeName));
|
|
149
|
+
} else if (e.preferenceName === filesExcludeName) {
|
|
150
|
+
this.fileServiceClient
|
|
151
|
+
.setFilesExcludes(
|
|
152
|
+
this.getFlattenExcludes(filesExcludeName),
|
|
153
|
+
this._roots.map((stat) => stat.uri),
|
|
154
|
+
)
|
|
155
|
+
.then(() => {
|
|
156
|
+
// 通知目录树更新
|
|
157
|
+
this.onWorkspaceFileExcludeChangeEmitter.fire();
|
|
158
|
+
});
|
|
159
|
+
} else if (e.preferenceName === multiRootPrefName) {
|
|
160
|
+
this.updateWorkspace();
|
|
161
|
+
}
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
protected async setFileServiceExcludes() {
|
|
167
|
+
const watchExcludeName = 'files.watcherExclude';
|
|
168
|
+
const filesExcludeName = 'files.exclude';
|
|
169
|
+
|
|
170
|
+
await this.preferenceService.ready;
|
|
171
|
+
await this.fileServiceClient.setWatchFileExcludes(this.getFlattenExcludes(watchExcludeName));
|
|
172
|
+
await this.fileServiceClient.setFilesExcludes(
|
|
173
|
+
this.getFlattenExcludes(filesExcludeName),
|
|
174
|
+
this._roots.map((stat) => stat.uri),
|
|
175
|
+
);
|
|
176
|
+
this.onWorkspaceFileExcludeChangeEmitter.fire();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
protected getFlattenExcludes(name: string): string[] {
|
|
180
|
+
const excludes: string[] = [];
|
|
181
|
+
const fileExcludes = this.preferenceService.get<any>(name);
|
|
182
|
+
if (fileExcludes) {
|
|
183
|
+
for (const key of Object.keys(fileExcludes)) {
|
|
184
|
+
if (fileExcludes[key]) {
|
|
185
|
+
excludes.push(key);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return excludes;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 获取默认的workspace路径
|
|
194
|
+
*/
|
|
195
|
+
protected getDefaultWorkspacePath(): string | undefined {
|
|
196
|
+
if (this.appConfig.workspaceDir) {
|
|
197
|
+
// 默认读取传入配置路径
|
|
198
|
+
let path: string;
|
|
199
|
+
try {
|
|
200
|
+
// 尝试使用 Windows 下带盘符的路径进行解析
|
|
201
|
+
path = new URL(URI.file(this.appConfig.workspaceDir).codeUri.fsPath).toString();
|
|
202
|
+
} catch (e) {
|
|
203
|
+
// 解析失败时仍然使用非 Windows 环境下的解析方式
|
|
204
|
+
path = URI.file(this.appConfig.workspaceDir).toString();
|
|
205
|
+
}
|
|
206
|
+
return path;
|
|
207
|
+
} else {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
get roots(): Promise<FileStat[]> {
|
|
213
|
+
return this.deferredRoots.promise;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
tryGetRoots(): FileStat[] {
|
|
217
|
+
return this._roots;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
get workspace(): FileStat | undefined {
|
|
221
|
+
return this._workspace;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 工作区改变事件
|
|
225
|
+
protected readonly onWorkspaceChangeEmitter = new Emitter<FileStat[]>();
|
|
226
|
+
get onWorkspaceChanged(): Event<FileStat[]> {
|
|
227
|
+
return this.onWorkspaceChangeEmitter.event;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
protected readonly onWorkspaceFileExcludeChangeEmitter = new Emitter<void>();
|
|
231
|
+
get onWorkspaceFileExcludeChanged(): Event<void> {
|
|
232
|
+
return this.onWorkspaceFileExcludeChangeEmitter.event;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 操作中的工作区改变事件
|
|
236
|
+
protected readonly onWorkspaceLocationChangedEmitter = new Emitter<FileStat | undefined>();
|
|
237
|
+
get onWorkspaceLocationChanged(): Event<FileStat | undefined> {
|
|
238
|
+
return this.onWorkspaceLocationChangedEmitter.event;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
protected readonly toDisposeOnWorkspace = new DisposableCollection();
|
|
242
|
+
|
|
243
|
+
public async setWorkspace(workspaceStat: FileStat | undefined): Promise<void> {
|
|
244
|
+
if (FileStat.equals(this._workspace, workspaceStat)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.toDisposeOnWorkspace.dispose();
|
|
248
|
+
this._workspace = workspaceStat;
|
|
249
|
+
if (this._workspace) {
|
|
250
|
+
const uri = new URI(this._workspace.uri);
|
|
251
|
+
this.fileServiceClient.watchFileChanges(uri).then((watcher) => {
|
|
252
|
+
this.toDisposeOnWorkspace.push(watcher);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
this.updateTitle();
|
|
256
|
+
await this.updateWorkspace();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
protected async updateWorkspace(): Promise<void> {
|
|
260
|
+
if (this._workspace) {
|
|
261
|
+
this.toFileStat(this._workspace.uri).then((stat) => (this._workspace = stat));
|
|
262
|
+
this.setMostRecentlyUsedWorkspace(this._workspace.uri);
|
|
263
|
+
}
|
|
264
|
+
await this.updateRoots();
|
|
265
|
+
if (!this._workspace?.isDirectory) {
|
|
266
|
+
// 工作区模式才需要额外监听根目录,否则会出现重复监听问题
|
|
267
|
+
this.watchRoots();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
protected async updateRoots(): Promise<void> {
|
|
272
|
+
const newRoots = await this.computeRoots();
|
|
273
|
+
let rootsChanged = false;
|
|
274
|
+
if (newRoots.length !== this._roots.length || newRoots.length === 0) {
|
|
275
|
+
rootsChanged = true;
|
|
276
|
+
} else {
|
|
277
|
+
for (const newRoot of newRoots) {
|
|
278
|
+
if (!this._roots.some((r) => r.uri === newRoot.uri)) {
|
|
279
|
+
rootsChanged = true;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (rootsChanged) {
|
|
285
|
+
this._roots = newRoots;
|
|
286
|
+
this.deferredRoots.resolve(this._roots); // in order to resolve first
|
|
287
|
+
this.deferredRoots = new Deferred<FileStat[]>();
|
|
288
|
+
this.deferredRoots.resolve(this._roots);
|
|
289
|
+
this.onWorkspaceChangeEmitter.fire(this._roots);
|
|
290
|
+
// 重新根据工作区Roots设置 fileExclude 及 watchExclude
|
|
291
|
+
this.setFileServiceExcludes();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
protected async computeRoots(): Promise<FileStat[]> {
|
|
296
|
+
const roots: FileStat[] = [];
|
|
297
|
+
if (this._workspace) {
|
|
298
|
+
if (this._workspace.isDirectory) {
|
|
299
|
+
return [this._workspace];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const workspaceData = await this.getWorkspaceDataFromFile();
|
|
303
|
+
if (workspaceData) {
|
|
304
|
+
for (let { path } of workspaceData.folders) {
|
|
305
|
+
if (path === '.') {
|
|
306
|
+
path = new URI(this._workspace.uri).parent.toString();
|
|
307
|
+
}
|
|
308
|
+
const valid = await this.toValidRoot(path);
|
|
309
|
+
if (valid) {
|
|
310
|
+
roots.push(valid);
|
|
311
|
+
} else {
|
|
312
|
+
roots.push({
|
|
313
|
+
uri: path,
|
|
314
|
+
lastModification: Date.now(),
|
|
315
|
+
isDirectory: true,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return roots;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
protected async getWorkspaceDataFromFile(): Promise<WorkspaceData | undefined> {
|
|
325
|
+
if (this._workspace && (await this.fileServiceClient.access(this._workspace.uri))) {
|
|
326
|
+
if (this._workspace.isDirectory) {
|
|
327
|
+
return {
|
|
328
|
+
folders: [{ path: this._workspace.uri }],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
const { content } = await this.fileServiceClient.resolveContent(this._workspace.uri);
|
|
332
|
+
const strippedContent = jsoncparser.stripComments(content);
|
|
333
|
+
const data = jsoncparser.parse(strippedContent);
|
|
334
|
+
if (data && WorkspaceData.is(data)) {
|
|
335
|
+
const stat = await this.fileServiceClient.getFileStat(this._workspace.uri);
|
|
336
|
+
return WorkspaceData.transformToAbsolute(data, stat);
|
|
337
|
+
}
|
|
338
|
+
this.logger.error(
|
|
339
|
+
`Unable to retrieve workspace data from the file: '${this._workspace.uri}'. Please check if the file is corrupted.`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
protected formatTitle(title?: string): string {
|
|
345
|
+
const name = this.applicationName;
|
|
346
|
+
|
|
347
|
+
let documentTitle = title ? `${title} — ${name}` : name;
|
|
348
|
+
if (this.appConfig.extensionDevelopmentHost) {
|
|
349
|
+
documentTitle = `[${localize('workspace.development.title')}] ${documentTitle}`;
|
|
350
|
+
}
|
|
351
|
+
return documentTitle;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 更新页面Title
|
|
355
|
+
protected updateTitle() {
|
|
356
|
+
// 是否允许按照 workspace dir 修改 document#title
|
|
357
|
+
if (!this.appConfig.allowSetDocumentTitleFollowWorkspaceDir) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let title: string | undefined;
|
|
362
|
+
if (this._workspace) {
|
|
363
|
+
const uri = new URI(this._workspace.uri);
|
|
364
|
+
const displayName = uri.displayName;
|
|
365
|
+
if (!this._workspace.isDirectory && displayName.endsWith(`.${this.workspaceSuffixName}`)) {
|
|
366
|
+
title = formatLocalize(
|
|
367
|
+
'file.workspace.defaultWorkspaceTip',
|
|
368
|
+
displayName.slice(0, displayName.lastIndexOf('.')),
|
|
369
|
+
);
|
|
370
|
+
} else {
|
|
371
|
+
title = displayName;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
document.title = this.formatTitle(title);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async getMostRecentlyUsedWorkspace(): Promise<string | undefined> {
|
|
378
|
+
await this.getGlobalRecentStorage();
|
|
379
|
+
const recentWorkspaces: string[] = (await this.recentGlobalStorage.get<string[]>('RECENT_WORKSPACES')) || [];
|
|
380
|
+
return recentWorkspaces[0];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async setMostRecentlyUsedWorkspace(path: string) {
|
|
384
|
+
await this.getGlobalRecentStorage();
|
|
385
|
+
const recentWorkspaces: string[] = (await this.recentGlobalStorage.get<string[]>('RECENT_WORKSPACES')) || [];
|
|
386
|
+
recentWorkspaces.unshift(path);
|
|
387
|
+
await this.recentGlobalStorage.set('RECENT_WORKSPACES', Array.from(new Set(recentWorkspaces)));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async getMostRecentlyUsedWorkspaces(): Promise<string[]> {
|
|
391
|
+
await this.getGlobalRecentStorage();
|
|
392
|
+
const recentWorkspaces: string[] = (await this.recentGlobalStorage.get<string[]>('RECENT_WORKSPACES')) || [];
|
|
393
|
+
return recentWorkspaces;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async getMostRecentlyUsedCommands(): Promise<string[]> {
|
|
397
|
+
await this.getGlobalRecentStorage();
|
|
398
|
+
const recentCommands: string[] = (await this.recentGlobalStorage.get<string[]>('RECENT_COMMANDS')) || [];
|
|
399
|
+
return recentCommands;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async setMostRecentlyUsedCommand(commandId: string) {
|
|
403
|
+
await this.getGlobalRecentStorage();
|
|
404
|
+
const recentCommands: string[] = (await this.recentGlobalStorage.get<string[]>('RECENT_COMMANDS')) || [];
|
|
405
|
+
const commandIndex = recentCommands.indexOf(commandId);
|
|
406
|
+
// 重新排到队列顶部
|
|
407
|
+
if (commandIndex > 0) {
|
|
408
|
+
recentCommands.splice(commandIndex, 1);
|
|
409
|
+
}
|
|
410
|
+
recentCommands.unshift(commandId);
|
|
411
|
+
await this.recentGlobalStorage.set('RECENT_COMMANDS', recentCommands);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private async getGlobalRecentStorage() {
|
|
415
|
+
this.recentGlobalStorage =
|
|
416
|
+
this.recentGlobalStorage || (await this.storageProvider(STORAGE_NAMESPACE.GLOBAL_RECENT_DATA));
|
|
417
|
+
await this.recentGlobalStorage.whenReady;
|
|
418
|
+
return this.recentGlobalStorage;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 当已经存在打开的工作区时,返回true
|
|
423
|
+
* @returns {boolean}
|
|
424
|
+
*/
|
|
425
|
+
get opened(): boolean {
|
|
426
|
+
return !!this._workspace;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* 当一个混合工作区打开时,返回 true
|
|
431
|
+
* @returns {boolean}
|
|
432
|
+
*/
|
|
433
|
+
get isMultiRootWorkspaceOpened(): boolean {
|
|
434
|
+
return !!this.workspace && !this.workspace.isDirectory;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 当前存在打开的工作区同时支持混合工作区时,返回true
|
|
439
|
+
* @returns {boolean}
|
|
440
|
+
*/
|
|
441
|
+
get isMultiRootWorkspaceEnabled(): boolean {
|
|
442
|
+
return this.opened && this.preferences['workspace.supportMultiRootWorkspace'];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 打开一个文件夹或创建一个工作区
|
|
447
|
+
* @param {URI} uri
|
|
448
|
+
* @param {WorkspaceInput} [options]
|
|
449
|
+
* @memberof WorkspaceService
|
|
450
|
+
*/
|
|
451
|
+
async open(uri: URI, options?: WorkspaceInput) {
|
|
452
|
+
await this.doOpen(uri, options);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 需要判断是否在当前工作区打开窗口
|
|
457
|
+
* 在这里传入的options优先级高于preference设置
|
|
458
|
+
* @protected
|
|
459
|
+
* @param {URI} uri
|
|
460
|
+
* @param {WorkspaceInput} [options]
|
|
461
|
+
* @returns {Promise<void>}
|
|
462
|
+
* @memberof WorkspaceService
|
|
463
|
+
*/
|
|
464
|
+
protected async doOpen(uri: URI, options?: WorkspaceInput): Promise<void> {
|
|
465
|
+
const rootUri = uri.toString();
|
|
466
|
+
const stat = await this.toFileStat(rootUri);
|
|
467
|
+
if (stat) {
|
|
468
|
+
await this.roots;
|
|
469
|
+
const { preserveWindow } = {
|
|
470
|
+
preserveWindow: this.preferences['workspace.preserveWindow'] || !this.opened,
|
|
471
|
+
...options,
|
|
472
|
+
};
|
|
473
|
+
if (preserveWindow) {
|
|
474
|
+
this._workspace = stat;
|
|
475
|
+
}
|
|
476
|
+
this.openWindow(stat, { preserveWindow });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
throw new Error('Invalid workspace root URI. Expected an existing directory location.');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* 为工作区设置根目录
|
|
484
|
+
* @param uri
|
|
485
|
+
*/
|
|
486
|
+
async addRoot(uri: URI): Promise<void> {
|
|
487
|
+
await this.spliceRoots(this._roots.length, 0, {}, uri);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* 工作区中移除对应根目录
|
|
492
|
+
*/
|
|
493
|
+
async removeRoots(uris: URI[]): Promise<void> {
|
|
494
|
+
if (!this.opened) {
|
|
495
|
+
throw new Error('Folder cannot be removed as there is no active folder in the current workspace.');
|
|
496
|
+
}
|
|
497
|
+
if (this._workspace) {
|
|
498
|
+
const workspaceData = await this.getWorkspaceDataFromFile();
|
|
499
|
+
this._workspace = await this.writeWorkspaceFile(
|
|
500
|
+
this._workspace,
|
|
501
|
+
WorkspaceData.buildWorkspaceData(
|
|
502
|
+
this._roots.filter((root) => uris.findIndex((u) => u.toString() === root.uri) < 0),
|
|
503
|
+
workspaceData!.settings,
|
|
504
|
+
),
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async spliceRoots(
|
|
510
|
+
start: number,
|
|
511
|
+
deleteCount = 0,
|
|
512
|
+
workspaceToName: { [key: string]: string } = {},
|
|
513
|
+
...rootsToAdd: URI[]
|
|
514
|
+
): Promise<URI[]> {
|
|
515
|
+
if (!this._workspace) {
|
|
516
|
+
throw new Error('There is not active workspace');
|
|
517
|
+
}
|
|
518
|
+
const dedup = new Set<string>();
|
|
519
|
+
const roots = this._roots.map((root) => (dedup.add(root.uri), root.uri));
|
|
520
|
+
const toAdd: string[] = [];
|
|
521
|
+
// 更新工作区映射
|
|
522
|
+
for (const uri of Object.keys(workspaceToName)) {
|
|
523
|
+
this.workspaceToName[new URI(uri).toString()] = workspaceToName[uri];
|
|
524
|
+
}
|
|
525
|
+
for (const root of rootsToAdd) {
|
|
526
|
+
const uri = root.toString();
|
|
527
|
+
if (!dedup.has(uri)) {
|
|
528
|
+
dedup.add(uri);
|
|
529
|
+
toAdd.push(uri);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const toRemove = roots.splice(start, deleteCount || 0, ...toAdd);
|
|
533
|
+
if (!toRemove.length && !toAdd.length) {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
if (this._workspace.isDirectory) {
|
|
537
|
+
const untitledWorkspace = await this.getUntitledWorkspace();
|
|
538
|
+
if (untitledWorkspace) {
|
|
539
|
+
await this.save(untitledWorkspace);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const currentData = await this.getWorkspaceDataFromFile();
|
|
543
|
+
const newData = WorkspaceData.buildWorkspaceData(roots, currentData && currentData.settings);
|
|
544
|
+
await this.writeWorkspaceFile(this._workspace, newData);
|
|
545
|
+
return toRemove.map((root) => new URI(root));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
public getWorkspaceName(uri: URI) {
|
|
549
|
+
return (
|
|
550
|
+
this.workspaceToName[uri.toString()] || this.workspaceToName[uri.toString() + Path.separator] || uri.displayName
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
protected async getUntitledWorkspace(): Promise<URI | undefined> {
|
|
555
|
+
const home = await this.fileServiceClient.getCurrentUserHome();
|
|
556
|
+
return home && this.getTemporaryWorkspaceFileUri(new URI(home.uri));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private async writeWorkspaceFile(
|
|
560
|
+
workspaceFile: FileStat | undefined,
|
|
561
|
+
workspaceData: WorkspaceData,
|
|
562
|
+
): Promise<FileStat | undefined> {
|
|
563
|
+
if (workspaceFile) {
|
|
564
|
+
const data = JSON.stringify(WorkspaceData.transformToRelative(workspaceData, workspaceFile));
|
|
565
|
+
const edits = jsoncparser.format(data, undefined, { tabSize: 2, insertSpaces: true, eol: '' });
|
|
566
|
+
const result = jsoncparser.applyEdits(data, edits);
|
|
567
|
+
const stat = await this.fileServiceClient.setContent(workspaceFile, result);
|
|
568
|
+
if (!stat) {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
return stat;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* 清理当前workspace
|
|
577
|
+
*/
|
|
578
|
+
async close(): Promise<void> {
|
|
579
|
+
this._workspace = undefined;
|
|
580
|
+
this._roots.length = 0;
|
|
581
|
+
this.reloadWindow();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 验证给定的URI是否为有效根目录
|
|
586
|
+
*/
|
|
587
|
+
protected async toValidRoot(uri: URI | string | undefined): Promise<FileStat | undefined> {
|
|
588
|
+
const fileStat = await this.toFileStat(uri);
|
|
589
|
+
if (fileStat && fileStat.isDirectory) {
|
|
590
|
+
return fileStat;
|
|
591
|
+
}
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* 返回文件的FileStat
|
|
597
|
+
*/
|
|
598
|
+
protected async toFileStat(uri: URI | string | undefined): Promise<FileStat | undefined> {
|
|
599
|
+
if (!uri) {
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
let uriStr = uri.toString();
|
|
603
|
+
try {
|
|
604
|
+
if (uriStr.endsWith('/')) {
|
|
605
|
+
uriStr = uriStr.slice(0, -1);
|
|
606
|
+
}
|
|
607
|
+
const fileStat = await this.fileServiceClient.getFileStat(uriStr);
|
|
608
|
+
if (!fileStat) {
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
return fileStat;
|
|
612
|
+
} catch (error) {
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
protected openWindow(fileStat: FileStat, options?: WorkspaceInput): void {
|
|
618
|
+
const workspacePath = new URI(fileStat.uri).path.toString();
|
|
619
|
+
|
|
620
|
+
if (this.shouldPreserveWindow(options)) {
|
|
621
|
+
this.reloadWindow();
|
|
622
|
+
} else {
|
|
623
|
+
try {
|
|
624
|
+
this.openNewWindow(workspacePath);
|
|
625
|
+
} catch (error) {
|
|
626
|
+
// Fall back to reloading the current window in case the browser has blocked the new window
|
|
627
|
+
this._workspace = fileStat;
|
|
628
|
+
this.logger.error(error.toString());
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
protected reloadWindow(): void {
|
|
634
|
+
this.clientApp.fireOnReload(true);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
protected openNewWindow(workspacePath: string): void {
|
|
638
|
+
const url = new URL(window.location.href);
|
|
639
|
+
url.hash = workspacePath;
|
|
640
|
+
this.windowService.openNewWindow(url.toString());
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
protected shouldPreserveWindow(options?: WorkspaceInput): boolean {
|
|
644
|
+
return options !== undefined && !!options.preserveWindow;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* 返回根目录下是否存在对应相对路径文件
|
|
649
|
+
*/
|
|
650
|
+
async containsSome(paths: string[]): Promise<boolean> {
|
|
651
|
+
await this.roots;
|
|
652
|
+
if (this.opened) {
|
|
653
|
+
for (const root of this._roots) {
|
|
654
|
+
const uri = new URI(root.uri);
|
|
655
|
+
for (const path of paths) {
|
|
656
|
+
const fileUri = uri.resolve(path).toString();
|
|
657
|
+
const exists = await this.fileServiceClient.access(fileUri);
|
|
658
|
+
if (exists) {
|
|
659
|
+
return exists;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
get saved(): boolean {
|
|
668
|
+
return !!this._workspace && !this._workspace.isDirectory;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* 存储工作区数据到文件中
|
|
673
|
+
* @param uri URI or FileStat of the workspace file
|
|
674
|
+
*/
|
|
675
|
+
async save(uri: URI | FileStat): Promise<void> {
|
|
676
|
+
const uriStr = uri instanceof URI ? uri.toString() : uri.uri;
|
|
677
|
+
if (!(await this.fileServiceClient.access(uriStr))) {
|
|
678
|
+
await this.fileServiceClient.createFile(uriStr);
|
|
679
|
+
}
|
|
680
|
+
const workspaceData: WorkspaceData = { folders: [], settings: {} };
|
|
681
|
+
if (!this.saved) {
|
|
682
|
+
for (const p of Object.keys(this.schemaProvider.getCombinedSchema().properties)) {
|
|
683
|
+
if (this.schemaProvider.isValidInScope(p, PreferenceScope.Folder)) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const preferences = this.preferenceService.inspect(p);
|
|
687
|
+
if (preferences && preferences.workspaceValue) {
|
|
688
|
+
workspaceData.settings![p] = preferences.workspaceValue;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
let stat = await this.toFileStat(uriStr);
|
|
693
|
+
Object.assign(workspaceData, await this.getWorkspaceDataFromFile());
|
|
694
|
+
stat = await this.writeWorkspaceFile(
|
|
695
|
+
stat,
|
|
696
|
+
WorkspaceData.buildWorkspaceData(this._roots, workspaceData ? workspaceData.settings : undefined),
|
|
697
|
+
);
|
|
698
|
+
await this.setWorkspace(stat);
|
|
699
|
+
this.onWorkspaceLocationChangedEmitter.fire(stat);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
protected readonly rootWatchers = new Map<string, IDisposable>();
|
|
703
|
+
|
|
704
|
+
// 监听所有根路径变化
|
|
705
|
+
protected async watchRoots(): Promise<void> {
|
|
706
|
+
const rootUris = new Set(this._roots.map((r) => r.uri));
|
|
707
|
+
for (const [uri, watcher] of this.rootWatchers.entries()) {
|
|
708
|
+
if (!rootUris.has(uri)) {
|
|
709
|
+
watcher.dispose();
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
for (const root of this._roots) {
|
|
713
|
+
this.watchRoot(root);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// 监听根路径变化
|
|
718
|
+
protected async watchRoot(root: FileStat): Promise<void> {
|
|
719
|
+
const uriStr = root.uri;
|
|
720
|
+
if (this.rootWatchers.has(uriStr)) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const watcher = this.fileServiceClient.watchFileChanges(new URI(root.uri));
|
|
724
|
+
this.rootWatchers.set(
|
|
725
|
+
uriStr,
|
|
726
|
+
Disposable.create(() => {
|
|
727
|
+
watcher.then((disposable) => disposable.dispose());
|
|
728
|
+
this.rootWatchers.delete(uriStr);
|
|
729
|
+
}),
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* 根据给定的uri获取其根节点
|
|
735
|
+
* 如果不指定uri,则获取默认的根节点
|
|
736
|
+
* @param uri
|
|
737
|
+
*/
|
|
738
|
+
getWorkspaceRootUri(uri: URI | undefined): URI | undefined {
|
|
739
|
+
// 获取非file协议文件的根目录,默认返回第一个根目录或undefined
|
|
740
|
+
if (!uri || uri.scheme !== Schemes.file) {
|
|
741
|
+
const root = this.tryGetRoots()[0];
|
|
742
|
+
if (root) {
|
|
743
|
+
return new URI(root.uri);
|
|
744
|
+
}
|
|
745
|
+
return undefined;
|
|
746
|
+
}
|
|
747
|
+
const rootUris: URI[] = [];
|
|
748
|
+
for (const root of this.tryGetRoots()) {
|
|
749
|
+
const rootUri = new URI(root.uri);
|
|
750
|
+
if (rootUri && rootUri.isEqualOrParent(uri)) {
|
|
751
|
+
rootUris.push(rootUri);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return rootUris.sort((r1, r2) => r2.toString().length - r1.toString().length)[0];
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* 获取相对路径
|
|
759
|
+
* @param pathOrUri
|
|
760
|
+
* @param includeWorkspaceFolder
|
|
761
|
+
*/
|
|
762
|
+
async asRelativePath(pathOrUri: string | URI, includeWorkspaceFolder?: boolean) {
|
|
763
|
+
// path 为 uri.path 非 uri.toString()
|
|
764
|
+
let path: string | undefined;
|
|
765
|
+
let root: string | undefined;
|
|
766
|
+
if (typeof pathOrUri === 'string') {
|
|
767
|
+
path = pathOrUri;
|
|
768
|
+
} else if (typeof pathOrUri !== 'undefined') {
|
|
769
|
+
path = pathOrUri.codeUri.fsPath;
|
|
770
|
+
}
|
|
771
|
+
if (!path) {
|
|
772
|
+
return {
|
|
773
|
+
path,
|
|
774
|
+
root,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const roots = await this.roots;
|
|
778
|
+
if (includeWorkspaceFolder && this.isMultiRootWorkspaceOpened) {
|
|
779
|
+
const workspace = await this.workspace;
|
|
780
|
+
if (workspace) {
|
|
781
|
+
roots.push(workspace);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
for (const r of roots) {
|
|
785
|
+
const rootPath = new URI(r.uri).codeUri.fsPath;
|
|
786
|
+
const isRelative = path && path.indexOf(rootPath) >= 0;
|
|
787
|
+
if (isRelative) {
|
|
788
|
+
root = rootPath;
|
|
789
|
+
if (path === rootPath) {
|
|
790
|
+
path = '';
|
|
791
|
+
}
|
|
792
|
+
if (rootPath.slice(-1) === '/') {
|
|
793
|
+
path = path.replace(rootPath, '');
|
|
794
|
+
} else {
|
|
795
|
+
path = path.replace(rootPath + '/', '');
|
|
796
|
+
}
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
path: decodeURI(path),
|
|
802
|
+
root,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
dispose() {
|
|
807
|
+
this.toDisposableCollection.dispose();
|
|
808
|
+
}
|
|
809
|
+
}
|