提交 d77d7d29 编写于 作者: M Martin Aeschlimann

workspaceHome -> untitledWorkspacesHome, is URI

上级 b34154e0
......@@ -531,7 +531,7 @@ export class WindowsManager implements IWindowsMainService {
newWindow: openFilesInNewWindow,
context: openConfig.context,
fileUri: fileToCheck && fileToCheck.fileUri,
workspaceResolver: workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesMainService.resolveWorkspaceSync(fsPath(workspace.configPath)) : null
localWorkspaceResolver: workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesMainService.resolveLocalWorkspaceSync(workspace.configPath) : null
});
// We found a window to open the files in
......@@ -1051,7 +1051,7 @@ export class WindowsManager implements IWindowsMainService {
// Workspace (unless disabled via flag)
if (!options.forceOpenWorkspaceAsFile) {
const workspace = this.workspacesMainService.resolveWorkspaceSync(candidate);
const workspace = this.workspacesMainService.resolveLocalWorkspaceSync(URI.file(candidate));
if (workspace) {
return { workspace: { id: workspace.id, configPath: workspace.configPath }, remoteAuthority };
}
......@@ -2128,7 +2128,7 @@ class WorkspacesManager {
return workspace.scheme === Schemas.file ? dirname(workspace.fsPath) : undefined;
}
const resolvedWorkspace = workspace.configPath.scheme === Schemas.file && this.workspacesMainService.resolveWorkspaceSync(workspace.configPath.fsPath);
const resolvedWorkspace = workspace.configPath.scheme === Schemas.file && this.workspacesMainService.resolveLocalWorkspaceSync(workspace.configPath);
if (resolvedWorkspace && resolvedWorkspace.folders.length > 0) {
for (const folder of resolvedWorkspace.folders) {
if (folder.uri.scheme === Schemas.file) {
......
......@@ -25,10 +25,10 @@ export interface IBestWindowOrFolderOptions<W extends ISimpleWindow> {
fileUri?: URI;
userHome?: string;
codeSettingsFolder?: string;
workspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null;
localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null;
}
export function findBestWindowOrFolderForFile<W extends ISimpleWindow>({ windows, newWindow, context, fileUri, workspaceResolver }: IBestWindowOrFolderOptions<W>): W | undefined {
export function findBestWindowOrFolderForFile<W extends ISimpleWindow>({ windows, newWindow, context, fileUri, localWorkspaceResolver: workspaceResolver }: IBestWindowOrFolderOptions<W>): W | undefined {
if (!newWindow && fileUri && (context === OpenContext.DESKTOP || context === OpenContext.CLI || context === OpenContext.DOCK)) {
const windowOnFilePath = findWindowOnFilePath(windows, fileUri, workspaceResolver);
if (windowOnFilePath) {
......@@ -38,15 +38,15 @@ export function findBestWindowOrFolderForFile<W extends ISimpleWindow>({ windows
return !newWindow ? getLastActiveWindow(windows) : undefined;
}
function findWindowOnFilePath<W extends ISimpleWindow>(windows: W[], fileUri: URI, workspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): W | null {
function findWindowOnFilePath<W extends ISimpleWindow>(windows: W[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): W | null {
// First check for windows with workspaces that have a parent folder of the provided path opened
for (const window of windows) {
const workspace = window.openedWorkspace;
if (workspace) {
const resolvedWorkspace = workspaceResolver(workspace);
const resolvedWorkspace = localWorkspaceResolver(workspace);
if (resolvedWorkspace) {
// workspace cpuld be resolved: It's in the local file system
// workspace could be resolved: It's in the local file system
if (resolvedWorkspace.folders.some(folder => isEqualOrParent(fileUri, folder.uri))) {
return window;
}
......
......@@ -24,7 +24,7 @@ function options(custom?: Partial<IBestWindowOrFolderOptions<ISimpleWindow>>): I
newWindow: false,
context: OpenContext.CLI,
codeSettingsFolder: '_vscode',
workspaceResolver: workspace => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }]) } : null!; },
localWorkspaceResolver: workspace => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }]) } : null!; },
...custom
};
}
......
......@@ -109,7 +109,7 @@ export interface IEnvironmentService {
backupHome: string;
backupWorkspacesPath: string;
workspacesHome: string;
untitledWorkspacesHome: URI;
isExtensionDevelopment: boolean;
disableExtensions: boolean | string[];
......
......@@ -135,7 +135,7 @@ export class EnvironmentService implements IEnvironmentService {
get backupWorkspacesPath(): string { return path.join(this.backupHome, 'workspaces.json'); }
@memoize
get workspacesHome(): string { return path.join(this.userDataPath, 'Workspaces'); }
get untitledWorkspacesHome(): URI { return URI.file(path.join(this.userDataPath, 'Workspaces')); }
@memoize
get installSourcePath(): string { return path.join(this.userDataPath, 'installSource'); }
......
......@@ -289,7 +289,7 @@ export class HistoryMainService implements IHistoryMainService {
type: 'custom',
name: nls.localize('recentFolders', "Recent Workspaces"),
items: arrays.coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(workspace => {
const title = getSimpleWorkspaceLabel(workspace, this.environmentService.workspacesHome);
const title = getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome);
let description;
let args;
if (isSingleFolderWorkspaceIdentifier(workspace)) {
......
......@@ -43,12 +43,12 @@ export interface ResourceLabelFormatting {
const LABEL_SERVICE_ID = 'label';
export function getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, workspaceHome: string): string {
export function getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, workspaceHome: URI): string {
if (isSingleFolderWorkspaceIdentifier(workspace)) {
return basename(workspace);
}
// Workspace: Untitled
if (isEqualOrParent(workspace.configPath, URI.file(workspaceHome))) {
if (isEqualOrParent(workspace.configPath, workspaceHome)) {
return localize('untitledWorkspace', "Untitled (Workspace)");
}
......
......@@ -19,7 +19,6 @@ import { BrowserWindow } from 'electron';
import { Event } from 'vs/base/common/event';
import { hasArgs } from 'vs/platform/environment/node/argv';
import { coalesce } from 'vs/base/common/arrays';
import { Schemas } from 'vs/base/common/network';
export const ID = 'launchService';
export const ILaunchService = createDecorator<ILaunchService>(ID);
......@@ -273,7 +272,7 @@ export class LaunchService implements ILaunchService {
} else if (window.openedWorkspace) {
// workspace folders can only be shown for local workspaces
const workspaceConfigPath = window.openedWorkspace.configPath;
const resolvedWorkspace = workspaceConfigPath.scheme === Schemas.file && this.workspacesMainService.resolveWorkspaceSync(workspaceConfigPath.fsPath);
const resolvedWorkspace = this.workspacesMainService.resolveLocalWorkspaceSync(workspaceConfigPath);
if (resolvedWorkspace) {
const rootFolders = resolvedWorkspace.folders;
rootFolders.forEach(root => {
......
......@@ -88,7 +88,7 @@ export interface IWorkspacesMainService extends IWorkspacesService {
createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier;
resolveWorkspaceSync(path: string): IResolvedWorkspace | null;
resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null;
isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean;
......
......@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { IWorkspacesMainService, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces';
import { isParent } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { join, dirname } from 'path';
import { mkdirp, writeFile, readFile } from 'vs/base/node/pfs';
......@@ -21,7 +20,7 @@ import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { Disposable } from 'vs/base/common/lifecycle';
import { fsPath, dirname as resourcesDirname } from 'vs/base/common/resources';
import { fsPath, dirname as resourcesDirname, isEqualOrParent, joinPath } from 'vs/base/common/resources';
export interface IStoredWorkspace {
folders: IStoredWorkspaceFolder[];
......@@ -31,7 +30,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
_serviceBrand: any;
private workspacesHome: string;
private readonly untitledWorkspacesHome: URI; // local URI that contains all untitled workspaces
private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter<IWorkspaceIdentifier>());
get onUntitledWorkspaceDeleted(): Event<IWorkspaceIdentifier> { return this._onUntitledWorkspaceDeleted.event; }
......@@ -42,26 +41,29 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
) {
super();
this.workspacesHome = environmentService.workspacesHome;
this.untitledWorkspacesHome = environmentService.untitledWorkspacesHome;
}
resolveWorkspaceSync(path: string): IResolvedWorkspace | null {
if (!this.isWorkspacePath(path)) {
resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | null {
if (!this.isWorkspacePath(uri)) {
return null; // does not look like a valid workspace config file
}
if (uri.scheme !== Schemas.file) {
return null;
}
let contents: string;
try {
contents = readFileSync(path, 'utf8');
contents = readFileSync(uri.fsPath, 'utf8');
} catch (error) {
return null; // invalid workspace
}
return this.doResolveWorkspace(URI.file(path), contents);
return this.doResolveWorkspace(uri, contents);
}
private isWorkspacePath(path: string): boolean {
return this.isInsideWorkspacesHome(path) || hasWorkspaceFileExtension(path);
private isWorkspacePath(uri: URI): boolean {
return this.isInsideWorkspacesHome(uri) || hasWorkspaceFileExtension(uri.path);
}
private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null {
......@@ -98,36 +100,34 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
return storedWorkspace;
}
private isInsideWorkspacesHome(path: string): boolean {
return isParent(path, this.environmentService.workspacesHome, !isLinux /* ignore case */);
private isInsideWorkspacesHome(path: URI): boolean {
return isEqualOrParent(path, this.environmentService.untitledWorkspacesHome);
}
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise<IWorkspaceIdentifier> {
const { workspace, configParent, storedWorkspace } = this.newUntitledWorkspace(folders);
const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders);
const configPath = workspace.configPath.fsPath;
return mkdirp(configParent).then(() => {
return writeFile(workspace.configPath.fsPath, JSON.stringify(storedWorkspace, null, '\t')).then(() => workspace);
return mkdirp(dirname(configPath)).then(() => {
return writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')).then(() => workspace);
});
}
createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier {
const { workspace, configParent, storedWorkspace } = this.newUntitledWorkspace(folders);
if (!existsSync(this.workspacesHome)) {
mkdirSync(this.workspacesHome);
}
const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders);
const configPath = workspace.configPath.fsPath;
mkdirSync(configParent);
mkdirSync(dirname(configPath), { recursive: true });
writeFileAndFlushSync(workspace.configPath.fsPath, JSON.stringify(storedWorkspace, null, '\t'));
writeFileAndFlushSync(configPath, JSON.stringify(storedWorkspace, null, '\t'));
return workspace;
}
private newUntitledWorkspace(folders: IWorkspaceFolderCreationData[] = []): { workspace: IWorkspaceIdentifier, configParent: string, storedWorkspace: IStoredWorkspace } {
private newUntitledWorkspace(folders: IWorkspaceFolderCreationData[] = []): { workspace: IWorkspaceIdentifier, storedWorkspace: IStoredWorkspace } {
const randomId = (Date.now() + Math.round(Math.random() * 1000)).toString();
const untitledWorkspaceConfigFolder = join(this.workspacesHome, randomId);
const untitledWorkspaceConfigPath = join(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME);
const untitledWorkspaceConfigFolder = joinPath(this.untitledWorkspacesHome, randomId);
const untitledWorkspaceConfigPath = joinPath(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME);
const storedWorkspace: IStoredWorkspace = {
folders: folders.map(folder => {
......@@ -136,7 +136,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
// File URI
if (folderResource.scheme === Schemas.file) {
storedWorkspace = { path: massageFolderPathForWorkspace(fsPath(folderResource), URI.file(untitledWorkspaceConfigFolder), []) };
storedWorkspace = { path: massageFolderPathForWorkspace(fsPath(folderResource), untitledWorkspaceConfigFolder, []) };
}
// Any URI
......@@ -153,8 +153,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
};
return {
workspace: this.getWorkspaceIdentifier(URI.file(untitledWorkspaceConfigPath)),
configParent: untitledWorkspaceConfigFolder,
workspace: this.getWorkspaceIdentifier(untitledWorkspaceConfigPath),
storedWorkspace
};
}
......@@ -176,7 +175,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
}
isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean {
return workspace.configPath.scheme === Schemas.file && this.isInsideWorkspacesHome(fsPath(workspace.configPath));
return this.isInsideWorkspacesHome(workspace.configPath);
}
saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPath: string): Promise<IWorkspaceIdentifier> {
......@@ -234,10 +233,10 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
getUntitledWorkspacesSync(): IWorkspaceIdentifier[] {
let untitledWorkspaces: IWorkspaceIdentifier[] = [];
try {
const untitledWorkspacePaths = readdirSync(this.workspacesHome).map(folder => join(this.workspacesHome, folder, UNTITLED_WORKSPACE_NAME));
const untitledWorkspacePaths = readdirSync(this.untitledWorkspacesHome.fsPath).map(folder => joinPath(this.untitledWorkspacesHome, folder, UNTITLED_WORKSPACE_NAME));
for (const untitledWorkspacePath of untitledWorkspacePaths) {
const workspace = this.getWorkspaceIdentifier(URI.file(untitledWorkspacePath));
if (!this.resolveWorkspaceSync(untitledWorkspacePath)) {
const workspace = this.getWorkspaceIdentifier(untitledWorkspacePath);
if (!this.resolveLocalWorkspaceSync(untitledWorkspacePath)) {
this.doDeleteUntitledWorkspaceSync(workspace);
} else {
untitledWorkspaces.push(workspace);
......@@ -245,7 +244,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
}
} catch (error) {
if (error && error.code !== 'ENOENT') {
this.logService.warn(`Unable to read folders in ${this.workspacesHome} (${error}).`);
this.logService.warn(`Unable to read folders in ${this.untitledWorkspacesHome} (${error}).`);
}
}
return untitledWorkspaces;
......
......@@ -21,11 +21,11 @@ import { normalizeDriveLetter } from 'vs/base/common/labels';
suite('WorkspacesMainService', () => {
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesservice');
const workspacesHome = path.join(parentDir, 'Workspaces');
const untitledWorkspacesHomePath = path.join(parentDir, 'Workspaces');
class TestEnvironmentService extends EnvironmentService {
get workspacesHome(): string {
return workspacesHome;
get untitledWorkspacesHome(): URI {
return URI.file(untitledWorkspacesHomePath);
}
}
......@@ -56,13 +56,13 @@ suite('WorkspacesMainService', () => {
service = new TestWorkspacesMainService(environmentService, logService);
// Delete any existing backups completely and then re-create it.
return pfs.del(workspacesHome, os.tmpdir()).then(() => {
return pfs.mkdirp(workspacesHome);
return pfs.del(untitledWorkspacesHomePath, os.tmpdir()).then(() => {
return pfs.mkdirp(untitledWorkspacesHomePath);
});
});
teardown(() => {
return pfs.del(workspacesHome, os.tmpdir());
return pfs.del(untitledWorkspacesHomePath, os.tmpdir());
});
function assertPathEquals(p1: string, p2): void {
......@@ -173,20 +173,20 @@ suite('WorkspacesMainService', () => {
test('resolveWorkspaceSync', () => {
return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => {
assert.ok(service.resolveWorkspaceSync(workspace.configPath.fsPath));
assert.ok(service.resolveLocalWorkspaceSync(workspace.configPath));
// make it a valid workspace path
const newPath = path.join(path.dirname(workspace.configPath.fsPath), `workspace.${WORKSPACE_EXTENSION}`);
fs.renameSync(workspace.configPath.fsPath, newPath);
workspace.configPath = URI.file(newPath);
const resolved = service.resolveWorkspaceSync(workspace.configPath.fsPath);
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
assert.equal(2, resolved!.folders.length);
assertEqualURI(resolved!.configPath, workspace.configPath);
assert.ok(resolved!.id);
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ something: 'something' })); // invalid workspace
const resolvedInvalid = service.resolveWorkspaceSync(workspace.configPath.fsPath);
const resolvedInvalid = service.resolveLocalWorkspaceSync(workspace.configPath);
assert.ok(!resolvedInvalid);
});
});
......@@ -195,7 +195,7 @@ suite('WorkspacesMainService', () => {
return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => {
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib' }] }));
const resolved = service.resolveWorkspaceSync(workspace.configPath.fsPath);
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib')));
});
});
......@@ -204,7 +204,7 @@ suite('WorkspacesMainService', () => {
return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => {
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib/../other' }] }));
const resolved = service.resolveWorkspaceSync(workspace.configPath.fsPath);
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'other')));
});
});
......@@ -213,7 +213,7 @@ suite('WorkspacesMainService', () => {
return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => {
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: 'ticino-playground/lib' }] }));
const resolved = service.resolveWorkspaceSync(workspace.configPath.fsPath);
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib')));
});
});
......@@ -222,7 +222,7 @@ suite('WorkspacesMainService', () => {
return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => {
fs.writeFileSync(workspace.configPath.fsPath, '{ "folders": [ { "path": "./ticino-playground/lib" } , ] }'); // trailing comma
const resolved = service.resolveWorkspaceSync(workspace.configPath.fsPath);
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib')));
});
});
......@@ -345,7 +345,7 @@ suite('WorkspacesMainService', () => {
test('getUntitledWorkspaceSync', () => {
let untitled = service.getUntitledWorkspacesSync();
assert.equal(0, untitled.length);
assert.equal(untitled.length, 0);
return createWorkspace([process.cwd(), os.tmpdir()]).then(untitledOne => {
assert.ok(fs.existsSync(untitledOne.configPath.fsPath));
......
......@@ -18,7 +18,6 @@ import { URI } from 'vs/base/common/uri';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { Schemas } from 'vs/base/common/network';
import * as resources from 'vs/base/common/resources';
import { isParent } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { RemoteFileDialog } from 'vs/workbench/services/dialogs/electron-browser/remoteFileDialog';
......@@ -196,9 +195,9 @@ export class FileDialogService implements IFileDialogService {
defaultWorkspacePath(schemeFilter: string): URI | undefined {
// Check for current workspace config file first...
if (schemeFilter === Schemas.file && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
const configuration = this.contextService.getWorkspace().configuration;
if (configuration && !isUntitledWorkspace(configuration.fsPath, this.environmentService)) {
if (configuration && !isUntitledWorkspace(configuration, this.environmentService)) {
return resources.dirname(configuration) || undefined;
}
}
......@@ -340,6 +339,6 @@ export class RemoteFileDialogService extends FileDialogService {
}
}
function isUntitledWorkspace(path: string, environmentService: IEnvironmentService): boolean {
return isParent(path, environmentService.workspacesHome, !isLinux /* ignore case */);
function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean {
return resources.isEqualOrParent(path, environmentService.untitledWorkspacesHome);
}
\ No newline at end of file
......@@ -186,7 +186,7 @@ export class LabelService implements ILabelService {
}
// Workspace: Untitled
if (isEqualOrParent(workspace.configPath, URI.file(this.environmentService.workspacesHome))) {
if (isEqualOrParent(workspace.configPath, this.environmentService.untitledWorkspacesHome)) {
return localize('untitledWorkspace', "Untitled (Workspace)");
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册