提交 e43be978 编写于 作者: S Sandeep Somavarapu

#54483 Prepare renderer to accept folders as URIs

上级 32bebd31
...@@ -29,6 +29,11 @@ export const UNTITLED_WORKSPACE_NAME = 'workspace.json'; ...@@ -29,6 +29,11 @@ export const UNTITLED_WORKSPACE_NAME = 'workspace.json';
*/ */
export type ISingleFolderWorkspaceIdentifier = string; export type ISingleFolderWorkspaceIdentifier = string;
/**
* A single folder workspace identifier is just the folder URI
*/
export type ISingleFolderWorkspaceIdentifier2 = URI;
export interface IWorkspaceIdentifier { export interface IWorkspaceIdentifier {
id: string; id: string;
configPath: string; configPath: string;
...@@ -137,6 +142,10 @@ export function isSingleFolderWorkspaceIdentifier(obj: any): obj is ISingleFolde ...@@ -137,6 +142,10 @@ export function isSingleFolderWorkspaceIdentifier(obj: any): obj is ISingleFolde
return typeof obj === 'string'; return typeof obj === 'string';
} }
export function isSingleFolderWorkspaceIdentifier2(obj: any): obj is ISingleFolderWorkspaceIdentifier2 {
return obj instanceof URI;
}
export function isWorkspaceIdentifier(obj: any): obj is IWorkspaceIdentifier { export function isWorkspaceIdentifier(obj: any): obj is IWorkspaceIdentifier {
const workspaceIdentifier = obj as IWorkspaceIdentifier; const workspaceIdentifier = obj as IWorkspaceIdentifier;
......
...@@ -49,6 +49,7 @@ import { LogLevelSetterChannelClient, FollowerLogService } from 'vs/platform/log ...@@ -49,6 +49,7 @@ import { LogLevelSetterChannelClient, FollowerLogService } from 'vs/platform/log
import { RelayURLService } from 'vs/platform/url/common/urlService'; import { RelayURLService } from 'vs/platform/url/common/urlService';
import { MenubarChannelClient } from 'vs/platform/menubar/common/menubarIpc'; import { MenubarChannelClient } from 'vs/platform/menubar/common/menubarIpc';
import { IMenubarService } from 'vs/platform/menubar/common/menubar'; import { IMenubarService } from 'vs/platform/menubar/common/menubar';
import { Schemas } from 'vs/base/common/network';
gracefulFs.gracefulify(fs); // enable gracefulFs gracefulFs.gracefulify(fs); // enable gracefulFs
...@@ -115,22 +116,24 @@ function openWorkbench(configuration: IWindowConfiguration): TPromise<void> { ...@@ -115,22 +116,24 @@ function openWorkbench(configuration: IWindowConfiguration): TPromise<void> {
} }
function createAndInitializeWorkspaceService(configuration: IWindowConfiguration, environmentService: EnvironmentService): TPromise<WorkspaceService> { function createAndInitializeWorkspaceService(configuration: IWindowConfiguration, environmentService: EnvironmentService): TPromise<WorkspaceService> {
return validateSingleFolderPath(configuration).then(() => { const folderUri = configuration.folderPath ? uri.file(configuration.folderPath) /* TODO: Change to URI.path parsing once main sends URIs*/ : null;
return validateFolderUri(folderUri, configuration.verbose).then(validatedFolderUri => {
const workspaceService = new WorkspaceService(environmentService); const workspaceService = new WorkspaceService(environmentService);
return workspaceService.initialize(configuration.workspace || configuration.folderPath || configuration).then(() => workspaceService, error => workspaceService); return workspaceService.initialize(configuration.workspace || validatedFolderUri || configuration).then(() => workspaceService, error => workspaceService);
}); });
} }
function validateSingleFolderPath(configuration: IWindowConfiguration): TPromise<void> { function validateFolderUri(folderUri: uri, verbose: boolean): TPromise<uri> {
// Return early if we do not have a single folder path // Return early if we do not have a single folder uri or if it is a non file uri
if (!configuration.folderPath) { if (!folderUri || folderUri.scheme !== Schemas.file) {
return TPromise.as(void 0); return TPromise.as(folderUri);
} }
// Otherwise: use realpath to resolve symbolic links to the truth // Otherwise: use realpath to resolve symbolic links to the truth
return realpath(configuration.folderPath).then(realFolderPath => { return realpath(folderUri.fsPath).then(realFolderPath => {
// For some weird reason, node adds a trailing slash to UNC paths // For some weird reason, node adds a trailing slash to UNC paths
// we never ever want trailing slashes as our workspace path unless // we never ever want trailing slashes as our workspace path unless
...@@ -140,19 +143,19 @@ function validateSingleFolderPath(configuration: IWindowConfiguration): TPromise ...@@ -140,19 +143,19 @@ function validateSingleFolderPath(configuration: IWindowConfiguration): TPromise
realFolderPath = strings.rtrim(realFolderPath, paths.nativeSep); realFolderPath = strings.rtrim(realFolderPath, paths.nativeSep);
} }
return realFolderPath; return uri.file(realFolderPath);
}, error => { }, error => {
if (configuration.verbose) { if (verbose) {
errors.onUnexpectedError(error); errors.onUnexpectedError(error);
} }
// Treat any error case as empty workbench case (no folder path) // Treat any error case as empty workbench case (no folder path)
return null; return null;
}).then(realFolderPathOrNull => { }).then(realFolderUriOrNull => {
// Update config with real path if we have one // Update config with real path if we have one
configuration.folderPath = realFolderPathOrNull; return realFolderUriOrNull;
}); });
} }
......
...@@ -26,7 +26,7 @@ import { IWorkspaceConfigurationService, FOLDER_CONFIG_FOLDER_NAME, defaultSetti ...@@ -26,7 +26,7 @@ import { IWorkspaceConfigurationService, FOLDER_CONFIG_FOLDER_NAME, defaultSetti
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry'; import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { getWorkspaceLabel, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { getWorkspaceLabel, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier2, isSingleFolderWorkspaceIdentifier2 } from 'vs/platform/workspaces/common/workspaces';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICommandService } from 'vs/platform/commands/common/commands';
...@@ -41,6 +41,7 @@ import { UserConfiguration } from 'vs/platform/configuration/node/configuration' ...@@ -41,6 +41,7 @@ import { UserConfiguration } from 'vs/platform/configuration/node/configuration'
import { getBaseLabel } from 'vs/base/common/labels'; import { getBaseLabel } from 'vs/base/common/labels';
import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { isEqual } from 'vs/base/common/resources';
export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService { export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService {
...@@ -131,13 +132,18 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat ...@@ -131,13 +132,18 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean {
switch (this.getWorkbenchState()) { switch (this.getWorkbenchState()) {
case WorkbenchState.FOLDER: case WorkbenchState.FOLDER:
return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && this.pathEquals(this.workspace.folders[0].uri.fsPath, workspaceIdentifier); return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && isEqual(this.workspace.folders[0].uri, this.toSingleFolderWorkspaceIdentifier2(workspaceIdentifier), this.workspace.folders[0].uri.scheme !== Schemas.file || !isLinux);
case WorkbenchState.WORKSPACE: case WorkbenchState.WORKSPACE:
return isWorkspaceIdentifier(workspaceIdentifier) && this.workspace.id === workspaceIdentifier.id; return isWorkspaceIdentifier(workspaceIdentifier) && this.workspace.id === workspaceIdentifier.id;
} }
return false; return false;
} }
private toSingleFolderWorkspaceIdentifier2(folderIdentifier: ISingleFolderWorkspaceIdentifier): URI {
const uri = URI.parse(folderIdentifier);
return uri.scheme ? uri : URI.file(folderIdentifier);
}
private doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToRemove: URI[], index?: number): TPromise<void> { private doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToRemove: URI[], index?: number): TPromise<void> {
if (this.getWorkbenchState() !== WorkbenchState.WORKSPACE) { if (this.getWorkbenchState() !== WorkbenchState.WORKSPACE) {
return TPromise.as(void 0); // we need a workspace to begin with return TPromise.as(void 0); // we need a workspace to begin with
...@@ -295,7 +301,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat ...@@ -295,7 +301,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
return this._configuration.keys(); return this._configuration.keys();
} }
initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration, postInitialisationTask: () => void = () => null): TPromise<any> { initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier2 | IWindowConfiguration, postInitialisationTask: () => void = () => null): TPromise<any> {
return this.createWorkspace(arg) return this.createWorkspace(arg)
.then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace, postInitialisationTask)); .then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace, postInitialisationTask));
} }
...@@ -322,12 +328,12 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat ...@@ -322,12 +328,12 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
this.jsonEditingService = instantiationService.createInstance(JSONEditingService); this.jsonEditingService = instantiationService.createInstance(JSONEditingService);
} }
private createWorkspace(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise<Workspace> { private createWorkspace(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier2 | IWindowConfiguration): TPromise<Workspace> {
if (isWorkspaceIdentifier(arg)) { if (isWorkspaceIdentifier(arg)) {
return this.createMulitFolderWorkspace(arg); return this.createMulitFolderWorkspace(arg);
} }
if (isSingleFolderWorkspaceIdentifier(arg)) { if (isSingleFolderWorkspaceIdentifier2(arg)) {
return this.createSingleFolderWorkspace(arg); return this.createSingleFolderWorkspace(arg);
} }
...@@ -345,15 +351,18 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat ...@@ -345,15 +351,18 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
}); });
} }
private createSingleFolderWorkspace(singleFolderWorkspaceIdentifier: ISingleFolderWorkspaceIdentifier): TPromise<Workspace> { private createSingleFolderWorkspace(folder: ISingleFolderWorkspaceIdentifier2): TPromise<Workspace> {
const folderPath = URI.file(singleFolderWorkspaceIdentifier); if (folder.scheme === Schemas.file) {
return stat(folderPath.fsPath) return stat(folder.fsPath)
.then(workspaceStat => { .then(workspaceStat => {
const ctime = isLinux ? workspaceStat.ino : workspaceStat.birthtime.getTime(); // On Linux, birthtime is ctime, so we cannot use it! We use the ino instead! const ctime = isLinux ? workspaceStat.ino : workspaceStat.birthtime.getTime(); // On Linux, birthtime is ctime, so we cannot use it! We use the ino instead!
const id = createHash('md5').update(folderPath.fsPath).update(ctime ? String(ctime) : '').digest('hex'); const id = createHash('md5').update(folder.fsPath).update(ctime ? String(ctime) : '').digest('hex');
const folder = URI.file(folderPath.fsPath); return new Workspace(id, getBaseLabel(folder), toWorkspaceFolders([{ path: folder.fsPath }]), null, ctime);
return new Workspace(id, getBaseLabel(folder), toWorkspaceFolders([{ path: folder.fsPath }]), null, ctime); });
}); } else {
const id = createHash('md5').update(folder.toString()).digest('hex');
return TPromise.as(new Workspace(id, getBaseLabel(folder), toWorkspaceFolders([{ uri: folder.toString() }]), null));
}
} }
private createEmptyWorkspace(configuration: IWindowConfiguration): TPromise<Workspace> { private createEmptyWorkspace(configuration: IWindowConfiguration): TPromise<Workspace> {
...@@ -670,15 +679,6 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat ...@@ -670,15 +679,6 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
} }
return {}; return {};
} }
private pathEquals(path1: string, path2: string): boolean {
if (!isLinux) {
path1 = path1.toLowerCase();
path2 = path2.toLowerCase();
}
return path1 === path2;
}
} }
interface IExportedConfigurationNode { interface IExportedConfigurationNode {
......
...@@ -38,6 +38,7 @@ import { mkdirp } from 'vs/base/node/pfs'; ...@@ -38,6 +38,7 @@ import { mkdirp } from 'vs/base/node/pfs';
import { INotificationService } from 'vs/platform/notification/common/notification'; import { INotificationService } from 'vs/platform/notification/common/notification';
import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICommandService } from 'vs/platform/commands/common/commands';
import { CommandService } from 'vs/workbench/services/commands/common/commandService'; import { CommandService } from 'vs/workbench/services/commands/common/commandService';
import URI from 'vs/base/common/uri';
class SettingsTestEnvironmentService extends EnvironmentService { class SettingsTestEnvironmentService extends EnvironmentService {
...@@ -103,7 +104,7 @@ suite('ConfigurationEditingService', () => { ...@@ -103,7 +104,7 @@ suite('ConfigurationEditingService', () => {
instantiationService.stub(IEnvironmentService, environmentService); instantiationService.stub(IEnvironmentService, environmentService);
const workspaceService = new WorkspaceService(environmentService); const workspaceService = new WorkspaceService(environmentService);
instantiationService.stub(IWorkspaceContextService, workspaceService); instantiationService.stub(IWorkspaceContextService, workspaceService);
return workspaceService.initialize(noWorkspace ? {} as IWindowConfiguration : workspaceDir).then(() => { return workspaceService.initialize(noWorkspace ? {} as IWindowConfiguration : URI.file(workspaceDir)).then(() => {
instantiationService.stub(IConfigurationService, workspaceService); instantiationService.stub(IConfigurationService, workspaceService);
instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }));
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
......
...@@ -87,7 +87,7 @@ suite('WorkspaceContextService - Folder', () => { ...@@ -87,7 +87,7 @@ suite('WorkspaceContextService - Folder', () => {
const globalSettingsFile = path.join(parentDir, 'settings.json'); const globalSettingsFile = path.join(parentDir, 'settings.json');
const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile); const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile);
workspaceContextService = new WorkspaceService(environmentService); workspaceContextService = new WorkspaceService(environmentService);
return (<WorkspaceService>workspaceContextService).initialize(folderDir); return (<WorkspaceService>workspaceContextService).initialize(URI.file(folderDir));
}); });
}); });
...@@ -445,7 +445,7 @@ suite('WorkspaceService - Initialization', () => { ...@@ -445,7 +445,7 @@ suite('WorkspaceService - Initialization', () => {
testObject.onDidChangeWorkspaceFolders(target); testObject.onDidChangeWorkspaceFolders(target);
testObject.onDidChangeConfiguration(target); testObject.onDidChangeConfiguration(target);
return testObject.initialize(path.join(parentResource, '1')) return testObject.initialize(URI.file(path.join(parentResource, '1')))
.then(() => { .then(() => {
assert.equal(testObject.getValue('initialization.testSetting1'), 'userValue'); assert.equal(testObject.getValue('initialization.testSetting1'), 'userValue');
assert.equal(target.callCount, 3); assert.equal(target.callCount, 3);
...@@ -474,7 +474,7 @@ suite('WorkspaceService - Initialization', () => { ...@@ -474,7 +474,7 @@ suite('WorkspaceService - Initialization', () => {
fs.writeFileSync(path.join(parentResource, '1', '.vscode', 'settings.json'), '{ "initialization.testSetting1": "workspaceValue" }'); fs.writeFileSync(path.join(parentResource, '1', '.vscode', 'settings.json'), '{ "initialization.testSetting1": "workspaceValue" }');
return testObject.initialize(path.join(parentResource, '1')) return testObject.initialize(URI.file(path.join(parentResource, '1')))
.then(() => { .then(() => {
assert.equal(testObject.getValue('initialization.testSetting1'), 'workspaceValue'); assert.equal(testObject.getValue('initialization.testSetting1'), 'workspaceValue');
assert.equal(target.callCount, 4); assert.equal(target.callCount, 4);
...@@ -548,7 +548,7 @@ suite('WorkspaceService - Initialization', () => { ...@@ -548,7 +548,7 @@ suite('WorkspaceService - Initialization', () => {
test('initialize a folder workspace from a folder workspace with no configuration changes', () => { test('initialize a folder workspace from a folder workspace with no configuration changes', () => {
return testObject.initialize(path.join(parentResource, '1')) return testObject.initialize(URI.file(path.join(parentResource, '1')))
.then(() => { .then(() => {
fs.writeFileSync(globalSettingsFile, '{ "initialization.testSetting1": "userValue" }'); fs.writeFileSync(globalSettingsFile, '{ "initialization.testSetting1": "userValue" }');
...@@ -560,7 +560,7 @@ suite('WorkspaceService - Initialization', () => { ...@@ -560,7 +560,7 @@ suite('WorkspaceService - Initialization', () => {
testObject.onDidChangeWorkspaceFolders(target); testObject.onDidChangeWorkspaceFolders(target);
testObject.onDidChangeConfiguration(target); testObject.onDidChangeConfiguration(target);
return testObject.initialize(path.join(parentResource, '2')) return testObject.initialize(URI.file(path.join(parentResource, '2')))
.then(() => { .then(() => {
assert.equal(testObject.getValue('initialization.testSetting1'), 'userValue'); assert.equal(testObject.getValue('initialization.testSetting1'), 'userValue');
assert.equal(target.callCount, 1); assert.equal(target.callCount, 1);
...@@ -576,7 +576,7 @@ suite('WorkspaceService - Initialization', () => { ...@@ -576,7 +576,7 @@ suite('WorkspaceService - Initialization', () => {
test('initialize a folder workspace from a folder workspace with configuration changes', () => { test('initialize a folder workspace from a folder workspace with configuration changes', () => {
return testObject.initialize(path.join(parentResource, '1')) return testObject.initialize(URI.file(path.join(parentResource, '1')))
.then(() => { .then(() => {
const target = sinon.spy(); const target = sinon.spy();
...@@ -586,7 +586,7 @@ suite('WorkspaceService - Initialization', () => { ...@@ -586,7 +586,7 @@ suite('WorkspaceService - Initialization', () => {
testObject.onDidChangeConfiguration(target); testObject.onDidChangeConfiguration(target);
fs.writeFileSync(path.join(parentResource, '2', '.vscode', 'settings.json'), '{ "initialization.testSetting1": "workspaceValue2" }'); fs.writeFileSync(path.join(parentResource, '2', '.vscode', 'settings.json'), '{ "initialization.testSetting1": "workspaceValue2" }');
return testObject.initialize(path.join(parentResource, '2')) return testObject.initialize(URI.file(path.join(parentResource, '2')))
.then(() => { .then(() => {
assert.equal(testObject.getValue('initialization.testSetting1'), 'workspaceValue2'); assert.equal(testObject.getValue('initialization.testSetting1'), 'workspaceValue2');
assert.equal(target.callCount, 2); assert.equal(target.callCount, 2);
...@@ -601,7 +601,7 @@ suite('WorkspaceService - Initialization', () => { ...@@ -601,7 +601,7 @@ suite('WorkspaceService - Initialization', () => {
test('initialize a multi folder workspace from a folder workspacce triggers change events in the right order', () => { test('initialize a multi folder workspace from a folder workspacce triggers change events in the right order', () => {
const folderDir = path.join(parentResource, '1'); const folderDir = path.join(parentResource, '1');
return testObject.initialize(folderDir) return testObject.initialize(URI.file(folderDir))
.then(() => { .then(() => {
const target = sinon.spy(); const target = sinon.spy();
...@@ -666,7 +666,7 @@ suite('WorkspaceConfigurationService - Folder', () => { ...@@ -666,7 +666,7 @@ suite('WorkspaceConfigurationService - Folder', () => {
instantiationService.stub(IConfigurationService, workspaceService); instantiationService.stub(IConfigurationService, workspaceService);
instantiationService.stub(IEnvironmentService, environmentService); instantiationService.stub(IEnvironmentService, environmentService);
return workspaceService.initialize(folderDir).then(() => { return workspaceService.initialize(URI.file(folderDir)).then(() => {
const fileService = new FileService(<IWorkspaceContextService>workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); const fileService = new FileService(<IWorkspaceContextService>workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true });
instantiationService.stub(IFileService, fileService); instantiationService.stub(IFileService, fileService);
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册