mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-28 12:33:35 +01:00
Make TypeScript extension ready for 2.0
This commit is contained in:
@@ -4,15 +4,24 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import { workspace, TextDocument, TextDocumentChangeEvent, TextDocumentContentChangeEvent, Disposable } from 'vscode';
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypescriptServiceClient } from '../typescriptService';
|
||||
import { ITypescriptServiceClient, APIVersion } from '../typescriptService';
|
||||
import { Delayer } from '../utils/async';
|
||||
|
||||
interface IDiagnosticRequestor {
|
||||
requestDiagnostic(filepath: string): void;
|
||||
}
|
||||
|
||||
const Mode2ScriptKind: Map<"TS" | "JS" | "TSX" | "JSX"> = {
|
||||
'typescript': 'TS',
|
||||
'typescriptreact': 'TSX',
|
||||
'javascript': 'JS',
|
||||
'javascriptreact': 'JSX'
|
||||
};
|
||||
|
||||
class SyncedBuffer {
|
||||
|
||||
private document: TextDocument;
|
||||
@@ -30,8 +39,19 @@ class SyncedBuffer {
|
||||
public open(): void {
|
||||
let args: Proto.OpenRequestArgs = {
|
||||
file: this.filepath,
|
||||
fileContent: this.document.getText()
|
||||
fileContent: this.document.getText(),
|
||||
};
|
||||
if (this.client.apiVersion === APIVersion.v2_0_0) {
|
||||
// we have no extension. So check the mode and
|
||||
// set the script kind accordningly.
|
||||
const ext = path.extname(this.filepath);
|
||||
if (ext === '') {
|
||||
const scriptKind = Mode2ScriptKind[this.document.languageId];
|
||||
if (scriptKind) {
|
||||
args.scriptKindName = scriptKind;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.client.execute('open', args, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { ReferenceProvider, Location, TextDocument, Position, Range, CancellationToken } from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypescriptServiceClient } from '../typescriptService';
|
||||
import { ITypescriptServiceClient, APIVersion } from '../typescriptService';
|
||||
|
||||
export default class TypeScriptReferenceSupport implements ReferenceProvider {
|
||||
|
||||
@@ -29,11 +29,15 @@ export default class TypeScriptReferenceSupport implements ReferenceProvider {
|
||||
if (!args.file) {
|
||||
return Promise.resolve<Location[]>([]);
|
||||
}
|
||||
const apiVersion = this.client.apiVersion;
|
||||
return this.client.execute('references', args, token).then((msg) => {
|
||||
let result: Location[] = [];
|
||||
let refs = msg.body.refs;
|
||||
for (let i = 0; i < refs.length; i++) {
|
||||
let ref = refs[i];
|
||||
if (!options.includeDeclaration && apiVersion >= APIVersion.v2_0_0 && ref.isDefinition) {
|
||||
continue;
|
||||
}
|
||||
let url = this.client.asUrl(ref.file);
|
||||
let location = new Location(
|
||||
url,
|
||||
|
||||
87
extensions/typescript/src/protocol.d.ts
vendored
87
extensions/typescript/src/protocol.d.ts
vendored
@@ -124,6 +124,10 @@ export interface ProjectInfo {
|
||||
* The list of normalized file name in the project, including 'lib.d.ts'
|
||||
*/
|
||||
fileNames?: string[];
|
||||
/**
|
||||
* Indicates if the project has a active language service instance
|
||||
*/
|
||||
languageServiceDisabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,6 +309,11 @@ export interface ReferencesResponseItem extends FileSpan {
|
||||
* True if reference is a write location, false otherwise.
|
||||
*/
|
||||
isWriteAccess: boolean;
|
||||
|
||||
/**
|
||||
* True if reference is a definition, false otherwise.
|
||||
*/
|
||||
isDefinition: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -430,6 +439,9 @@ export interface EditorOptions {
|
||||
/** Number of spaces to indent during formatting. Default value is 4. */
|
||||
indentSize?: number;
|
||||
|
||||
/** Number of additional spaces to indent during formatting to preserve base indentation (ex. script block indentation). Default value is 0. */
|
||||
baseIndentSize?: number;
|
||||
|
||||
/** The new line character to be used. Default value is the OS line delimiter. */
|
||||
newLineCharacter?: string;
|
||||
|
||||
@@ -445,7 +457,7 @@ export interface FormatOptions extends EditorOptions {
|
||||
/** Defines space handling after a comma delimiter. Default value is true. */
|
||||
insertSpaceAfterCommaDelimiter?: boolean;
|
||||
|
||||
/** Defines space handling after a semicolon in a for statemen. Default value is true */
|
||||
/** Defines space handling after a semicolon in a for statement. Default value is true */
|
||||
insertSpaceAfterSemicolonInForStatements?: boolean;
|
||||
|
||||
/** Defines space handling after a binary operator. Default value is true. */
|
||||
@@ -532,6 +544,11 @@ export interface OpenRequestArgs extends FileRequestArgs {
|
||||
* Then the known content will be used upon opening instead of the disk copy
|
||||
*/
|
||||
fileContent?: string;
|
||||
/**
|
||||
* Used to specify the script kind of the file explicitly. It could be one of the following:
|
||||
* "TS", "JS", "TSX", "JSX"
|
||||
*/
|
||||
scriptKindName?: "TS" | "JS" | "TSX" | "JSX";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -849,7 +866,7 @@ export interface SignatureHelpItem {
|
||||
prefixDisplayParts: SymbolDisplayPart[];
|
||||
|
||||
/**
|
||||
* The suffix disaply parts.
|
||||
* The suffix display parts.
|
||||
*/
|
||||
suffixDisplayParts: SymbolDisplayPart[];
|
||||
|
||||
@@ -904,7 +921,6 @@ export interface SignatureHelpItems {
|
||||
* Arguments of a signature help request.
|
||||
*/
|
||||
export interface SignatureHelpRequestArgs extends FileLocationRequestArgs {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -917,12 +933,38 @@ export interface SignatureHelpRequest extends FileLocationRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Repsonse object for a SignatureHelpRequest.
|
||||
* Response object for a SignatureHelpRequest.
|
||||
*/
|
||||
export interface SignatureHelpResponse extends Response {
|
||||
body?: SignatureHelpItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous request for semantic diagnostics of one file.
|
||||
*/
|
||||
export interface SemanticDiagnosticsSyncRequest extends FileRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Response object for synchronous sematic diagnostics request.
|
||||
*/
|
||||
export interface SemanticDiagnosticsSyncResponse extends Response {
|
||||
body?: Diagnostic[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous request for syntactic diagnostics of one file.
|
||||
*/
|
||||
export interface SyntacticDiagnosticsSyncRequest extends FileRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Response object for synchronous syntactic diagnostics request.
|
||||
*/
|
||||
export interface SyntacticDiagnosticsSyncResponse extends Response {
|
||||
body?: Diagnostic[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments for GeterrForProject request.
|
||||
*/
|
||||
@@ -984,7 +1026,7 @@ export interface GeterrRequest extends Request {
|
||||
*/
|
||||
export interface Diagnostic {
|
||||
/**
|
||||
* Starting file location at which text appies.
|
||||
* Starting file location at which text applies.
|
||||
*/
|
||||
start: Location;
|
||||
|
||||
@@ -1024,6 +1066,32 @@ export interface DiagnosticEvent extends Event {
|
||||
body?: DiagnosticEventBody;
|
||||
}
|
||||
|
||||
export interface ConfigFileDiagnosticEventBody {
|
||||
/**
|
||||
* The file which trigged the searching and error-checking of the config file
|
||||
*/
|
||||
triggerFile: string;
|
||||
|
||||
/**
|
||||
* The name of the found config file.
|
||||
*/
|
||||
configFile: string;
|
||||
|
||||
/**
|
||||
* An arry of diagnostic information items for the found config file.
|
||||
*/
|
||||
diagnostics: Diagnostic[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Event message for "configFileDiag" event type.
|
||||
* This event provides errors for a found config file.
|
||||
*/
|
||||
export interface ConfigFileDiagnosticEvent extends Event {
|
||||
body?: ConfigFileDiagnosticEventBody;
|
||||
event: "configFileDiag";
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments for reload request.
|
||||
*/
|
||||
@@ -1198,7 +1266,7 @@ export interface BraceRequest extends FileLocationRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* NavBar itesm request; value of command field is "navbar".
|
||||
* NavBar items request; value of command field is "navbar".
|
||||
* Return response giving the list of navigation bar entries
|
||||
* extracted from the requested file.
|
||||
*/
|
||||
@@ -1230,8 +1298,13 @@ export interface NavigationBarItem {
|
||||
* Optional children.
|
||||
*/
|
||||
childItems?: NavigationBarItem[];
|
||||
|
||||
/**
|
||||
* Number of levels deep this item should appear.
|
||||
*/
|
||||
indent: number;
|
||||
}
|
||||
|
||||
export interface NavBarResponse extends Response {
|
||||
body?: NavigationBarItem[];
|
||||
}
|
||||
}
|
||||
@@ -262,6 +262,10 @@ class LanguageProvider {
|
||||
}
|
||||
this.currentDiagnostics.set(Uri.file(file), diagnostics);
|
||||
}
|
||||
|
||||
public configFileDiagnosticsReceived(file: string, diagnostics: Diagnostic[]): void {
|
||||
this.currentDiagnostics.set(Uri.file(file), diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
class TypeScriptServiceClientHost implements ITypescriptServiceClientHost {
|
||||
@@ -351,6 +355,18 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost {
|
||||
}
|
||||
}
|
||||
|
||||
/* internal */ configFileDiagnosticsReceived(event: Proto.ConfigFileDiagnosticEvent): void {
|
||||
/* See https://github.com/Microsoft/TypeScript/issues/10384
|
||||
const body = event.body;
|
||||
if (body.diagnostics) {
|
||||
const language = this.findLanguage(body.triggerFile);
|
||||
if (language) {
|
||||
language.configFileDiagnosticsReceived(body.configFile, this.createMarkerDatas(body.diagnostics, language.diagnosticSource));
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private createMarkerDatas(diagnostics: Proto.Diagnostic[], source: string): Diagnostic[] {
|
||||
let result: Diagnostic[] = [];
|
||||
for (let diagnostic of diagnostics) {
|
||||
|
||||
@@ -11,9 +11,37 @@ import * as Proto from './protocol';
|
||||
export interface ITypescriptServiceClientHost {
|
||||
syntaxDiagnosticsReceived(event: Proto.DiagnosticEvent): void;
|
||||
semanticDiagnosticsReceived(event: Proto.DiagnosticEvent): void;
|
||||
configFileDiagnosticsReceived(event: Proto.ConfigFileDiagnosticEvent): void;
|
||||
populateService(): void;
|
||||
}
|
||||
|
||||
export enum APIVersion {
|
||||
v1_x = 1,
|
||||
v2_0_0 = 2
|
||||
};
|
||||
|
||||
export namespace APIVersion {
|
||||
export function fromString(value: string): APIVersion {
|
||||
if (!value) {
|
||||
return APIVersion.v1_x;
|
||||
}
|
||||
const index = value.indexOf('.');
|
||||
var major: number;
|
||||
if (index > 0) {
|
||||
major = parseInt(value.substr(0, index));
|
||||
} else {
|
||||
major = parseInt(value);
|
||||
}
|
||||
if (isNaN(major)) {
|
||||
return APIVersion.v1_x;
|
||||
}
|
||||
if (major >= 2) {
|
||||
return APIVersion.v2_0_0;
|
||||
}
|
||||
return APIVersion.v1_x;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITypescriptServiceClient {
|
||||
asAbsolutePath(resource: Uri): string;
|
||||
asUrl(filepath: string): Uri;
|
||||
@@ -21,26 +49,27 @@ export interface ITypescriptServiceClient {
|
||||
logTelemetry(eventName: string, properties?: { [prop: string]: string });
|
||||
|
||||
experimentalAutoBuild: boolean;
|
||||
apiVersion: APIVersion;
|
||||
|
||||
execute(command:'configure', args: Proto.ConfigureRequestArguments, token?: CancellationToken):Promise<Proto.ConfigureResponse>;
|
||||
execute(command:'open', args: Proto.OpenRequestArgs, expectedResult:boolean, token?: CancellationToken):Promise<any>;
|
||||
execute(command:'close', args: Proto.FileRequestArgs, expectedResult:boolean, token?: CancellationToken):Promise<any>;
|
||||
execute(command:'change', args: Proto.ChangeRequestArgs, expectedResult:boolean, token?: CancellationToken):Promise<any>;
|
||||
execute(command:'geterr', args: Proto.GeterrRequestArgs, expectedResult:boolean, token?: CancellationToken):Promise<any>;
|
||||
execute(command:'quickinfo', args: Proto.FileLocationRequestArgs, token?: CancellationToken):Promise<Proto.QuickInfoResponse>;
|
||||
execute(command:'completions', args: Proto.CompletionsRequestArgs, token?: CancellationToken):Promise<Proto.CompletionsResponse>;
|
||||
execute(commant:'completionEntryDetails', args: Proto.CompletionDetailsRequestArgs, token?: CancellationToken):Promise<Proto.CompletionDetailsResponse>;
|
||||
execute(commant:'signatureHelp', args: Proto.SignatureHelpRequestArgs, token?: CancellationToken):Promise<Proto.SignatureHelpResponse>;
|
||||
execute(command:'definition', args: Proto.FileLocationRequestArgs, token?: CancellationToken):Promise<Proto.DefinitionResponse>;
|
||||
execute(command:'references', args: Proto.FileLocationRequestArgs, token?: CancellationToken):Promise<Proto.ReferencesResponse>;
|
||||
execute(command:'navto', args: Proto.NavtoRequestArgs, token?: CancellationToken):Promise<Proto.NavtoResponse>;
|
||||
execute(command:'navbar', args: Proto.FileRequestArgs, token?: CancellationToken):Promise<Proto.NavBarResponse>;
|
||||
execute(command:'format', args: Proto.FormatRequestArgs, token?: CancellationToken):Promise<Proto.FormatResponse>;
|
||||
execute(command:'formatonkey', args: Proto.FormatOnKeyRequestArgs, token?: CancellationToken):Promise<Proto.FormatResponse>;
|
||||
execute(command:'rename', args: Proto.RenameRequestArgs, token?: CancellationToken): Promise<Proto.RenameResponse>;
|
||||
execute(command:'occurrences', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise<Proto.OccurrencesResponse>;
|
||||
execute(command:'projectInfo', args: Proto.ProjectInfoRequestArgs, token?: CancellationToken): Promise<Proto.ProjectInfoResponse>;
|
||||
execute(command:'reloadProjects', args: any, expectedResult:boolean, token?: CancellationToken): Promise<any>;
|
||||
execute(command:'reload', args: Proto.ReloadRequestArgs, expectedResult: boolean, token?: CancellationToken): Promise<any>;
|
||||
execute(command:string, args:any, expectedResult:boolean| CancellationToken, token?: CancellationToken):Promise<any>;
|
||||
execute(command: 'configure', args: Proto.ConfigureRequestArguments, token?: CancellationToken):Promise<Proto.ConfigureResponse>;
|
||||
execute(command: 'open', args: Proto.OpenRequestArgs, expectedResult:boolean, token?: CancellationToken):Promise<any>;
|
||||
execute(command: 'close', args: Proto.FileRequestArgs, expectedResult:boolean, token?: CancellationToken):Promise<any>;
|
||||
execute(command: 'change', args: Proto.ChangeRequestArgs, expectedResult:boolean, token?: CancellationToken):Promise<any>;
|
||||
execute(command: 'geterr', args: Proto.GeterrRequestArgs, expectedResult:boolean, token?: CancellationToken):Promise<any>;
|
||||
execute(command: 'quickinfo', args: Proto.FileLocationRequestArgs, token?: CancellationToken):Promise<Proto.QuickInfoResponse>;
|
||||
execute(command: 'completions', args: Proto.CompletionsRequestArgs, token?: CancellationToken):Promise<Proto.CompletionsResponse>;
|
||||
execute(commant: 'completionEntryDetails', args: Proto.CompletionDetailsRequestArgs, token?: CancellationToken):Promise<Proto.CompletionDetailsResponse>;
|
||||
execute(commant: 'signatureHelp', args: Proto.SignatureHelpRequestArgs, token?: CancellationToken):Promise<Proto.SignatureHelpResponse>;
|
||||
execute(command: 'definition', args: Proto.FileLocationRequestArgs, token?: CancellationToken):Promise<Proto.DefinitionResponse>;
|
||||
execute(command: 'references', args: Proto.FileLocationRequestArgs, token?: CancellationToken):Promise<Proto.ReferencesResponse>;
|
||||
execute(command: 'navto', args: Proto.NavtoRequestArgs, token?: CancellationToken):Promise<Proto.NavtoResponse>;
|
||||
execute(command: 'navbar', args: Proto.FileRequestArgs, token?: CancellationToken):Promise<Proto.NavBarResponse>;
|
||||
execute(command: 'format', args: Proto.FormatRequestArgs, token?: CancellationToken):Promise<Proto.FormatResponse>;
|
||||
execute(command: 'formatonkey', args: Proto.FormatOnKeyRequestArgs, token?: CancellationToken):Promise<Proto.FormatResponse>;
|
||||
execute(command: 'rename', args: Proto.RenameRequestArgs, token?: CancellationToken): Promise<Proto.RenameResponse>;
|
||||
execute(command: 'occurrences', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise<Proto.OccurrencesResponse>;
|
||||
execute(command: 'projectInfo', args: Proto.ProjectInfoRequestArgs, token?: CancellationToken): Promise<Proto.ProjectInfoResponse>;
|
||||
execute(command: 'reloadProjects', args: any, expectedResult:boolean, token?: CancellationToken): Promise<any>;
|
||||
execute(command: 'reload', args: Proto.ReloadRequestArgs, expectedResult: boolean, token?: CancellationToken): Promise<any>;
|
||||
execute(command: string, args: any, expectedResult: boolean | CancellationToken, token?: CancellationToken): Promise<any>;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { Reader } from './utils/wireProtocol';
|
||||
|
||||
import { workspace, window, Uri, CancellationToken, OutputChannel } from 'vscode';
|
||||
import * as Proto from './protocol';
|
||||
import { ITypescriptServiceClient, ITypescriptServiceClientHost } from './typescriptService';
|
||||
import { ITypescriptServiceClient, ITypescriptServiceClientHost, APIVersion } from './typescriptService';
|
||||
|
||||
import * as VersionStatus from './utils/versionStatus';
|
||||
|
||||
@@ -90,6 +90,7 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
|
||||
private callbacks: CallbackMap;
|
||||
|
||||
private _packageInfo: IPackageInfo;
|
||||
private _apiVersion: APIVersion;
|
||||
private telemetryReporter: TelemetryReporter;
|
||||
|
||||
constructor(host: ITypescriptServiceClientHost, storagePath: string) {
|
||||
@@ -115,6 +116,7 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
|
||||
const configuration = workspace.getConfiguration();
|
||||
this.tsdk = configuration.get<string>('typescript.tsdk', null);
|
||||
this._experimentalAutoBuild = configuration.get<boolean>('typescript.tsserver.experimentalAutoBuild', false);
|
||||
this._apiVersion = APIVersion.v1_x;
|
||||
this.trace = this.readTrace();
|
||||
workspace.onDidChangeConfiguration(() => {
|
||||
this.trace = this.readTrace();
|
||||
@@ -145,6 +147,10 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
|
||||
return this._experimentalAutoBuild;
|
||||
}
|
||||
|
||||
public get apiVersion(): APIVersion {
|
||||
return this._apiVersion;
|
||||
}
|
||||
|
||||
public onReady(): Promise<void> {
|
||||
return this._onReady.promise;
|
||||
}
|
||||
@@ -202,8 +208,15 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
|
||||
return;
|
||||
}
|
||||
|
||||
let label = this.getTypeScriptVersion(modulePath);
|
||||
let tooltip = modulePath;
|
||||
let version = this.getTypeScriptVersion(modulePath);
|
||||
if (!version) {
|
||||
version = workspace.getConfiguration().get<string>('typescript.tsdk_version', undefined);
|
||||
}
|
||||
if (version) {
|
||||
this._apiVersion = APIVersion.fromString(version);
|
||||
}
|
||||
const label = version || localize('versionNumber.custom' ,'custom');
|
||||
const tooltip = modulePath;
|
||||
VersionStatus.enable(!!this.tsdk);
|
||||
VersionStatus.setInfo(label, tooltip);
|
||||
|
||||
@@ -264,26 +277,25 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
|
||||
}
|
||||
|
||||
private getTypeScriptVersion(serverPath: string): string {
|
||||
const custom = localize('versionNumber.custom' ,'custom');
|
||||
let p = serverPath.split(path.sep);
|
||||
if (p.length <= 2) {
|
||||
return custom;
|
||||
return undefined;
|
||||
}
|
||||
let p2 = p.slice(0, -2);
|
||||
let modulePath = p2.join(path.sep);
|
||||
let fileName = path.join(modulePath, 'package.json');
|
||||
if (!fs.existsSync(fileName)) {
|
||||
return custom;
|
||||
return undefined;
|
||||
}
|
||||
let contents = fs.readFileSync(fileName).toString();
|
||||
let desc = null;
|
||||
try {
|
||||
desc = JSON.parse(contents);
|
||||
} catch(err) {
|
||||
return custom;
|
||||
return undefined;
|
||||
}
|
||||
if (!desc.version) {
|
||||
return custom;
|
||||
return undefined;
|
||||
}
|
||||
return desc.version;
|
||||
}
|
||||
@@ -431,10 +443,11 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
|
||||
let event: Proto.Event = <Proto.Event>message;
|
||||
this.traceEvent(event);
|
||||
if (event.event === 'syntaxDiag') {
|
||||
this.host.syntaxDiagnosticsReceived(event);
|
||||
}
|
||||
if (event.event === 'semanticDiag') {
|
||||
this.host.semanticDiagnosticsReceived(event);
|
||||
this.host.syntaxDiagnosticsReceived(event as Proto.DiagnosticEvent);
|
||||
} else if (event.event === 'semanticDiag') {
|
||||
this.host.semanticDiagnosticsReceived(event as Proto.DiagnosticEvent);
|
||||
} else if (event.event === 'configFileDiag') {
|
||||
this.host.configFileDiagnosticsReceived(event as Proto.ConfigFileDiagnosticEvent);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unknown message type ' + message.type + ' recevied');
|
||||
|
||||
Reference in New Issue
Block a user