提交 79a25127 编写于 作者: R Ramya Achutha Rao

Emmet: Allow css completions in style tag/attribute, html completions in script tags with html type

上级 c314388d
......@@ -5,11 +5,11 @@
import * as vscode from 'vscode';
import { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetNode';
import { getEmmetHelper, getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet } from './util';
import { getEmmetHelper, getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny } from './util';
const trimRegex = /[\u00a0]*[\d|#|\-|\*|\u2022]+\.?/;
const hexColorRegex = /^#\d+$/;
const allowedMimeTypesInScriptTag = ['text/html', 'text/plain', 'text/x-template', 'text/template'];
const inlineElements = ['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo',
'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i',
'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'object', 'q',
......@@ -294,8 +294,24 @@ export function expandEmmetAbbreviation(args: any): Thenable<boolean | undefined
if (!helper.isAbbreviationValid(syntax, abbreviation)) {
return;
}
let currentNode = getNode(rootNode, position, true);
let validateLocation = true;
let syntaxToUse = syntax;
if (editor.document.languageId === 'html') {
if (isStyleAttribute(currentNode, position)) {
syntaxToUse = 'css';
validateLocation = false;
} else {
const embeddedCssNode = getEmbeddedCssNodeIfAny(editor.document, currentNode, position);
if (embeddedCssNode) {
currentNode = getNode(embeddedCssNode, position, true);
syntaxToUse = 'css';
}
}
}
if (!isValidLocationForEmmetAbbreviation(editor.document, rootNode, syntax, position, rangeToReplace)) {
if (validateLocation && !isValidLocationForEmmetAbbreviation(editor.document, rootNode, currentNode, syntaxToUse, position, rangeToReplace)) {
return;
}
......@@ -305,7 +321,7 @@ export function expandEmmetAbbreviation(args: any): Thenable<boolean | undefined
allAbbreviationsSame = false;
}
abbreviationList.push({ syntax, abbreviation, rangeToReplace, filter });
abbreviationList.push({ syntax: syntaxToUse, abbreviation, rangeToReplace, filter });
});
return expandAbbreviationInRange(editor, abbreviationList, allAbbreviationsSame).then(success => {
......@@ -326,12 +342,12 @@ function fallbackTab(): Thenable<boolean | undefined> {
* Works only on html and css/less/scss syntax
* @param document current Text Document
* @param rootNode parsed document
* @param currentNode current node in the parsed document
* @param syntax syntax of the abbreviation
* @param position position to validate
* @param abbreviationRange The range of the abbreviation for which given position is being validated
*/
export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, rootNode: Node | undefined, syntax: string, position: vscode.Position, abbreviationRange: vscode.Range): boolean {
const currentNode = rootNode ? getNode(rootNode, position, true) : null;
export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, rootNode: Node | undefined, currentNode: Node | null, syntax: string, position: vscode.Position, abbreviationRange: vscode.Range): boolean {
if (isStyleSheet(syntax)) {
const stylesheet = <Stylesheet>rootNode;
if (stylesheet && (stylesheet.comments || []).some(x => position.isAfterOrEqual(x.start) && position.isBeforeOrEqual(x.end))) {
......@@ -407,6 +423,12 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen
let start = new vscode.Position(0, 0);
if (currentHtmlNode) {
if (currentHtmlNode.name === 'script') {
return (currentHtmlNode.attributes
&& currentHtmlNode.attributes.some(x => x.name.toString() === 'type'
&& allowedMimeTypesInScriptTag.indexOf(x.value.toString()) > -1));
}
const innerRange = getInnerRange(currentHtmlNode);
// Fix for https://github.com/Microsoft/vscode/issues/28829
......
......@@ -6,11 +6,39 @@
import * as vscode from 'vscode';
import { Node, Stylesheet } from 'EmmetNode';
import { isValidLocationForEmmetAbbreviation } from './abbreviationActions';
import { getEmmetHelper, getMappingForIncludedLanguages, parsePartialStylesheet, getEmmetConfiguration, getEmmetMode, isStyleSheet, parseDocument, } from './util';
import { getEmmetHelper, getMappingForIncludedLanguages, parsePartialStylesheet, getEmmetConfiguration, getEmmetMode, isStyleSheet, parseDocument, getEmbeddedCssNodeIfAny, isStyleAttribute, getNode } from './util';
export class DefaultCompletionItemProvider implements vscode.CompletionItemProvider {
private lastCompletionType: string | undefined;
public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Thenable<vscode.CompletionList | undefined> | undefined {
const completionResult = this.provideCompletionItemsInternal(document, position, token, context);
if (!completionResult) {
this.lastCompletionType = undefined;
return;
}
return completionResult.then(completionList => {
if (!completionList || !completionList.items.length) {
this.lastCompletionType = undefined;
return completionList;
}
const item = completionList.items[0];
const expandedText = item.documentation ? item.documentation.toString() : '';
if (expandedText.startsWith('<')) {
this.lastCompletionType = 'html';
} else if (expandedText.indexOf(':') > 0 && expandedText.endsWith(';')) {
this.lastCompletionType = 'css';
} else {
this.lastCompletionType = undefined;
}
return completionList;
});
}
private provideCompletionItemsInternal(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Thenable<vscode.CompletionList | undefined> | undefined {
const emmetConfig = vscode.workspace.getConfiguration('emmet');
const excludedLanguages = emmetConfig['excludeLanguages'] ? emmetConfig['excludeLanguages'] : [];
if (excludedLanguages.indexOf(document.languageId) > -1) {
......@@ -28,29 +56,60 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi
}
const helper = getEmmetHelper();
let validateLocation = syntax === 'html' || syntax === 'jsx' || syntax === 'xml';
let rootNode: Node | undefined = undefined;
let currentNode: Node | null = null;
if (document.languageId === 'html') {
if (context.triggerKind === vscode.CompletionTriggerKind.TriggerForIncompleteCompletions) {
switch (this.lastCompletionType) {
case 'html':
validateLocation = false;
break;
case 'css':
validateLocation = false;
syntax = 'css';
break;
default:
break;
}
}
if (validateLocation) {
rootNode = parseDocument(document, false);
currentNode = getNode(rootNode, position, true);
if (isStyleAttribute(currentNode, position)) {
syntax = 'css';
validateLocation = false;
} else {
const embeddedCssNode = getEmbeddedCssNodeIfAny(document, currentNode, position);
if (embeddedCssNode) {
currentNode = getNode(embeddedCssNode, position, true);
syntax = 'css';
}
}
}
}
const extractAbbreviationResults = helper.extractAbbreviation(document, position, !isStyleSheet(syntax));
if (!extractAbbreviationResults || !helper.isAbbreviationValid(syntax, extractAbbreviationResults.abbreviation)) {
return;
}
let validateLocation = false;
let rootNode: Node | undefined = undefined;
if (context.triggerKind !== vscode.CompletionTriggerKind.TriggerForIncompleteCompletions) {
validateLocation = syntax === 'html' || syntax === 'jsx' || syntax === 'xml' || isStyleSheet(document.languageId);
// If document can be css parsed, get currentNode
if (isStyleSheet(document.languageId)) {
let usePartialParsing = vscode.workspace.getConfiguration('emmet')['optimizeStylesheetParsing'] === true;
rootNode = usePartialParsing && document.lineCount > 1000 ? parsePartialStylesheet(document, position) : <Stylesheet>parseDocument(document, false);
if (!rootNode) {
return;
}
} else if (document.languageId === 'html') {
rootNode = parseDocument(document, false);
if (isStyleSheet(document.languageId) && context.triggerKind !== vscode.CompletionTriggerKind.TriggerForIncompleteCompletions) {
validateLocation = true;
let usePartialParsing = vscode.workspace.getConfiguration('emmet')['optimizeStylesheetParsing'] === true;
rootNode = usePartialParsing && document.lineCount > 1000 ? parsePartialStylesheet(document, position) : <Stylesheet>parseDocument(document, false);
if (!rootNode) {
return;
}
currentNode = getNode(rootNode, position, true);
}
if (validateLocation && !isValidLocationForEmmetAbbreviation(document, rootNode, syntax, position, extractAbbreviationResults.abbreviationRange)) {
if (validateLocation && !isValidLocationForEmmetAbbreviation(document, rootNode, currentNode, syntax, position, extractAbbreviationResults.abbreviationRange)) {
return;
}
......
......@@ -25,13 +25,19 @@ const htmlContents = `
</ul>
<style>
.boo {
m10
display: dn; m10
}
</style>
<span></span>
(ul>li.item$)*2
(ul>li.item$)*2+span
(div>dl>(dt+dd)*2)
<script type="text/html">
span.hello
</script>
<script type="text/javascript">
span.hello
</script>
</body>
`;
......@@ -216,7 +222,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => {
test('Expand css when inside style tag (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', (editor, doc) => {
editor.selection = new Selection(13, 3, 13, 6);
editor.selection = new Selection(13, 16, 13, 19);
let expandPromise = expandEmmetAbbreviation({ language: 'css' });
if (!expandPromise) {
return Promise.resolve();
......@@ -228,32 +234,154 @@ suite('Tests for Expand Abbreviations (HTML)', () => {
});
});
// test('Expand css when inside style tag in completion list (HTML)', () => {
// const abbreviation = 'm10';
// const expandedText = 'margin: 10px;';
// return withRandomFileEditor(htmlContents, 'html', (editor, doc) => {
// editor.selection = new Selection(13, 3, 13, 6);
// const cancelSrc = new CancellationTokenSource();
// const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
// if (!completionPromise) {
// assert.equal(1, 2, `Problem with expanding m10`);
// return Promise.resolve();
// }
// return completionPromise.then((completionList: CompletionList) => {
// if (!completionList.items || !completionList.items.length) {
// assert.equal(1, 2, `Problem with expanding m10`);
// return Promise.resolve();
// }
// const emmetCompletionItem = completionList.items[0];
// assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`);
// assert.equal((<string>emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
// assert.equal(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`);
// return Promise.resolve();
// });
// });
// });
test('Expand css when inside style tag in completion list (HTML)', () => {
const abbreviation = 'm10';
const expandedText = 'margin: 10px;';
return withRandomFileEditor(htmlContents, 'html', (editor, doc) => {
editor.selection = new Selection(13, 16, 13, 19);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise) {
assert.equal(1, 2, `Problem with expanding m10`);
return Promise.resolve();
}
return completionPromise.then((completionList: CompletionList) => {
if (!completionList.items || !completionList.items.length) {
assert.equal(1, 2, `Problem with expanding m10`);
return Promise.resolve();
}
const emmetCompletionItem = completionList.items[0];
assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`);
assert.equal((<string>emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
assert.equal(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`);
return Promise.resolve();
});
});
});
test('No expanding text inside style tag if position is not for property name (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', (editor, doc) => {
editor.selection = new Selection(13, 14, 13, 14);
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), htmlContents);
return Promise.resolve();
});
});
});
test('No expanding text in completion list inside style tag if position is not for property name (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', (editor, doc) => {
editor.selection = new Selection(13, 14, 13, 14);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`);
return Promise.resolve();
});
});
test('Expand css when inside style attribute (HTML)', () => {
const styleAttributeContent = '<div style="m10" class="hello"></div>';
return withRandomFileEditor(styleAttributeContent, 'html', (editor, doc) => {
editor.selection = new Selection(0, 15, 0, 15);
let expandPromise = expandEmmetAbbreviation(null);
if (!expandPromise) {
return Promise.resolve();
}
return expandPromise.then(() => {
assert.equal(editor.document.getText(), styleAttributeContent.replace('m10', 'margin: 10px;'));
return Promise.resolve();
});
});
});
test('Expand css when inside style attribute in completion list (HTML)', () => {
const abbreviation = 'm10';
const expandedText = 'margin: 10px;';
return withRandomFileEditor('<div style="m10" class="hello"></div>', 'html', (editor, doc) => {
editor.selection = new Selection(0, 15, 0, 15);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise) {
assert.equal(1, 2, `Problem with expanding m10`);
return Promise.resolve();
}
return completionPromise.then((completionList: CompletionList) => {
if (!completionList.items || !completionList.items.length) {
assert.equal(1, 2, `Problem with expanding m10`);
return Promise.resolve();
}
const emmetCompletionItem = completionList.items[0];
assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`);
assert.equal((<string>emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
assert.equal(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`);
return Promise.resolve();
});
});
});
test('Expand html when inside script tag with html type (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', (editor, doc) => {
editor.selection = new Selection(21, 12, 21, 12);
let expandPromise = expandEmmetAbbreviation(null);
if (!expandPromise) {
return Promise.resolve();
}
return expandPromise.then(() => {
assert.equal(editor.document.getText(), htmlContents.replace('span.hello', '<span class="hello"></span>'));
return Promise.resolve();
});
});
});
test('Expand html when inside script tag with html type (HTML)', () => {
const abbreviation = 'span.hello';
const expandedText = '<span class="hello"></span>';
return withRandomFileEditor(htmlContents, 'html', (editor, doc) => {
editor.selection = new Selection(21, 12, 21, 12);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise) {
assert.equal(1, 2, `Problem with expanding span.hello`);
return Promise.resolve();
}
return completionPromise.then((completionList: CompletionList) => {
if (!completionList.items || !completionList.items.length) {
assert.equal(1, 2, `Problem with expanding span.hello`);
return Promise.resolve();
}
const emmetCompletionItem = completionList.items[0];
assert.equal(emmetCompletionItem.label, abbreviation, `Label of completion item doesnt match.`);
assert.equal((<string>emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
return Promise.resolve();
});
});
});
test('No expanding text inside script tag with javascript type (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', (editor, doc) => {
editor.selection = new Selection(24, 12, 24, 12);
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), htmlContents);
return Promise.resolve();
});
});
});
test('No expanding text in completion list inside script tag with javascript type (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', (editor, doc) => {
editor.selection = new Selection(24, 12, 24, 12);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`);
return Promise.resolve();
});
});
// test('No expanding when html is excluded in the settings', () => {
// return workspace.getConfiguration('emmet').update('excludeLanguages', ['html'], ConfigurationTarget.Global).then(() => {
......
......@@ -7,12 +7,17 @@ import 'mocha';
import * as assert from 'assert';
import { withRandomFileEditor } from './testUtils';
import * as vscode from 'vscode';
import { parsePartialStylesheet } from '../util';
import { parsePartialStylesheet, getNode } from '../util';
import { isValidLocationForEmmetAbbreviation } from '../abbreviationActions';
suite('Tests for partial parse of Stylesheets', () => {
function isValid(doc: vscode.TextDocument, range: vscode.Range, syntax: string): boolean {
const rootNode = parsePartialStylesheet(doc, range.end);
const currentNode = getNode(rootNode, range.end, true);
return isValidLocationForEmmetAbbreviation(doc, rootNode, currentNode, 'css', range.end, range);
}
suite('Tests for partial parse of Stylesheets', () => {
test('Ignore block comment inside rule', function (): any {
const cssContents = `
p {
......@@ -37,10 +42,10 @@ p {
]
rangesForEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'css', range.end, range), true);
assert.equal(isValid(doc, range, 'css'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'css', range.end, range), false);
assert.equal(isValid(doc, range, 'css'), false);
});
return Promise.resolve();
......@@ -67,7 +72,7 @@ dn {
new vscode.Range(7, 2, 7, 4) // bg after ending of badly constructed block
];
rangesNotEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'scss', range.end, range), false);
assert.equal(isValid(doc, range, 'scss'), false);
});
return Promise.resolve();
});
......@@ -102,10 +107,10 @@ comment */
new vscode.Range(10, 2, 10, 3) // p after ending of block
];
rangesForEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'css', range.end, range), true);
assert.equal(isValid(doc, range, 'css'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'css', range.end, range), false);
assert.equal(isValid(doc, range, 'css'), false);
});
return Promise.resolve();
});
......@@ -137,10 +142,10 @@ comment */
new vscode.Range(6, 3, 6, 4) // In selector
];
rangesForEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'scss', range.end, range), true);
assert.equal(isValid(doc, range, 'scss'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'scss', range.end, range), false);
assert.equal(isValid(doc, range, 'scss'), false);
});
return Promise.resolve();
});
......@@ -169,10 +174,10 @@ comment */
new vscode.Range(1, 66, 1, 68) // Outside any ruleset
];
rangesForEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'scss', range.end, range), true);
assert.equal(isValid(doc, range, 'scss'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'scss', range.end, range), false);
assert.equal(isValid(doc, range, 'scss'), false);
});
return Promise.resolve();
});
......@@ -204,10 +209,10 @@ p.#{dn} {
new vscode.Range(7, 1, 7, 3) // dn inside ruleset whose selector uses nested interpolation
];
rangesForEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'scss', range.end, range), true);
assert.equal(isValid(doc, range, 'scss'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'scss', range.end, range), false);
assert.equal(isValid(doc, range, 'scss'), false);
});
return Promise.resolve();
});
......@@ -242,10 +247,10 @@ ment */{
new vscode.Range(6, 3, 6, 4) // In c inside block comment
];
rangesForEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'scss', range.end, range), true);
assert.equal(isValid(doc, range, 'scss'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValidLocationForEmmetAbbreviation(doc, parsePartialStylesheet(doc, range.end), 'scss', range.end, range), false);
assert.equal(isValid(doc, range, 'scss'), false);
});
return Promise.resolve();
});
......
......@@ -529,3 +529,37 @@ export function getCssPropertyFromDocument(editor: vscode.TextEditor, position:
return (node && node.type === 'property') ? <Property>node : null;
}
}
export function getEmbeddedCssNodeIfAny(document: vscode.TextDocument, currentNode: Node | null, position: vscode.Position): Node | undefined {
if (!currentNode) {
return;
}
const currentHtmlNode = <HtmlNode>currentNode;
if (currentHtmlNode && currentHtmlNode.close) {
const innerRange = getInnerRange(currentHtmlNode);
if (innerRange && innerRange.contains(position)) {
if (currentHtmlNode.name === 'style'
&& currentHtmlNode.open.end.isBefore(position)
&& currentHtmlNode.close.start.isAfter(position)
) {
let buffer = new DocumentStreamReader(document, currentHtmlNode.open.end, new vscode.Range(currentHtmlNode.open.end, currentHtmlNode.close.start));
return parseStylesheet(buffer);
}
}
}
}
export function isStyleAttribute(currentNode: Node | null, position: vscode.Position): boolean {
if (!currentNode) {
return false;
}
const currentHtmlNode = <HtmlNode>currentNode;
const index = (currentHtmlNode.attributes || []).findIndex(x => x.name.toString() === 'style');
if (index === -1) {
return false;
}
const styleAttribute = currentHtmlNode.attributes[index];
return position.isAfterOrEqual(styleAttribute.value.start) && position.isBeforeOrEqual(styleAttribute.value.end);
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册