mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-22 09:35:37 -05:00
Refresh master with initial release/0.24 snapshot (#332)
* Initial port of release/0.24 source code * Fix additional headers * Fix a typo in launch.json
This commit is contained in:
@@ -9,22 +9,19 @@ import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import * as vscode from 'vscode';
|
||||
import * as TypeConverters from 'vs/workbench/api/node/extHostTypeConverters';
|
||||
import { Range, Disposable, CompletionList, CompletionItem, SnippetString } from 'vs/workbench/api/node/extHostTypes';
|
||||
import { Range, Disposable, CompletionList, SnippetString, Color } from 'vs/workbench/api/node/extHostTypes';
|
||||
import { ISingleEditOperation } from 'vs/editor/common/editorCommon';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService';
|
||||
import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments';
|
||||
import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/node/extHostCommands';
|
||||
import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics';
|
||||
import { IWorkspaceSymbolProvider } from 'vs/workbench/parts/search/common/search';
|
||||
import { asWinJsPromise } from 'vs/base/common/async';
|
||||
import { MainContext, MainThreadTelemetryShape, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IRawColorFormatMap, IMainContext } from './extHost.protocol';
|
||||
import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IExtHostSuggestResult, IExtHostSuggestion, IWorkspaceSymbols, IWorkspaceSymbol, IdObject } from './extHost.protocol';
|
||||
import { regExpLeadsToEndlessLoop } from 'vs/base/common/strings';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { containsCommandLink } from 'vs/base/common/htmlContent';
|
||||
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
|
||||
// --- adapter
|
||||
|
||||
@@ -179,7 +176,6 @@ class HoverAdapter {
|
||||
constructor(
|
||||
private readonly _documents: ExtHostDocuments,
|
||||
private readonly _provider: vscode.HoverProvider,
|
||||
private readonly _telemetryLog: (name: string, data: object) => void,
|
||||
) {
|
||||
//
|
||||
}
|
||||
@@ -200,14 +196,7 @@ class HoverAdapter {
|
||||
value.range = new Range(pos, pos);
|
||||
}
|
||||
|
||||
const result = TypeConverters.fromHover(value);
|
||||
|
||||
// we wanna know which extension uses command links
|
||||
// because that is a potential trick-attack on users
|
||||
if (result.contents.some(h => containsCommandLink(h.value))) {
|
||||
this._telemetryLog('usesCommandLink', { from: 'hover' });
|
||||
}
|
||||
return result;
|
||||
return TypeConverters.fromHover(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -273,7 +262,7 @@ class QuickFixAdapter {
|
||||
private _diagnostics: ExtHostDiagnostics;
|
||||
private _provider: vscode.CodeActionProvider;
|
||||
|
||||
constructor(documents: ExtHostDocuments, commands: CommandsConverter, diagnostics: ExtHostDiagnostics, heapService: ExtHostHeapService, provider: vscode.CodeActionProvider) {
|
||||
constructor(documents: ExtHostDocuments, commands: CommandsConverter, diagnostics: ExtHostDiagnostics, provider: vscode.CodeActionProvider) {
|
||||
this._documents = documents;
|
||||
this._commands = commands;
|
||||
this._diagnostics = diagnostics;
|
||||
@@ -283,13 +272,13 @@ class QuickFixAdapter {
|
||||
provideCodeActions(resource: URI, range: IRange): TPromise<modes.Command[]> {
|
||||
|
||||
const doc = this._documents.getDocumentData(resource).document;
|
||||
const ran = TypeConverters.toRange(range);
|
||||
const ran = <vscode.Range>TypeConverters.toRange(range);
|
||||
const allDiagnostics: vscode.Diagnostic[] = [];
|
||||
|
||||
this._diagnostics.forEach(collection => {
|
||||
if (collection.has(resource)) {
|
||||
for (let diagnostic of collection.get(resource)) {
|
||||
if (diagnostic.range.intersection(ran)) {
|
||||
if (ran.contains(diagnostic.range)) {
|
||||
allDiagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -378,45 +367,56 @@ class OnTypeFormattingAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
class NavigateTypeAdapter {
|
||||
|
||||
class NavigateTypeAdapter implements IWorkspaceSymbolProvider {
|
||||
private readonly _symbolCache: { [id: number]: vscode.SymbolInformation } = Object.create(null);
|
||||
private readonly _resultCache: { [id: number]: [number, number] } = Object.create(null);
|
||||
private readonly _provider: vscode.WorkspaceSymbolProvider;
|
||||
|
||||
private _provider: vscode.WorkspaceSymbolProvider;
|
||||
private _heapService: ExtHostHeapService;
|
||||
|
||||
constructor(provider: vscode.WorkspaceSymbolProvider, heapService: ExtHostHeapService) {
|
||||
constructor(provider: vscode.WorkspaceSymbolProvider) {
|
||||
this._provider = provider;
|
||||
this._heapService = heapService;
|
||||
}
|
||||
|
||||
provideWorkspaceSymbols(search: string): TPromise<modes.SymbolInformation[]> {
|
||||
|
||||
provideWorkspaceSymbols(search: string): TPromise<IWorkspaceSymbols> {
|
||||
const result: IWorkspaceSymbols = IdObject.mixin({ symbols: [] });
|
||||
return asWinJsPromise(token => this._provider.provideWorkspaceSymbols(search, token)).then(value => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => {
|
||||
const id = this._heapService.keep(item);
|
||||
const result = TypeConverters.fromSymbolInformation(item);
|
||||
return ObjectIdentifier.mixin(result, id);
|
||||
});
|
||||
if (!isFalsyOrEmpty(value)) {
|
||||
for (const item of value) {
|
||||
const symbol = IdObject.mixin(TypeConverters.fromSymbolInformation(item));
|
||||
this._symbolCache[symbol._id] = item;
|
||||
result.symbols.push(symbol);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}).then(() => {
|
||||
this._resultCache[result._id] = [result.symbols[0]._id, result.symbols[result.symbols.length - 1]._id];
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
resolveWorkspaceSymbol(item: modes.SymbolInformation): TPromise<modes.SymbolInformation> {
|
||||
resolveWorkspaceSymbol(symbol: IWorkspaceSymbol): TPromise<IWorkspaceSymbol> {
|
||||
|
||||
if (typeof this._provider.resolveWorkspaceSymbol !== 'function') {
|
||||
return TPromise.as(item);
|
||||
return TPromise.as(symbol);
|
||||
}
|
||||
|
||||
const symbolInfo = this._heapService.get<vscode.SymbolInformation>(ObjectIdentifier.of(item));
|
||||
if (symbolInfo) {
|
||||
return asWinJsPromise(token => this._provider.resolveWorkspaceSymbol(symbolInfo, token)).then(value => {
|
||||
return value && TypeConverters.fromSymbolInformation(value);
|
||||
const item = this._symbolCache[symbol._id];
|
||||
if (item) {
|
||||
return asWinJsPromise(token => this._provider.resolveWorkspaceSymbol(item, token)).then(value => {
|
||||
return value && mixin(symbol, TypeConverters.fromSymbolInformation(value), true);
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
releaseWorkspaceSymbols(id: number): any {
|
||||
const range = this._resultCache[id];
|
||||
if (range) {
|
||||
for (let [from, to] = range; from <= to; from++) {
|
||||
delete this._symbolCache[from];
|
||||
}
|
||||
delete this._resultCache[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RenameAdapter {
|
||||
@@ -447,7 +447,7 @@ class RenameAdapter {
|
||||
let [uri, textEdits] = entry;
|
||||
for (let textEdit of textEdits) {
|
||||
result.edits.push({
|
||||
resource: <URI>uri,
|
||||
resource: uri,
|
||||
newText: textEdit.newText,
|
||||
range: TypeConverters.fromRange(textEdit.range)
|
||||
});
|
||||
@@ -469,26 +469,36 @@ class RenameAdapter {
|
||||
|
||||
class SuggestAdapter {
|
||||
|
||||
static supportsResolving(provider: vscode.CompletionItemProvider): boolean {
|
||||
return typeof provider.resolveCompletionItem === 'function';
|
||||
}
|
||||
|
||||
private _documents: ExtHostDocuments;
|
||||
private _commands: CommandsConverter;
|
||||
private _heapService: ExtHostHeapService;
|
||||
private _provider: vscode.CompletionItemProvider;
|
||||
|
||||
constructor(documents: ExtHostDocuments, commands: CommandsConverter, heapService: ExtHostHeapService, provider: vscode.CompletionItemProvider) {
|
||||
private _cache = new Map<number, vscode.CompletionItem[]>();
|
||||
private _idPool = 0;
|
||||
|
||||
constructor(documents: ExtHostDocuments, commands: CommandsConverter, provider: vscode.CompletionItemProvider) {
|
||||
this._documents = documents;
|
||||
this._commands = commands;
|
||||
this._heapService = heapService;
|
||||
this._provider = provider;
|
||||
}
|
||||
|
||||
provideCompletionItems(resource: URI, position: IPosition): TPromise<modes.ISuggestResult> {
|
||||
provideCompletionItems(resource: URI, position: IPosition, context: modes.SuggestContext): TPromise<IExtHostSuggestResult> {
|
||||
|
||||
const doc = this._documents.getDocumentData(resource).document;
|
||||
const pos = TypeConverters.toPosition(position);
|
||||
|
||||
return asWinJsPromise<vscode.CompletionItem[] | vscode.CompletionList>(token => this._provider.provideCompletionItems(doc, pos, token)).then(value => {
|
||||
return asWinJsPromise<vscode.CompletionItem[] | vscode.CompletionList>(token => {
|
||||
return this._provider.provideCompletionItems(doc, pos, token, TypeConverters.CompletionContext.from(context));
|
||||
}).then(value => {
|
||||
|
||||
const result: modes.ISuggestResult = {
|
||||
const _id = this._idPool++;
|
||||
|
||||
const result: IExtHostSuggestResult = {
|
||||
_id,
|
||||
suggestions: [],
|
||||
};
|
||||
|
||||
@@ -509,19 +519,15 @@ class SuggestAdapter {
|
||||
const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) || new Range(pos, pos))
|
||||
.with({ end: pos });
|
||||
|
||||
for (const item of list.items) {
|
||||
|
||||
const suggestion = this._convertCompletionItem(item, pos, wordRangeBeforePos);
|
||||
|
||||
// bad completion item
|
||||
if (!suggestion) {
|
||||
// converter did warn
|
||||
continue;
|
||||
for (let i = 0; i < list.items.length; i++) {
|
||||
const suggestion = this._convertCompletionItem(list.items[i], pos, wordRangeBeforePos, i, _id);
|
||||
// check for bad completion item
|
||||
// for the converter did warn
|
||||
if (suggestion) {
|
||||
result.suggestions.push(suggestion);
|
||||
}
|
||||
|
||||
ObjectIdentifier.mixin(suggestion, this._heapService.keep(item));
|
||||
result.suggestions.push(suggestion);
|
||||
}
|
||||
this._cache.set(_id, list.items);
|
||||
|
||||
return result;
|
||||
});
|
||||
@@ -533,8 +539,8 @@ class SuggestAdapter {
|
||||
return TPromise.as(suggestion);
|
||||
}
|
||||
|
||||
const id = ObjectIdentifier.of(suggestion);
|
||||
const item = this._heapService.get<CompletionItem>(id);
|
||||
const { _parentId, _id } = (<IExtHostSuggestion>suggestion);
|
||||
const item = this._cache.has(_parentId) && this._cache.get(_parentId)[_id];
|
||||
if (!item) {
|
||||
return TPromise.as(suggestion);
|
||||
}
|
||||
@@ -548,7 +554,7 @@ class SuggestAdapter {
|
||||
const doc = this._documents.getDocumentData(resource).document;
|
||||
const pos = TypeConverters.toPosition(position);
|
||||
const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) || new Range(pos, pos)).with({ end: pos });
|
||||
const newSuggestion = this._convertCompletionItem(resolvedItem, pos, wordRangeBeforePos);
|
||||
const newSuggestion = this._convertCompletionItem(resolvedItem, pos, wordRangeBeforePos, _id, _parentId);
|
||||
if (newSuggestion) {
|
||||
mixin(suggestion, newSuggestion, true);
|
||||
}
|
||||
@@ -557,13 +563,20 @@ class SuggestAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
private _convertCompletionItem(item: vscode.CompletionItem, position: vscode.Position, defaultRange: vscode.Range): modes.ISuggestion {
|
||||
releaseCompletionItems(id: number): any {
|
||||
this._cache.delete(id);
|
||||
}
|
||||
|
||||
private _convertCompletionItem(item: vscode.CompletionItem, position: vscode.Position, defaultRange: vscode.Range, _id: number, _parentId: number): IExtHostSuggestion {
|
||||
if (typeof item.label !== 'string' || item.label.length === 0) {
|
||||
console.warn('INVALID text edit -> must have at least a label');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result: modes.ISuggestion = {
|
||||
const result: IExtHostSuggestion = {
|
||||
//
|
||||
_id,
|
||||
_parentId,
|
||||
//
|
||||
label: item.label,
|
||||
type: TypeConverters.CompletionItemKind.from(item.kind),
|
||||
@@ -693,8 +706,6 @@ class LinkProviderAdapter {
|
||||
|
||||
class ColorProviderAdapter {
|
||||
|
||||
private static _colorFormatHandlePool: number = 0;
|
||||
|
||||
constructor(
|
||||
private _proxy: MainThreadLanguageFeaturesShape,
|
||||
private _documents: ExtHostDocuments,
|
||||
@@ -709,39 +720,25 @@ class ColorProviderAdapter {
|
||||
return [];
|
||||
}
|
||||
|
||||
const newRawColorFormats: IRawColorFormatMap = [];
|
||||
const getFormatId = (format: string) => {
|
||||
let id = this._colorFormatCache.get(format);
|
||||
|
||||
if (typeof id !== 'number') {
|
||||
id = ColorProviderAdapter._colorFormatHandlePool++;
|
||||
this._colorFormatCache.set(format, id);
|
||||
newRawColorFormats.push([id, format]);
|
||||
}
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
const colorInfos: IRawColorInfo[] = colors.map(ci => {
|
||||
const availableFormats = ci.availableFormats.map(format => {
|
||||
if (typeof format === 'string') {
|
||||
return getFormatId(format);
|
||||
} else {
|
||||
return [getFormatId(format.opaque), getFormatId(format.transparent)] as [number, number];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
color: [ci.color.red, ci.color.green, ci.color.blue, ci.color.alpha] as [number, number, number, number],
|
||||
availableFormats: availableFormats,
|
||||
range: TypeConverters.fromRange(ci.range)
|
||||
};
|
||||
});
|
||||
|
||||
this._proxy.$registerColorFormats(newRawColorFormats);
|
||||
return colorInfos;
|
||||
});
|
||||
}
|
||||
|
||||
provideColorPresentations(resource: URI, raw: IRawColorInfo): TPromise<modes.IColorPresentation[]> {
|
||||
const document = this._documents.getDocumentData(resource).document;
|
||||
const range = TypeConverters.toRange(raw.range);
|
||||
const color = new Color(raw.color[0], raw.color[1], raw.color[2], raw.color[3]);
|
||||
return asWinJsPromise(token => this._provider.provideColorPresentations(color, { document, range }, token)).then(value => {
|
||||
return value.map(TypeConverters.ColorPresentation.from);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type Adapter = OutlineAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter
|
||||
@@ -754,7 +751,6 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
|
||||
private static _handlePool: number = 0;
|
||||
|
||||
private _proxy: MainThreadLanguageFeaturesShape;
|
||||
private _telemetry: MainThreadTelemetryShape;
|
||||
private _documents: ExtHostDocuments;
|
||||
private _commands: ExtHostCommands;
|
||||
private _heapService: ExtHostHeapService;
|
||||
@@ -770,7 +766,6 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
|
||||
diagnostics: ExtHostDiagnostics
|
||||
) {
|
||||
this._proxy = mainContext.get(MainContext.MainThreadLanguageFeatures);
|
||||
this._telemetry = mainContext.get(MainContext.MainThreadTelemetry);
|
||||
this._documents = documents;
|
||||
this._commands = commands;
|
||||
this._heapService = heapMonitor;
|
||||
@@ -874,10 +869,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
|
||||
|
||||
registerHoverProvider(selector: vscode.DocumentSelector, provider: vscode.HoverProvider, extensionId?: string): vscode.Disposable {
|
||||
const handle = this._nextHandle();
|
||||
this._adapter.set(handle, new HoverAdapter(this._documents, provider, once((name, data) => {
|
||||
data['extension'] = extensionId;
|
||||
this._telemetry.$publicLog(name, data);
|
||||
})));
|
||||
this._adapter.set(handle, new HoverAdapter(this._documents, provider));
|
||||
this._proxy.$registerHoverProvider(handle, selector);
|
||||
return this._createDisposable(handle);
|
||||
}
|
||||
@@ -916,7 +908,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
|
||||
|
||||
registerCodeActionProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider): vscode.Disposable {
|
||||
const handle = this._nextHandle();
|
||||
this._adapter.set(handle, new QuickFixAdapter(this._documents, this._commands.converter, this._diagnostics, this._heapService, provider));
|
||||
this._adapter.set(handle, new QuickFixAdapter(this._documents, this._commands.converter, this._diagnostics, provider));
|
||||
this._proxy.$registerQuickFixSupport(handle, selector);
|
||||
return this._createDisposable(handle);
|
||||
}
|
||||
@@ -964,19 +956,23 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
|
||||
|
||||
registerWorkspaceSymbolProvider(provider: vscode.WorkspaceSymbolProvider): vscode.Disposable {
|
||||
const handle = this._nextHandle();
|
||||
this._adapter.set(handle, new NavigateTypeAdapter(provider, this._heapService));
|
||||
this._adapter.set(handle, new NavigateTypeAdapter(provider));
|
||||
this._proxy.$registerNavigateTypeSupport(handle);
|
||||
return this._createDisposable(handle);
|
||||
}
|
||||
|
||||
$provideWorkspaceSymbols(handle: number, search: string): TPromise<modes.SymbolInformation[]> {
|
||||
$provideWorkspaceSymbols(handle: number, search: string): TPromise<IWorkspaceSymbols> {
|
||||
return this._withAdapter(handle, NavigateTypeAdapter, adapter => adapter.provideWorkspaceSymbols(search));
|
||||
}
|
||||
|
||||
$resolveWorkspaceSymbol(handle: number, symbol: modes.SymbolInformation): TPromise<modes.SymbolInformation> {
|
||||
$resolveWorkspaceSymbol(handle: number, symbol: IWorkspaceSymbol): TPromise<IWorkspaceSymbol> {
|
||||
return this._withAdapter(handle, NavigateTypeAdapter, adapter => adapter.resolveWorkspaceSymbol(symbol));
|
||||
}
|
||||
|
||||
$releaseWorkspaceSymbols(handle: number, id: number) {
|
||||
this._withAdapter(handle, NavigateTypeAdapter, adapter => adapter.releaseWorkspaceSymbols(id));
|
||||
}
|
||||
|
||||
// --- rename
|
||||
|
||||
registerRenameProvider(selector: vscode.DocumentSelector, provider: vscode.RenameProvider): vscode.Disposable {
|
||||
@@ -994,19 +990,23 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
|
||||
|
||||
registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, triggerCharacters: string[]): vscode.Disposable {
|
||||
const handle = this._nextHandle();
|
||||
this._adapter.set(handle, new SuggestAdapter(this._documents, this._commands.converter, this._heapService, provider));
|
||||
this._proxy.$registerSuggestSupport(handle, selector, triggerCharacters);
|
||||
this._adapter.set(handle, new SuggestAdapter(this._documents, this._commands.converter, provider));
|
||||
this._proxy.$registerSuggestSupport(handle, selector, triggerCharacters, SuggestAdapter.supportsResolving(provider));
|
||||
return this._createDisposable(handle);
|
||||
}
|
||||
|
||||
$provideCompletionItems(handle: number, resource: URI, position: IPosition): TPromise<modes.ISuggestResult> {
|
||||
return this._withAdapter(handle, SuggestAdapter, adapter => adapter.provideCompletionItems(resource, position));
|
||||
$provideCompletionItems(handle: number, resource: URI, position: IPosition, context: modes.SuggestContext): TPromise<IExtHostSuggestResult> {
|
||||
return this._withAdapter(handle, SuggestAdapter, adapter => adapter.provideCompletionItems(resource, position, context));
|
||||
}
|
||||
|
||||
$resolveCompletionItem(handle: number, resource: URI, position: IPosition, suggestion: modes.ISuggestion): TPromise<modes.ISuggestion> {
|
||||
return this._withAdapter(handle, SuggestAdapter, adapter => adapter.resolveCompletionItem(resource, position, suggestion));
|
||||
}
|
||||
|
||||
$releaseCompletionItems(handle: number, id: number): void {
|
||||
this._withAdapter(handle, SuggestAdapter, adapter => adapter.releaseCompletionItems(id));
|
||||
}
|
||||
|
||||
// --- parameter hints
|
||||
|
||||
registerSignatureHelpProvider(selector: vscode.DocumentSelector, provider: vscode.SignatureHelpProvider, triggerCharacters: string[]): vscode.Disposable {
|
||||
@@ -1048,6 +1048,10 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
|
||||
return this._withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColors(resource));
|
||||
}
|
||||
|
||||
$provideColorPresentations(handle: number, resource: URI, colorInfo: IRawColorInfo): TPromise<modes.IColorPresentation[]> {
|
||||
return this._withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColorPresentations(resource, colorInfo));
|
||||
}
|
||||
|
||||
// --- configuration
|
||||
|
||||
setLanguageConfiguration(languageId: string, configuration: vscode.LanguageConfiguration): vscode.Disposable {
|
||||
|
||||
Reference in New Issue
Block a user