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

Merge branch 'master' into sandy081/sync/globalStorage

......@@ -45,7 +45,7 @@ export function getComparisonKey(resource: URI, caseInsensitivePath = hasToIgnor
if (caseInsensitivePath) {
path = path.toLowerCase();
}
return `${resource.scheme}://${resource.authority.toLowerCase()}/${path}?${resource.query}`;
return resource.with({ authority: resource.authority.toLowerCase(), path: path, fragment: null }).toString();
}
export function hasToIgnoreCase(resource: URI | undefined): boolean {
......
......@@ -205,7 +205,7 @@ export class URI implements UriComponents {
// if (this.scheme !== 'file') {
// console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`);
// }
return _makeFsPath(this);
return _makeFsPath(this, false);
}
// ---- modify to new -------------------------
......@@ -347,9 +347,13 @@ export class URI implements UriComponents {
if (!uri.path) {
throw new Error(`[UriError]: cannot call joinPaths on URI without path`);
}
return uri.with({
path: paths.posix.join(uri.path, ...pathFragment)
});
let newPath: string;
if (isWindows && uri.scheme === 'file') {
newPath = URI.file(paths.win32.join(_makeFsPath(uri, true), ...pathFragment)).path;
} else {
newPath = paths.posix.join(uri.path, ...pathFragment);
}
return uri.with({ path: newPath });
}
// ---- printing/externalize ---------------------------
......@@ -416,7 +420,7 @@ class _URI extends URI {
get fsPath(): string {
if (!this._fsPath) {
this._fsPath = _makeFsPath(this);
this._fsPath = _makeFsPath(this, false);
}
return this._fsPath;
}
......@@ -572,7 +576,7 @@ function encodeURIComponentMinimal(path: string): string {
/**
* Compute `fsPath` for the given uri
*/
function _makeFsPath(uri: URI): string {
function _makeFsPath(uri: URI, keepDriveLetterCasing: boolean): string {
let value: string;
if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') {
......@@ -584,7 +588,11 @@ function _makeFsPath(uri: URI): string {
&& uri.path.charCodeAt(2) === CharCode.Colon
) {
// windows drive letter: file:///c:/far/boo
value = uri.path[1].toLowerCase() + uri.path.substr(2);
if (!keepDriveLetterCasing) {
value = uri.path[1].toLowerCase() + uri.path.substr(2);
} else {
value = uri.path.substr(1, 2);
}
} else {
// other path
value = uri.path;
......
......@@ -503,29 +503,22 @@ suite('URI', () => {
// }
// console.profileEnd();
});
test('URI#joinPath', function () {
function assertJoined(base: string, fragment: string, expected: string, checkWithUrl: boolean = true) {
const baseUri = URI.parse(base);
const newUri = URI.joinPath(baseUri, fragment);
const actual = newUri.toString(true);
assert.equal(actual, expected);
if (checkWithUrl) {
const actualUrl = new URL(fragment, base).href;
assert.equal(actualUrl, expected);
}
function assertJoined(base: string, fragment: string, expected: string, checkWithUrl: boolean = true) {
const baseUri = URI.parse(base);
const newUri = URI.joinPath(baseUri, fragment);
const actual = newUri.toString(true);
assert.equal(actual, expected);
if (checkWithUrl) {
const actualUrl = new URL(fragment, base).href;
assert.equal(actualUrl, expected, 'DIFFERENT from URL');
}
}
test('URI#joinPath', function () {
assertJoined(('file://server/share/c:/'), '../../bazz', 'file://server/bazz');
assertJoined(('file://server/share/c:'), '../../bazz', 'file://server/bazz');
assertJoined(('file:///foo/'), '../../bazz', 'file:///bazz');
assertJoined(('file:///foo'), '../../bazz', 'file:///bazz');
assertJoined(('file:///foo'), '../../bazz', 'file:///bazz');
assertJoined(('file:///c:/foo/'), '../../bazz', 'file:///bazz', false);
assertJoined(('file://ser/foo/'), '../../bazz', 'file://ser/bazz');
assertJoined(('file://ser/foo'), '../../bazz', 'file://ser/bazz');
assertJoined(('file:///foo/bar/'), './bazz', 'file:///foo/bar/bazz');
assertJoined(('file:///foo/bar'), './bazz', 'file:///foo/bar/bazz', false);
assertJoined(('file:///foo/bar'), 'bazz', 'file:///foo/bar/bazz', false);
......@@ -547,4 +540,28 @@ suite('URI', () => {
assert.throws(() => assertJoined(('foo://bar'), 'bazz', ''));
assert.throws(() => new URL('bazz', 'foo://bar'));
});
test('URI#joinPath (posix)', function () {
if (isWindows) {
this.skip();
}
assertJoined(('file:///c:/foo/'), '../../bazz', 'file:///bazz', false);
assertJoined(('file://server/share/c:/'), '../../bazz', 'file://server/bazz', false);
assertJoined(('file://server/share/c:'), '../../bazz', 'file://server/bazz', false);
assertJoined(('file://ser/foo/'), '../../bazz', 'file://ser/bazz');
assertJoined(('file://ser/foo'), '../../bazz', 'file://ser/bazz');
});
test('URI#joinPath (windows)', function () {
if (!isWindows) {
this.skip();
}
assertJoined(('file:///c:/foo/'), '../../bazz', 'file:///c:/bazz', false);
assertJoined(('file://server/share/c:/'), '../../bazz', 'file://server/share/bazz', false);
assertJoined(('file://server/share/c:'), '../../bazz', 'file://server/share/bazz', false);
assertJoined(('file://ser/foo/'), '../../bazz', 'file://ser/foo/bazz', false);
assertJoined(('file://ser/foo'), '../../bazz', 'file://ser/foo/bazz', false);
});
});
......@@ -452,6 +452,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments
id: COMMENTS_VIEW_ID,
name: COMMENTS_VIEW_TITLE,
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]),
hideIfEmpty: true,
order: 10,
}, ViewContainerLocation.Panel);
......@@ -460,6 +461,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments
name: COMMENTS_VIEW_TITLE,
canToggleVisibility: false,
ctorDescriptor: new SyncDescriptor(CommentsPanel),
canMoveView: true,
focusCommand: {
id: 'workbench.action.focusCommentsPanel'
}
......
......@@ -64,6 +64,11 @@ export const viewsContainersContribution: IJSONSchema = {
description: localize('views.container.activitybar', "Contribute views containers to Activity Bar"),
type: 'array',
items: viewsContainerSchema
},
'panel': {
description: localize('views.container.panel', "Contribute views containers to Panel"),
type: 'array',
items: viewsContainerSchema
}
}
};
......@@ -214,7 +219,8 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
private addCustomViewContainers(extensionPoints: readonly IExtensionPointUser<ViewContainerExtensionPointType>[], existingViewContainers: ViewContainer[]): void {
const viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
let order = TEST_VIEW_CONTAINER_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId).length + 1;
let activityBarOrder = TEST_VIEW_CONTAINER_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length + 1;
let panelOrder = 5 + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Panel).length + 1;
for (let { value, collector, description } of extensionPoints) {
forEach(value, entry => {
if (!this.isValidViewsContainer(entry.value, collector)) {
......@@ -222,7 +228,10 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
}
switch (entry.key) {
case 'activitybar':
order = this.registerCustomViewContainers(entry.value, description, order, existingViewContainers);
activityBarOrder = this.registerCustomViewContainers(entry.value, description, activityBarOrder, existingViewContainers, ViewContainerLocation.Sidebar);
break;
case 'panel':
panelOrder = this.registerCustomViewContainers(entry.value, description, panelOrder, existingViewContainers, ViewContainerLocation.Panel);
break;
}
});
......@@ -248,7 +257,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
const title = localize('test', "Test");
const icon = URI.parse(require.toUrl('./media/test.svg'));
this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, undefined);
this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, undefined, ViewContainerLocation.Sidebar);
}
private isValidViewsContainer(viewsContainersDescriptors: IUserFriendlyViewsContainerDescriptor[], collector: ExtensionMessageCollector): boolean {
......@@ -279,11 +288,11 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
return true;
}
private registerCustomViewContainers(containers: IUserFriendlyViewsContainerDescriptor[], extension: IExtensionDescription, order: number, existingViewContainers: ViewContainer[]): number {
private registerCustomViewContainers(containers: IUserFriendlyViewsContainerDescriptor[], extension: IExtensionDescription, order: number, existingViewContainers: ViewContainer[], location: ViewContainerLocation): number {
containers.forEach(descriptor => {
const icon = resources.joinPath(extension.extensionLocation, descriptor.icon);
const id = `workbench.view.extension.${descriptor.id}`;
const viewContainer = this.registerCustomViewContainer(id, descriptor.title, icon, order++, extension.identifier);
const viewContainer = this.registerCustomViewContainer(id, descriptor.title, icon, order++, extension.identifier, location);
// Move those views that belongs to this container
if (existingViewContainers.length) {
......@@ -301,7 +310,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
return order;
}
private registerCustomViewContainer(id: string, title: string, icon: URI, order: number, extensionId: ExtensionIdentifier | undefined): ViewContainer {
private registerCustomViewContainer(id: string, title: string, icon: URI, order: number, extensionId: ExtensionIdentifier | undefined, location: ViewContainerLocation): ViewContainer {
let viewContainer = this.viewContainersRegistry.get(id);
if (!viewContainer) {
......@@ -316,7 +325,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
hideIfEmpty: true,
order,
icon,
}, ViewContainerLocation.Sidebar);
}, location);
// Register Action to Open Viewlet
class OpenCustomViewletAction extends ShowViewletAction {
......
......@@ -20,7 +20,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { WorkbenchAsyncDataTree, IListService } from 'vs/platform/list/browser/listService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { PANEL_BACKGROUND } from 'vs/workbench/common/theme';
import { IColorMapping } from 'vs/platform/theme/common/styler';
export const COMMENTS_VIEW_ID = 'workbench.panel.comments';
export const COMMENTS_VIEW_TITLE = 'Comments';
......@@ -149,10 +149,15 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode>
}
}
export interface ICommentsListOptions {
overrideStyles?: IColorMapping;
}
export class CommentsList extends WorkbenchAsyncDataTree<any, any> {
constructor(
labels: ResourceLabels,
container: HTMLElement,
options: ICommentsListOptions,
@IContextKeyService contextKeyService: IContextKeyService,
@IListService listService: IListService,
@IThemeService themeService: IThemeService,
......@@ -202,9 +207,7 @@ export class CommentsList extends WorkbenchAsyncDataTree<any, any> {
collapseByDefault: () => {
return false;
},
overrideStyles: {
listBackground: PANEL_BACKGROUND
}
overrideStyles: options.overrideStyles
},
contextKeyService,
listService,
......
......@@ -149,7 +149,7 @@ export class CommentsPanel extends ViewPane {
private createTree(): void {
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
this.tree = this._register(this.instantiationService.createInstance(CommentsList, this.treeLabels, this.treeContainer));
this.tree = this._register(this.instantiationService.createInstance(CommentsList, this.treeLabels, this.treeContainer, { overrideStyles: { listBackground: this.getBackgroundColor() } }));
const commentsNavigator = this._register(ResourceNavigator.createTreeResourceNavigator(this.tree, { openOnFocus: true }));
this._register(commentsNavigator.onDidOpenResource(e => {
......
......@@ -76,7 +76,7 @@ const viewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewCo
name: nls.localize('run', "Run"),
ctorDescriptor: new SyncDescriptor(DebugViewPaneContainer),
icon: 'codicon-debug-alt-2',
order: 3
order: 2
}, ViewContainerLocation.Sidebar);
const openViewletKb: IKeybindings = {
......@@ -96,6 +96,7 @@ const VIEW_CONTAINER: ViewContainer = Registry.as<IViewContainersRegistry>(ViewE
id: OpenDebugPanelAction.ID,
keybindings: openPanelKb
},
order: 3,
hideIfEmpty: true
}, ViewContainerLocation.Panel);
......
......@@ -208,9 +208,20 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
}));
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) {
this.onDidFontChange();
this.onDidStyleChange();
}
}));
this._register(this.themeService.onDidColorThemeChange(e => {
this.onDidStyleChange();
}));
this._register(this.viewDescriptorService.onDidChangeLocation(e => {
if (e.views.some(v => v.id === this.id)) {
this.onDidStyleChange();
}
}));
this._register(this.editorService.onDidActiveEditorChange(() => {
this.setMode();
}));
......@@ -253,14 +264,15 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
}
}
private onDidFontChange(): void {
private onDidStyleChange(): void {
if (this.styleElement) {
const debugConsole = this.configurationService.getValue<IDebugConfiguration>('debug').console;
const fontSize = debugConsole.fontSize;
const fontFamily = debugConsole.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : debugConsole.fontFamily;
const lineHeight = debugConsole.lineHeight ? `${debugConsole.lineHeight}px` : '1.4em';
const backgroundColor = this.themeService.getColorTheme().getColor(this.getBackgroundColor());
// Set the font size, font family, line height and align the twistie to be centered
// Set the font size, font family, line height and align the twistie to be centered, and input theme color
this.styleElement.innerHTML = `
.repl .repl-tree .expression {
font-size: ${fontSize}px;
......@@ -274,6 +286,10 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
.repl .repl-tree .monaco-tl-twistie {
background-position-y: calc(100% - ${fontSize * 1.4 / 2 - 8}px);
}
.repl .repl-input-wrapper .monaco-editor .lines-content {
background-color: ${backgroundColor};
}
`;
this.tree.rerender();
......@@ -510,7 +526,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
// Make sure to select the session if debugging is already active
this.selectSession();
this.styleElement = dom.createStyleSheet(this.container);
this.onDidFontChange();
this.onDidStyleChange();
}
private createReplInput(container: HTMLElement): void {
......
......@@ -112,6 +112,7 @@ const VIEW_CONTAINER: ViewContainer = Registry.as<IViewContainersRegistry>(ViewC
id: Constants.MARKERS_CONTAINER_ID,
name: Messages.MARKERS_PANEL_TITLE_PROBLEMS,
hideIfEmpty: true,
order: 0,
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [Constants.MARKERS_CONTAINER_ID, Constants.MARKERS_VIEW_STORAGE_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]),
focusCommand: {
id: ToggleMarkersPanelAction.ID, keybindings: {
......@@ -122,6 +123,7 @@ const VIEW_CONTAINER: ViewContainer = Registry.as<IViewContainersRegistry>(ViewC
Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry).registerViews([{
id: Constants.MARKERS_VIEW_ID,
containerIcon: 'codicon-warning',
name: Messages.MARKERS_PANEL_TITLE_PROBLEMS,
canToggleVisibility: false,
canMoveView: true,
......
......@@ -61,6 +61,7 @@ const toggleOutputActionKeybindings = {
const VIEW_CONTAINER: ViewContainer = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({
id: OUTPUT_VIEW_ID,
name: nls.localize('output', "Output"),
order: 1,
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [OUTPUT_VIEW_ID, OUTPUT_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]),
focusCommand: { id: toggleOutputAcitonId, keybindings: toggleOutputActionKeybindings }
}, ViewContainerLocation.Panel);
......
......@@ -382,7 +382,8 @@ const VIEW_CONTAINER = Registry.as<IViewContainersRegistry>(ViewContainerExtensi
name: nls.localize('terminal', "Terminal"),
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [TERMINAL_VIEW_ID, TERMINAL_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]),
focusCommand: { id: TERMINAL_COMMAND_ID.FOCUS },
hideIfEmpty: true
hideIfEmpty: true,
order: 3
}, ViewContainerLocation.Panel);
Registry.as<panel.PanelRegistry>(panel.Extensions.Panels).setDefaultPanelId(TERMINAL_VIEW_ID);
......
......@@ -148,31 +148,19 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
// update settings schema setting for theme specific settings
this.colorThemeRegistry.onDidChange(async event => {
updateColorThemeConfigurationSchemas(event.themes);
const colorThemeSetting = this.settings.colorTheme;
if (colorThemeSetting !== this.currentColorTheme.settingsId) {
const theme = await this.colorThemeRegistry.findThemeBySettingsId(colorThemeSetting, undefined);
if (theme) {
this.setColorTheme(theme.id, undefined);
return;
}
}
if (this.currentColorTheme.isLoaded) {
const themeData = await this.colorThemeRegistry.findThemeById(this.currentColorTheme.id);
if (!themeData) {
// current theme is no longer available
prevColorId = this.currentColorTheme.id;
this.setColorTheme(DEFAULT_COLOR_THEME_ID, 'auto');
} else {
if (this.currentColorTheme.id === DEFAULT_COLOR_THEME_ID && !types.isUndefined(prevColorId) && await this.colorThemeRegistry.findThemeById(prevColorId)) {
// restore theme
this.setColorTheme(prevColorId, 'auto');
prevColorId = undefined;
} else if (event.added.some(t => t.settingsId === this.currentColorTheme.settingsId)) {
this.reloadCurrentColorTheme();
}
if (await this.restoreColorTheme()) { // checks if theme from settings exists and is set
// restore theme
if (this.currentColorTheme.id === DEFAULT_COLOR_THEME_ID && !types.isUndefined(prevColorId) && await this.colorThemeRegistry.findThemeById(prevColorId)) {
// restore theme
this.setColorTheme(prevColorId, 'auto');
prevColorId = undefined;
} else if (event.added.some(t => t.settingsId === this.currentColorTheme.settingsId)) {
this.reloadCurrentColorTheme();
}
} else if (event.removed.some(t => t.settingsId === this.currentColorTheme.settingsId)) {
// current theme is no longer available
prevColorId = this.currentColorTheme.id;
this.setColorTheme(DEFAULT_COLOR_THEME_ID, 'auto');
}
});
......@@ -187,7 +175,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
} else if (event.added.some(t => t.settingsId === this.currentFileIconTheme.settingsId)) {
this.reloadCurrentFileIconTheme();
}
} else {
} else if (event.removed.some(t => t.settingsId === this.currentFileIconTheme.settingsId)) {
// current theme is no longer available
prevFileIconId = this.currentFileIconTheme.id;
this.setFileIconTheme(DEFAULT_FILE_ICON_THEME_ID, 'auto');
......@@ -206,7 +194,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
} else if (event.added.some(t => t.settingsId === this.currentProductIconTheme.settingsId)) {
this.reloadCurrentProductIconTheme();
}
} else {
} else if (event.removed.some(t => t.settingsId === this.currentProductIconTheme.settingsId)) {
// current theme is no longer available
prevProductIconId = this.currentProductIconTheme.id;
this.setProductIconTheme(DEFAULT_PRODUCT_ICON_THEME_ID, 'auto');
......@@ -389,17 +377,19 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
this.applyTheme(this.currentColorTheme, undefined, false);
}
public restoreColorTheme() {
const colorThemeSetting = this.settings.colorTheme;
if (colorThemeSetting !== this.currentColorTheme.settingsId) {
this.colorThemeRegistry.findThemeBySettingsId(colorThemeSetting, undefined).then(theme => {
if (theme) {
this.setColorTheme(theme.id, undefined);
}
});
public async restoreColorTheme(): Promise<boolean> {
const settingId = this.settings.colorTheme;
const theme = await this.colorThemeRegistry.findThemeBySettingsId(settingId);
if (theme) {
if (settingId !== this.currentColorTheme.settingsId) {
await this.setColorTheme(theme.id, undefined);
}
return true;
}
return false;
}
private updateDynamicCSSRules(themeData: IColorTheme) {
const cssRules = new Set<string>();
const ruleCollector = {
......
......@@ -107,6 +107,7 @@ export function registerProductIconThemeExtensionPoint() {
export interface ThemeChangeEvent<T> {
themes: T[];
added: T[];
removed: T[];
}
export interface IThemeData {
......@@ -135,10 +136,11 @@ export class ThemeRegistry<T extends IThemeData> {
private initialize() {
this.themesExtPoint.setHandler((extensions, delta) => {
const previousIds: { [key: string]: boolean } = {};
const previousIds: { [key: string]: T } = {};
const added: T[] = [];
for (const theme of this.extensionThemes) {
previousIds[theme.id] = true;
previousIds[theme.id] = theme;
}
this.extensionThemes.length = 0;
for (let ext of extensions) {
......@@ -154,9 +156,12 @@ export class ThemeRegistry<T extends IThemeData> {
for (const theme of this.extensionThemes) {
if (!previousIds[theme.id]) {
added.push(theme);
} else {
delete previousIds[theme.id];
}
}
this.onDidChangeEmitter.fire({ themes: this.extensionThemes, added });
const removed = Object.values(previousIds);
this.onDidChangeEmitter.fire({ themes: this.extensionThemes, added, removed });
});
}
......
......@@ -4,7 +4,7 @@
./scripts/test.[sh|bat]
All unit tests are run inside a electron-browser environment which access to DOM and Nodejs api. This is the closest to the enviroment in which VS Code itself ships. Notes:
All unit tests are run inside a electron-browser environment which access to DOM and Nodejs api. This is the closest to the environment in which VS Code itself ships. Notes:
- use the `--debug` to see an electron window with dev tools which allows for debugging
- to run only a subset of tests use the `--run` or `--glob` options
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册