Commit 6cc4917f authored by Joel Einbinder's avatar Joel Einbinder Committed by Commit Bot

DevTools: Auto pretty print in sources panel experiment

Change-Id: Ib16be938e7df8c90f02da1211f350d172c34ebff
Reviewed-on: https://chromium-review.googlesource.com/1047949
Commit-Queue: Joel Einbinder <einbinder@chromium.org>
Reviewed-by: default avatarAndrey Lushnikov <lushnikov@chromium.org>
Cr-Commit-Position: refs/heads/master@{#558766}
parent ac8b8e11
...@@ -9,55 +9,56 @@ ...@@ -9,55 +9,56 @@
var textEditor = SourcesTestRunner.createTestEditor(); var textEditor = SourcesTestRunner.createTestEditor();
textEditor.setText('1\n2\n3\n4'); textEditor.setText('1\n2\n3\n4');
let changeGeneration = 0;
TestRunner.runTestSuite([ TestRunner.runTestSuite([
function testMarkiningInitialStateAsClean(next) { function testMarkiningInitialStateAsClean(next) {
TestRunner.addResult('Initial state: clean=' + textEditor.isClean()); TestRunner.addResult('Initial state: clean=' + textEditor.isClean(changeGeneration));
textEditor.markClean(); changeGeneration = textEditor.markClean();
TestRunner.addResult('After marking clean: clean=' + textEditor.isClean()); TestRunner.addResult('After marking clean: clean=' + textEditor.isClean(changeGeneration));
textEditor.editRange(TextUtils.TextRange.createFromLocation(0, 0), 'newText'); textEditor.editRange(TextUtils.TextRange.createFromLocation(0, 0), 'newText');
TestRunner.addResult('EDIT; clean=' + textEditor.isClean()); TestRunner.addResult('EDIT; clean=' + textEditor.isClean(changeGeneration));
textEditor.undo(); textEditor.undo();
TestRunner.addResult('UNDO; clean=' + textEditor.isClean()); TestRunner.addResult('UNDO; clean=' + textEditor.isClean(changeGeneration));
textEditor.redo(); textEditor.redo();
TestRunner.addResult('REDO; clean=' + textEditor.isClean()); TestRunner.addResult('REDO; clean=' + textEditor.isClean(changeGeneration));
textEditor.undo(); textEditor.undo();
TestRunner.addResult('UNDO; clean=' + textEditor.isClean()); TestRunner.addResult('UNDO; clean=' + textEditor.isClean(changeGeneration));
textEditor.editRange(TextUtils.TextRange.createFromLocation(1, 0), 'newText2'); textEditor.editRange(TextUtils.TextRange.createFromLocation(1, 0), 'newText2');
TestRunner.addResult('EDIT; clean=' + textEditor.isClean()); TestRunner.addResult('EDIT; clean=' + textEditor.isClean(changeGeneration));
textEditor.undo(); textEditor.undo();
TestRunner.addResult('UNDO; clean=' + textEditor.isClean()); TestRunner.addResult('UNDO; clean=' + textEditor.isClean(changeGeneration));
next(); next();
}, },
function testMiddleStateAsClean(next) { function testMiddleStateAsClean(next) {
TestRunner.addResult('Initial state: clean=' + textEditor.isClean()); TestRunner.addResult('Initial state: clean=' + textEditor.isClean(changeGeneration));
for (var i = 0; i < 3; ++i) { for (var i = 0; i < 3; ++i) {
textEditor.editRange(TextUtils.TextRange.createFromLocation(i, 0), 'newText' + i); textEditor.editRange(TextUtils.TextRange.createFromLocation(i, 0), 'newText' + i);
TestRunner.addResult('EDIT; clean=' + textEditor.isClean()); TestRunner.addResult('EDIT; clean=' + textEditor.isClean(changeGeneration));
} }
textEditor.markClean(); changeGeneration = textEditor.markClean();
TestRunner.addResult('After marking clean: clean=' + textEditor.isClean()); TestRunner.addResult('After marking clean: clean=' + textEditor.isClean(changeGeneration));
textEditor.editRange(TextUtils.TextRange.createFromLocation(3, 0), 'newText' + 3); textEditor.editRange(TextUtils.TextRange.createFromLocation(3, 0), 'newText' + 3);
TestRunner.addResult('EDIT; clean=' + textEditor.isClean()); TestRunner.addResult('EDIT; clean=' + textEditor.isClean(changeGeneration));
for (var i = 0; i < 4; ++i) { for (var i = 0; i < 4; ++i) {
textEditor.undo(); textEditor.undo();
TestRunner.addResult('UNDO; clean=' + textEditor.isClean()); TestRunner.addResult('UNDO; clean=' + textEditor.isClean(changeGeneration));
} }
for (var i = 0; i < 4; ++i) { for (var i = 0; i < 4; ++i) {
textEditor.redo(); textEditor.redo();
TestRunner.addResult('REDO; clean=' + textEditor.isClean()); TestRunner.addResult('REDO; clean=' + textEditor.isClean(changeGeneration));
} }
for (var i = 0; i < 2; ++i) { for (var i = 0; i < 2; ++i) {
textEditor.undo(); textEditor.undo();
TestRunner.addResult('UNDO; clean=' + textEditor.isClean()); TestRunner.addResult('UNDO; clean=' + textEditor.isClean(changeGeneration));
} }
textEditor.editRange(TextUtils.TextRange.createFromLocation(1, 0), 'foo'); textEditor.editRange(TextUtils.TextRange.createFromLocation(1, 0), 'foo');
TestRunner.addResult('EDIT; clean=' + textEditor.isClean()); TestRunner.addResult('EDIT; clean=' + textEditor.isClean(changeGeneration));
textEditor.undo(); textEditor.undo();
TestRunner.addResult('UNDO; clean=' + textEditor.isClean()); TestRunner.addResult('UNDO; clean=' + textEditor.isClean(changeGeneration));
textEditor.undo(); textEditor.undo();
TestRunner.addResult('UNDO; clean=' + textEditor.isClean()); TestRunner.addResult('UNDO; clean=' + textEditor.isClean(changeGeneration));
next(); next();
}, },
]); ]);
......
function uglyFunction() {console.log('ugly function');}
\ No newline at end of file
Verifies that editing a pretty printed resource works properly.
* Initial state *
pretty print button: off
text: function uglyFunction() {console.log('ugly function');}
isDirty: false
workingCopy: function uglyFunction() {console.log('ugly function');}
* Toggle pretty print on *
pretty print button: on
text: function uglyFunction() {
console.log('ugly function');
}
isDirty: false
workingCopy: function uglyFunction() {console.log('ugly function');}
* Type in "X" *
pretty print button: off disabled
text: Xfunction uglyFunction() {
console.log('ugly function');
}
isDirty: true
workingCopy: Xfunction uglyFunction() {
console.log('ugly function');
}
* Undo *
pretty print button: on
text: function uglyFunction() {
console.log('ugly function');
}
isDirty: false
workingCopy: function uglyFunction() {console.log('ugly function');}
* Toggle pretty print off *
pretty print button: off
text: function uglyFunction() {console.log('ugly function');}
isDirty: false
workingCopy: function uglyFunction() {console.log('ugly function');}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(async function() {
TestRunner.addResult(`Verifies that editing a pretty printed resource works properly.\n`);
Runtime.experiments.enableForTest('sourcesPrettyPrint');
await TestRunner.loadModule('sources_test_runner');
await TestRunner.showPanel('sources');
await TestRunner.addScriptTag('resources/ugly-function.js');
const sourceFrame = await SourcesTestRunner.showScriptSourcePromise('ugly-function.js');
dumpState('Initial state');
await sourceFrame._setPretty(true);
dumpState('Toggle pretty print on');
await new Promise(x => SourcesTestRunner.typeIn(sourceFrame.textEditor, 'X', x));
dumpState('Type in "X"');
sourceFrame.textEditor.codeMirror().execCommand('undo');
dumpState('Undo');
await sourceFrame._setPretty(false);
dumpState('Toggle pretty print off');
TestRunner.completeTest();
function dumpState(info) {
const uiSourceCode = sourceFrame.uiSourceCode();
const button = sourceFrame._prettyToggle;
let buttonState = 'invisible';
button.element.disabled
if (button.visible()) {
buttonState = button.toggled() ? 'on' : 'off';
if (button.element.disabled)
buttonState += ' disabled';
}
TestRunner.addResult(`* ${info} *`);
TestRunner.addResult(`pretty print button: ${buttonState}`);
TestRunner.addResult(`text: ${sourceFrame.textEditor.text().trim()}`);
TestRunner.addResult(`isDirty: ${uiSourceCode.isDirty()}`);
TestRunner.addResult(`workingCopy: ${uiSourceCode.workingCopy().trim()}`);
TestRunner.addResult('');
}
})();
...@@ -410,6 +410,8 @@ CodeMirror.prototype = { ...@@ -410,6 +410,8 @@ CodeMirror.prototype = {
*/ */
addOverlay: function(spec, options) {}, addOverlay: function(spec, options) {},
addWidget: function(pos, node, scroll, vert, horiz) {}, addWidget: function(pos, node, scroll, vert, horiz) {},
/** @param {boolean=} isClosed bv */
changeGeneration: function(isClosed) {},
charCoords: function(pos, mode) {}, charCoords: function(pos, mode) {},
clearGutter: function(gutterID) {}, clearGutter: function(gutterID) {},
clearHistory: function() {}, clearHistory: function() {},
...@@ -478,7 +480,8 @@ CodeMirror.prototype = { ...@@ -478,7 +480,8 @@ CodeMirror.prototype = {
indentLine: function(n, dir, aggressive) {}, indentLine: function(n, dir, aggressive) {},
indentSelection: function(how) {}, indentSelection: function(how) {},
indexFromPos: function(coords) {}, indexFromPos: function(coords) {},
isClean: function() {}, /** @param {number=} generation */
isClean: function(generation) {},
iterLinkedDocs: function(f) {}, iterLinkedDocs: function(f) {},
lastLine: function() {}, lastLine: function() {},
lineCount: function() {}, lineCount: function() {},
......
...@@ -116,6 +116,7 @@ Main.Main = class { ...@@ -116,6 +116,7 @@ Main.Main = class {
Runtime.experiments.register('oopifInlineDOM', 'OOPIF: inline DOM ', true); Runtime.experiments.register('oopifInlineDOM', 'OOPIF: inline DOM ', true);
Runtime.experiments.register('protocolMonitor', 'Protocol Monitor'); Runtime.experiments.register('protocolMonitor', 'Protocol Monitor');
Runtime.experiments.register('sourceDiff', 'Source diff'); Runtime.experiments.register('sourceDiff', 'Source diff');
Runtime.experiments.register('sourcesPrettyPrint', 'Automatically pretty print in the Sources Panel');
Runtime.experiments.register( Runtime.experiments.register(
'stepIntoAsync', 'Introduce separate step action, stepInto becomes powerful enough to go inside async call'); 'stepIntoAsync', 'Introduce separate step action, stepInto becomes powerful enough to go inside async call');
Runtime.experiments.register('splitInDrawer', 'Split in drawer', true); Runtime.experiments.register('splitInDrawer', 'Split in drawer', true);
......
...@@ -43,8 +43,9 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -43,8 +43,9 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
this._lazyContent = lazyContent; this._lazyContent = lazyContent;
this._pretty = false; this._pretty = false;
this._rawContent = ''; /** @type {?string} */
/** @type {?Promise<string>} */ this._rawContent = null;
/** @type {?Promise<{content: string, map: !Formatter.FormatterSourceMapping}>} */
this._formattedContentPromise = null; this._formattedContentPromise = null;
/** @type {?Formatter.FormatterSourceMapping} */ /** @type {?Formatter.FormatterSourceMapping} */
this._formattedMap = null; this._formattedMap = null;
...@@ -58,6 +59,10 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -58,6 +59,10 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
this._textEditor = new SourceFrame.SourcesTextEditor(this); this._textEditor = new SourceFrame.SourcesTextEditor(this);
this._textEditor.show(this.element); this._textEditor.show(this.element);
/** @type {?number} */
this._prettyCleanGeneration = null;
this._cleanGeneration = 0;
this._searchConfig = null; this._searchConfig = null;
this._delayedFindSearchMatches = null; this._delayedFindSearchMatches = null;
this._currentSearchResultIndex = -1; this._currentSearchResultIndex = -1;
...@@ -91,6 +96,30 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -91,6 +96,30 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
this._loaded = false; this._loaded = false;
this._contentRequested = false; this._contentRequested = false;
this._highlighterType = ''; this._highlighterType = '';
/** @type {!SourceFrame.Transformer} */
this._transformer = {
/**
* @param {number} editorLineNumber
* @param {number=} editorColumnNumber
* @return {!Array<number>}
*/
editorToRawLocation: (editorLineNumber, editorColumnNumber = 0) => {
if (!this._pretty)
return [editorLineNumber, editorColumnNumber];
return this._prettyToRawLocation(editorLineNumber, editorColumnNumber);
},
/**
* @param {number} lineNumber
* @param {number=} columnNumber
* @return {!Array<number>}
*/
rawToEditorLocation: (lineNumber, columnNumber = 0) => {
if (!this._pretty)
return [lineNumber, columnNumber];
return this._rawToPrettyLocation(lineNumber, columnNumber);
}
};
} }
/** /**
...@@ -104,22 +133,26 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -104,22 +133,26 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
/** /**
* @param {boolean} value * @param {boolean} value
* @return {!Promise}
*/ */
async _setPretty(value) { async _setPretty(value) {
this._pretty = value; this._pretty = value;
this._prettyToggle.setToggled(value);
this._prettyToggle.setEnabled(false); this._prettyToggle.setEnabled(false);
const wasLoaded = this.loaded; const wasLoaded = this.loaded;
const selection = this.selection(); const selection = this.selection();
let newSelection; let newSelection;
if (this._pretty && this._rawContent) { if (this._pretty) {
this.setContent(await this._requestFormattedContent()); const formatInfo = await this._requestFormattedContent();
this._formattedMap = formatInfo.map;
this.setContent(formatInfo.content);
this._prettyCleanGeneration = this._textEditor.markClean();
const start = this._rawToPrettyLocation(selection.startLine, selection.startColumn); const start = this._rawToPrettyLocation(selection.startLine, selection.startColumn);
const end = this._rawToPrettyLocation(selection.endLine, selection.endColumn); const end = this._rawToPrettyLocation(selection.endLine, selection.endColumn);
newSelection = new TextUtils.TextRange(start[0], start[1], end[0], end[1]); newSelection = new TextUtils.TextRange(start[0], start[1], end[0], end[1]);
} else { } else {
this.setContent(this._rawContent); this.setContent(this._rawContent);
this._cleanGeneration = this._textEditor.markClean();
const start = this._prettyToRawLocation(selection.startLine, selection.startColumn); const start = this._prettyToRawLocation(selection.startLine, selection.startColumn);
const end = this._prettyToRawLocation(selection.endLine, selection.endColumn); const end = this._prettyToRawLocation(selection.endLine, selection.endColumn);
newSelection = new TextUtils.TextRange(start[0], start[1], end[0], end[1]); newSelection = new TextUtils.TextRange(start[0], start[1], end[0], end[1]);
...@@ -151,6 +184,14 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -151,6 +184,14 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
} }
} }
/**
* @return {!SourceFrame.Transformer}
*/
transformer() {
return this._transformer;
}
/** /**
* @param {number} line * @param {number} line
* @param {number} column * @param {number} column
...@@ -216,6 +257,13 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -216,6 +257,13 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
return this._textEditor; return this._textEditor;
} }
/**
* @protected
*/
get pretty() {
return this._pretty;
}
async _ensureContentLoaded() { async _ensureContentLoaded() {
if (!this._contentRequested) { if (!this._contentRequested) {
this._contentRequested = true; this._contentRequested = true;
...@@ -223,7 +271,8 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -223,7 +271,8 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
this._rawContent = content || ''; this._rawContent = content || '';
this._formattedContentPromise = null; this._formattedContentPromise = null;
this._formattedMap = null; this._formattedMap = null;
if (this._shouldAutoPrettyPrint && TextUtils.isMinified(this._rawContent)) this._prettyToggle.setEnabled(true);
if (this._shouldAutoPrettyPrint && TextUtils.isMinified(content))
await this._setPretty(true); await this._setPretty(true);
else else
this.setContent(this._rawContent); this.setContent(this._rawContent);
...@@ -231,16 +280,15 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -231,16 +280,15 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
} }
/** /**
* @return {!Promise<string>} * @return {!Promise<{content: string, map: !Formatter.FormatterSourceMapping}>}
*/ */
_requestFormattedContent() { _requestFormattedContent() {
if (this._formattedContentPromise) if (this._formattedContentPromise)
return this._formattedContentPromise; return this._formattedContentPromise;
let fulfill; let fulfill;
this._formattedContentPromise = new Promise(x => fulfill = x); this._formattedContentPromise = new Promise(x => fulfill = x);
new Formatter.ScriptFormatter(this._highlighterType, this._rawContent, (data, map) => { new Formatter.ScriptFormatter(this._highlighterType, this._rawContent || '', (content, map) => {
this._formattedMap = map; fulfill({content, map});
fulfill(data);
}); });
return this._formattedContentPromise; return this._formattedContentPromise;
} }
...@@ -325,10 +373,37 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -325,10 +373,37 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
* @param {!TextUtils.TextRange} newRange * @param {!TextUtils.TextRange} newRange
*/ */
onTextChanged(oldRange, newRange) { onTextChanged(oldRange, newRange) {
const wasPretty = this.pretty;
this._pretty = this._prettyCleanGeneration !== null && this.textEditor.isClean(this._prettyCleanGeneration);
if (this._pretty !== wasPretty)
this._updatePrettyPrintState();
this._prettyToggle.setEnabled(this.isClean());
if (this._searchConfig && this._searchableView) if (this._searchConfig && this._searchableView)
this.performSearch(this._searchConfig, false, false); this.performSearch(this._searchConfig, false, false);
} }
/**
* @return {boolean}
*/
isClean() {
return this.textEditor.isClean(this._cleanGeneration) ||
(this._prettyCleanGeneration !== null && this.textEditor.isClean(this._prettyCleanGeneration));
}
contentCommitted() {
this._cleanGeneration = this._textEditor.markClean();
this._prettyCleanGeneration = null;
this._rawContent = this.textEditor.text();
this._formattedMap = null;
this._formattedContentPromise = null;
if (this._pretty) {
this._pretty = false;
this._updatePrettyPrintState();
}
this._prettyToggle.setEnabled(true);
}
/** /**
* @param {string} content * @param {string} content
* @param {string} mimeType * @param {string} mimeType
...@@ -376,7 +451,7 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -376,7 +451,7 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
if (!this._loaded) { if (!this._loaded) {
this._loaded = true; this._loaded = true;
this._textEditor.setText(content || ''); this._textEditor.setText(content || '');
this._textEditor.markClean(); this._cleanGeneration = this._textEditor.markClean();
this._textEditor.setReadOnly(!this._editable); this._textEditor.setReadOnly(!this._editable);
} else { } else {
const scrollTop = this._textEditor.scrollTop(); const scrollTop = this._textEditor.scrollTop();
...@@ -625,7 +700,7 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -625,7 +700,7 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
* @override * @override
* @return {!Promise} * @return {!Promise}
*/ */
populateLineGutterContextMenu(contextMenu, lineNumber) { populateLineGutterContextMenu(contextMenu, editorLineNumber) {
return Promise.resolve(); return Promise.resolve();
} }
...@@ -633,7 +708,7 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { ...@@ -633,7 +708,7 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
* @override * @override
* @return {!Promise} * @return {!Promise}
*/ */
populateTextAreaContextMenu(contextMenu, lineNumber, columnNumber) { populateTextAreaContextMenu(contextMenu, editorLineNumber, editorColumnNumber) {
return Promise.resolve(); return Promise.resolve();
} }
...@@ -682,3 +757,11 @@ SourceFrame.LineDecorator.prototype = { ...@@ -682,3 +757,11 @@ SourceFrame.LineDecorator.prototype = {
*/ */
decorate(uiSourceCode, textEditor) {} decorate(uiSourceCode, textEditor) {}
}; };
/**
* @typedef {{
* editorToRawLocation: function(number, number=):!Array<number>,
* rawToEditorLocation: function(number, number=):!Array<number>
* }}
*/
SourceFrame.Transformer;
...@@ -36,9 +36,10 @@ Sources.SourcesView = class extends UI.VBox { ...@@ -36,9 +36,10 @@ Sources.SourcesView = class extends UI.VBox {
this._historyManager = new Sources.EditingLocationHistoryManager(this, this.currentSourceFrame.bind(this)); this._historyManager = new Sources.EditingLocationHistoryManager(this, this.currentSourceFrame.bind(this));
this._toolbarContainerElement = this.element.createChild('div', 'sources-toolbar'); this._toolbarContainerElement = this.element.createChild('div', 'sources-toolbar');
this._toolbarEditorActions = new UI.Toolbar('', this._toolbarContainerElement); if (!Runtime.experiments.isEnabled('sourcesPrettyPrint')) {
this._toolbarEditorActions = new UI.Toolbar('', this._toolbarContainerElement);
self.runtime.allInstances(Sources.SourcesView.EditorAction).then(appendButtonsForExtensions.bind(this)); self.runtime.allInstances(Sources.SourcesView.EditorAction).then(appendButtonsForExtensions.bind(this));
}
/** /**
* @param {!Array.<!Sources.SourcesView.EditorAction>} actions * @param {!Array.<!Sources.SourcesView.EditorAction>} actions
* @this {Sources.SourcesView} * @this {Sources.SourcesView}
......
...@@ -636,14 +636,18 @@ TextEditor.CodeMirrorTextEditor = class extends UI.VBox { ...@@ -636,14 +636,18 @@ TextEditor.CodeMirrorTextEditor = class extends UI.VBox {
} }
/** /**
* @param {number} generation
* @return {boolean} * @return {boolean}
*/ */
isClean() { isClean(generation) {
return this._codeMirror.isClean(); return this._codeMirror.isClean(generation);
} }
/**
* @return {number}
*/
markClean() { markClean() {
this._codeMirror.markClean(); return this._codeMirror.changeGeneration(true);
} }
/** /**
...@@ -1216,6 +1220,9 @@ TextEditor.CodeMirrorTextEditor = class extends UI.VBox { ...@@ -1216,6 +1220,9 @@ TextEditor.CodeMirrorTextEditor = class extends UI.VBox {
this._enableLongLinesMode(); this._enableLongLinesMode();
else else
this._disableLongLinesMode(); this._disableLongLinesMode();
if (!this.isShowing())
this.refresh();
} }
/** /**
......
...@@ -114,6 +114,10 @@ ...@@ -114,6 +114,10 @@
white-space: nowrap; white-space: nowrap;
} }
.pretty-printed .CodeMirror-linenumber {
color: var( --accent-color-b);
}
.cm-highlight { .cm-highlight {
-webkit-animation: fadeout 2s 0s; -webkit-animation: fadeout 2s 0s;
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment