[ci] yarn format

This commit is contained in:
matthewp 2021-08-10 13:31:09 +00:00 committed by GitHub Actions
parent 2c5380a266
commit 16790aee7b
10 changed files with 280 additions and 340 deletions

View file

@ -24,4 +24,4 @@ interface Astro {
declare const Astro: Astro;
export default function(): string;
export default function (): string;

View file

@ -67,8 +67,8 @@ export function startServer() {
hoverProvider: true,
signatureHelpProvider: {
triggerCharacters: ['(', ',', '<'],
retriggerCharacters: [')']
}
retriggerCharacters: [')'],
},
},
};
});

View file

@ -7,7 +7,7 @@ import type {
Position,
SignatureHelp,
SignatureHelpContext,
TextDocumentIdentifier
TextDocumentIdentifier,
} from 'vscode-languageserver';
import type { DocumentManager } from '../core/documents';
import type * as d from './interfaces';
@ -74,7 +74,7 @@ export class PluginHost {
async doHover(textDocument: TextDocumentIdentifier, position: Position): Promise<Hover | null> {
const document = this.getDocument(textDocument.uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
throw new Error('Cannot call methods on an unopened document');
}
return this.execute<Hover>('doHover', [document, position], ExecuteMode.FirstNonNull);
@ -121,16 +121,12 @@ export class PluginHost {
context: SignatureHelpContext | undefined,
cancellationToken: CancellationToken
): Promise<SignatureHelp | null> {
const document = this.getDocument(textDocument.uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
const document = this.getDocument(textDocument.uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
return await this.execute<any>(
'getSignatureHelp',
[document, position, context, cancellationToken],
ExecuteMode.FirstNonNull
);
return await this.execute<any>('getSignatureHelp', [document, position, context, cancellationToken], ExecuteMode.FirstNonNull);
}
onWatchFileChanges(onWatchFileChangesParams: any[]): void {
@ -159,10 +155,12 @@ export class PluginHost {
}
return null;
case ExecuteMode.Collect:
return Promise.all(plugins.map((plugin) => {
let ret = this.tryExecutePlugin(plugin, name, args, []);
return ret;
}));
return Promise.all(
plugins.map((plugin) => {
let ret = this.tryExecutePlugin(plugin, name, args, []);
return ret;
})
);
case ExecuteMode.None:
await Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, null)));
return;

View file

@ -6,7 +6,7 @@ import { isInTag, positionAt, offsetAt } from '../../core/documents/utils';
import { pathToUrl } from '../../utils';
import { getScriptKindFromFileName, isAstroFilePath, toVirtualAstroFilePath } from './utils';
const ASTRO_DEFINITION = readFileSync(require.resolve('../../../astro.d.ts'));
const ASTRO_DEFINITION = readFileSync(require.resolve('../../../astro.d.ts'));
/**
* The mapper to get from original snapshot positions to generated and vice versa.
@ -75,9 +75,11 @@ class AstroDocumentSnapshot implements DocumentSnapshot {
/** @internal */
private transformContent(content: string) {
return content.replace(/---/g, '///') +
// Add TypeScript definitions
ASTRO_DEFINITION;
return (
content.replace(/---/g, '///') +
// Add TypeScript definitions
ASTRO_DEFINITION
);
}
get filePath() {
@ -139,9 +141,11 @@ export class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFrag
/** @internal */
private transformContent(content: string) {
return content.replace(/---/g, '///') +
// Add TypeScript definitions
ASTRO_DEFINITION;
return (
content.replace(/---/g, '///') +
// Add TypeScript definitions
ASTRO_DEFINITION
);
}
getText(start: number, end: number) {

View file

@ -1,11 +1,6 @@
import type { ConfigManager } from '../../core/config';
import type { CompletionsProvider, AppCompletionItem, AppCompletionList } from '../interfaces';
import type {
CancellationToken,
Hover,
SignatureHelp,
SignatureHelpContext
} from 'vscode-languageserver';
import type { CancellationToken, Hover, SignatureHelp, SignatureHelpContext } from 'vscode-languageserver';
import { join as pathJoin, dirname as pathDirname } from 'path';
import { Document, DocumentManager, isInsideFrontmatter } from '../../core/documents';
import { SourceFile, ImportDeclaration, Node, SyntaxKind } from 'typescript';
@ -134,18 +129,8 @@ export class TypeScriptPlugin implements CompletionsProvider {
}
}
async getSignatureHelp(
document: Document,
position: Position,
context: SignatureHelpContext | undefined,
cancellationToken?: CancellationToken
): Promise<SignatureHelp | null> {
return this.signatureHelpProvider.getSignatureHelp(
document,
position,
context,
cancellationToken
);
async getSignatureHelp(document: Document, position: Position, context: SignatureHelpContext | undefined, cancellationToken?: CancellationToken): Promise<SignatureHelp | null> {
return this.signatureHelpProvider.getSignatureHelp(document, position, context, cancellationToken);
}
/**

View file

@ -34,8 +34,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
const offset = document.offsetAt(position);
const entries =
lang.getCompletionsAtPosition(fragment.filePath, offset, completionOptions)?.entries || [];
const entries = lang.getCompletionsAtPosition(fragment.filePath, offset, completionOptions)?.entries || [];
const completionItems = entries
.map((entry: ts.CompletionEntry) => this.toCompletionItem(fragment, entry, document.uri, position, new Set()))

View file

@ -7,36 +7,34 @@ import { getMarkdownDocumentation } from '../previewer';
import { convertRange, toVirtualAstroFilePath } from '../utils';
export class HoverProviderImpl implements HoverProvider {
constructor(private readonly lang: LanguageServiceManager) {}
constructor(private readonly lang: LanguageServiceManager) {}
async doHover(document: Document, position: Position): Promise<Hover | null> {
const { lang, tsDoc } = await this.getLSAndTSDoc(document);
const fragment = await tsDoc.getFragment();
async doHover(document: Document, position: Position): Promise<Hover | null> {
const { lang, tsDoc } = await this.getLSAndTSDoc(document);
const fragment = await tsDoc.getFragment();
const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
const filePath = toVirtualAstroFilePath(tsDoc.filePath);
let info = lang.getQuickInfoAtPosition(filePath, offset);
if (!info) {
return null;
}
const textSpan = info.textSpan;
const declaration = ts.displayPartsToString(info.displayParts);
const documentation = getMarkdownDocumentation(info.documentation, info.tags);
// https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
const contents = ['```typescript', declaration, '```']
.concat(documentation ? ['---', documentation] : [])
.join('\n');
return mapObjWithRangeToOriginal(fragment, {
range: convertRange(fragment, textSpan),
contents
});
const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
const filePath = toVirtualAstroFilePath(tsDoc.filePath);
let info = lang.getQuickInfoAtPosition(filePath, offset);
if (!info) {
return null;
}
private async getLSAndTSDoc(document: Document) {
return this.lang.getTypeScriptDoc(document);
}
}
const textSpan = info.textSpan;
const declaration = ts.displayPartsToString(info.displayParts);
const documentation = getMarkdownDocumentation(info.documentation, info.tags);
// https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
const contents = ['```typescript', declaration, '```'].concat(documentation ? ['---', documentation] : []).join('\n');
return mapObjWithRangeToOriginal(fragment, {
range: convertRange(fragment, textSpan),
contents,
});
}
private async getLSAndTSDoc(document: Document) {
return this.lang.getTypeScriptDoc(document);
}
}

View file

@ -2,157 +2,128 @@ import type { LanguageServiceManager } from '../LanguageServiceManager';
import type { SignatureHelpProvider } from '../../interfaces';
import ts from 'typescript';
import {
Position,
SignatureHelpContext,
SignatureHelp,
SignatureHelpTriggerKind,
SignatureInformation,
ParameterInformation,
MarkupKind,
CancellationToken
Position,
SignatureHelpContext,
SignatureHelp,
SignatureHelpTriggerKind,
SignatureInformation,
ParameterInformation,
MarkupKind,
CancellationToken,
} from 'vscode-languageserver';
import { Document } from '../../../core/documents';
import { getMarkdownDocumentation } from '../previewer';
import { toVirtualAstroFilePath } from '../utils';
export class SignatureHelpProviderImpl implements SignatureHelpProvider {
constructor(private readonly lang: LanguageServiceManager) {}
constructor(private readonly lang: LanguageServiceManager) {}
private static readonly triggerCharacters = ['(', ',', '<'];
private static readonly retriggerCharacters = [')'];
private static readonly triggerCharacters = ['(', ',', '<'];
private static readonly retriggerCharacters = [')'];
async getSignatureHelp(
document: Document,
position: Position,
context: SignatureHelpContext | undefined,
cancellationToken?: CancellationToken
): Promise<SignatureHelp | null> {
const { lang, tsDoc } = await this.lang.getTypeScriptDoc(document);
const fragment = await tsDoc.getFragment();
async getSignatureHelp(document: Document, position: Position, context: SignatureHelpContext | undefined, cancellationToken?: CancellationToken): Promise<SignatureHelp | null> {
const { lang, tsDoc } = await this.lang.getTypeScriptDoc(document);
const fragment = await tsDoc.getFragment();
if (cancellationToken?.isCancellationRequested) {
return null;
if (cancellationToken?.isCancellationRequested) {
return null;
}
const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
const triggerReason = this.toTsTriggerReason(context);
const info = lang.getSignatureHelpItems(toVirtualAstroFilePath(tsDoc.filePath), offset, triggerReason ? { triggerReason } : undefined);
if (!info || info.items.some((signature) => this.isInSvelte2tsxGeneratedFunction(signature))) {
return null;
}
const signatures = info.items.map(this.toSignatureHelpInformation);
return {
signatures,
activeSignature: info.selectedItemIndex,
activeParameter: info.argumentIndex,
};
}
private isReTrigger(isRetrigger: boolean, triggerCharacter: string): triggerCharacter is ts.SignatureHelpRetriggerCharacter {
return isRetrigger && (this.isTriggerCharacter(triggerCharacter) || SignatureHelpProviderImpl.retriggerCharacters.includes(triggerCharacter));
}
private isTriggerCharacter(triggerCharacter: string): triggerCharacter is ts.SignatureHelpTriggerCharacter {
return SignatureHelpProviderImpl.triggerCharacters.includes(triggerCharacter);
}
/**
* adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L103
*/
private toTsTriggerReason(context: SignatureHelpContext | undefined): ts.SignatureHelpTriggerReason {
switch (context?.triggerKind) {
case SignatureHelpTriggerKind.TriggerCharacter:
if (context.triggerCharacter) {
if (this.isReTrigger(context.isRetrigger, context.triggerCharacter)) {
return { kind: 'retrigger', triggerCharacter: context.triggerCharacter };
}
if (this.isTriggerCharacter(context.triggerCharacter)) {
return {
kind: 'characterTyped',
triggerCharacter: context.triggerCharacter,
};
}
}
return { kind: 'invoked' };
case SignatureHelpTriggerKind.ContentChange:
return context.isRetrigger ? { kind: 'retrigger' } : { kind: 'invoked' };
const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
const triggerReason = this.toTsTriggerReason(context);
const info = lang.getSignatureHelpItems(
toVirtualAstroFilePath(tsDoc.filePath),
offset,
triggerReason ? { triggerReason } : undefined
);
if (
!info ||
info.items.some((signature) => this.isInSvelte2tsxGeneratedFunction(signature))
) {
return null;
}
const signatures = info.items.map(this.toSignatureHelpInformation);
return {
signatures,
activeSignature: info.selectedItemIndex,
activeParameter: info.argumentIndex
};
case SignatureHelpTriggerKind.Invoked:
default:
return { kind: 'invoked' };
}
}
private isReTrigger(
isRetrigger: boolean,
triggerCharacter: string
): triggerCharacter is ts.SignatureHelpRetriggerCharacter {
return (
isRetrigger &&
(this.isTriggerCharacter(triggerCharacter) ||
SignatureHelpProviderImpl.retriggerCharacters.includes(triggerCharacter))
);
}
/**
* adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L73
*/
private toSignatureHelpInformation(item: ts.SignatureHelpItem): SignatureInformation {
const [prefixLabel, separatorLabel, suffixLabel] = [item.prefixDisplayParts, item.separatorDisplayParts, item.suffixDisplayParts].map(ts.displayPartsToString);
private isTriggerCharacter(
triggerCharacter: string
): triggerCharacter is ts.SignatureHelpTriggerCharacter {
return SignatureHelpProviderImpl.triggerCharacters.includes(triggerCharacter);
}
let textIndex = prefixLabel.length;
let signatureLabel = '';
const parameters: ParameterInformation[] = [];
const lastIndex = item.parameters.length - 1;
/**
* adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L103
*/
private toTsTriggerReason(
context: SignatureHelpContext | undefined
): ts.SignatureHelpTriggerReason {
switch (context?.triggerKind) {
case SignatureHelpTriggerKind.TriggerCharacter:
if (context.triggerCharacter) {
if (this.isReTrigger(context.isRetrigger, context.triggerCharacter)) {
return { kind: 'retrigger', triggerCharacter: context.triggerCharacter };
}
if (this.isTriggerCharacter(context.triggerCharacter)) {
return {
kind: 'characterTyped',
triggerCharacter: context.triggerCharacter
};
}
}
return { kind: 'invoked' };
case SignatureHelpTriggerKind.ContentChange:
return context.isRetrigger ? { kind: 'retrigger' } : { kind: 'invoked' };
item.parameters.forEach((parameter, index) => {
const label = ts.displayPartsToString(parameter.displayParts);
case SignatureHelpTriggerKind.Invoked:
default:
return { kind: 'invoked' };
}
}
const startIndex = textIndex;
const endIndex = textIndex + label.length;
const doc = ts.displayPartsToString(parameter.documentation);
/**
* adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L73
*/
private toSignatureHelpInformation(item: ts.SignatureHelpItem): SignatureInformation {
const [prefixLabel, separatorLabel, suffixLabel] = [
item.prefixDisplayParts,
item.separatorDisplayParts,
item.suffixDisplayParts
].map(ts.displayPartsToString);
signatureLabel += label;
parameters.push(ParameterInformation.create([startIndex, endIndex], doc));
let textIndex = prefixLabel.length;
let signatureLabel = '';
const parameters: ParameterInformation[] = [];
const lastIndex = item.parameters.length - 1;
if (index < lastIndex) {
textIndex = endIndex + separatorLabel.length;
signatureLabel += separatorLabel;
}
});
const signatureDocumentation = getMarkdownDocumentation(
item.documentation,
item.tags.filter((tag) => tag.name !== 'param')
);
item.parameters.forEach((parameter, index) => {
const label = ts.displayPartsToString(parameter.displayParts);
return {
label: prefixLabel + signatureLabel + suffixLabel,
documentation: signatureDocumentation
? {
value: signatureDocumentation,
kind: MarkupKind.Markdown,
}
: undefined,
parameters,
};
}
const startIndex = textIndex;
const endIndex = textIndex + label.length;
const doc = ts.displayPartsToString(parameter.documentation);
signatureLabel += label;
parameters.push(ParameterInformation.create([startIndex, endIndex], doc));
if (index < lastIndex) {
textIndex = endIndex + separatorLabel.length;
signatureLabel += separatorLabel;
}
});
const signatureDocumentation = getMarkdownDocumentation(
item.documentation,
item.tags.filter((tag) => tag.name !== 'param')
);
return {
label: prefixLabel + signatureLabel + suffixLabel,
documentation: signatureDocumentation
? {
value: signatureDocumentation,
kind: MarkupKind.Markdown
}
: undefined,
parameters
};
}
private isInSvelte2tsxGeneratedFunction(signatureHelpItem: ts.SignatureHelpItem) {
return signatureHelpItem.prefixDisplayParts.some((part) =>
part.text.includes('__sveltets')
);
}
}
private isInSvelte2tsxGeneratedFunction(signatureHelpItem: ts.SignatureHelpItem) {
return signatureHelpItem.prefixDisplayParts.some((part) => part.text.includes('__sveltets'));
}
}

View file

@ -7,134 +7,119 @@
* adopted from https://github.com/microsoft/vscode/blob/10722887b8629f90cc38ee7d90d54e8246dc895f/extensions/typescript-language-features/src/utils/previewer.ts
*/
import ts from 'typescript';
import { isNotNullOrUndefined } from '../../utils';
function replaceLinks(text: string): string {
return (
text
// Http(s) links
.replace(
/\{@(link|linkplain|linkcode) (https?:\/\/[^ |}]+?)(?:[| ]([^{}\n]+?))?\}/gi,
(_, tag: string, link: string, text?: string) => {
switch (tag) {
case 'linkcode':
return `[\`${text ? text.trim() : link}\`](${link})`;
default:
return `[${text ? text.trim() : link}](${link})`;
}
}
)
);
}
function processInlineTags(text: string): string {
return replaceLinks(text);
}
function getTagBodyText(tag: ts.JSDocTagInfo): string | undefined {
if (!tag.text) {
return undefined;
}
// Convert to markdown code block if it is not already one
function makeCodeblock(text: string): string {
if (text.match(/^\s*[~`]{3}/g)) {
return text;
}
return '```\n' + text + '\n```';
}
function makeExampleTag(text: string) {
// check for caption tags, fix for https://github.com/microsoft/vscode/issues/79704
const captionTagMatches = text.match(/<caption>(.*?)<\/caption>\s*(\r\n|\n)/);
if (captionTagMatches && captionTagMatches.index === 0) {
return (
captionTagMatches[1] +
'\n\n' +
makeCodeblock(text.substr(captionTagMatches[0].length))
);
} else {
return makeCodeblock(text);
}
}
function makeEmailTag(text: string) {
// fix obsucated email address, https://github.com/microsoft/vscode/issues/80898
const emailMatch = text.match(/(.+)\s<([-.\w]+@[-.\w]+)>/);
if (emailMatch === null) {
return text;
} else {
return `${emailMatch[1]} ${emailMatch[2]}`;
}
}
switch (tag.name) {
case 'example':
return makeExampleTag(ts.displayPartsToString(tag.text));
case 'author':
return makeEmailTag(ts.displayPartsToString(tag.text));
case 'default':
return makeCodeblock(ts.displayPartsToString(tag.text));
}
return processInlineTags(ts.displayPartsToString(tag.text));
}
export function getTagDocumentation(tag: ts.JSDocTagInfo): string | undefined {
function getWithType() {
const body = (ts.displayPartsToString(tag.text) || '').split(/^(\S+)\s*-?\s*/);
if (body?.length === 3) {
const param = body[1];
const doc = body[2];
const label = `*@${tag.name}* \`${param}\``;
if (!doc) {
return label;
}
return (
label +
(doc.match(/\r\n|\n/g)
? ' \n' + processInlineTags(doc)
: `${processInlineTags(doc)}`)
);
}
}
switch (tag.name) {
case 'augments':
case 'extends':
case 'param':
case 'template':
return getWithType();
}
// Generic tag
const label = `*@${tag.name}*`;
const text = getTagBodyText(tag);
if (!text) {
return label;
}
return label + (text.match(/\r\n|\n/g) ? ' \n' + text : `${text}`);
}
export function plain(parts: ts.SymbolDisplayPart[] | string): string {
return processInlineTags(typeof parts === 'string' ? parts : ts.displayPartsToString(parts));
}
export function getMarkdownDocumentation(
documentation: ts.SymbolDisplayPart[] | undefined,
tags: ts.JSDocTagInfo[] | undefined
) {
let result: Array<string | undefined> = [];
if (documentation) {
result.push(plain(documentation));
}
if (tags) {
result = result.concat(tags.map(getTagDocumentation));
}
return result.filter(isNotNullOrUndefined).join('\n\n');
}
import ts from 'typescript';
import { isNotNullOrUndefined } from '../../utils';
function replaceLinks(text: string): string {
return (
text
// Http(s) links
.replace(/\{@(link|linkplain|linkcode) (https?:\/\/[^ |}]+?)(?:[| ]([^{}\n]+?))?\}/gi, (_, tag: string, link: string, text?: string) => {
switch (tag) {
case 'linkcode':
return `[\`${text ? text.trim() : link}\`](${link})`;
default:
return `[${text ? text.trim() : link}](${link})`;
}
})
);
}
function processInlineTags(text: string): string {
return replaceLinks(text);
}
function getTagBodyText(tag: ts.JSDocTagInfo): string | undefined {
if (!tag.text) {
return undefined;
}
// Convert to markdown code block if it is not already one
function makeCodeblock(text: string): string {
if (text.match(/^\s*[~`]{3}/g)) {
return text;
}
return '```\n' + text + '\n```';
}
function makeExampleTag(text: string) {
// check for caption tags, fix for https://github.com/microsoft/vscode/issues/79704
const captionTagMatches = text.match(/<caption>(.*?)<\/caption>\s*(\r\n|\n)/);
if (captionTagMatches && captionTagMatches.index === 0) {
return captionTagMatches[1] + '\n\n' + makeCodeblock(text.substr(captionTagMatches[0].length));
} else {
return makeCodeblock(text);
}
}
function makeEmailTag(text: string) {
// fix obsucated email address, https://github.com/microsoft/vscode/issues/80898
const emailMatch = text.match(/(.+)\s<([-.\w]+@[-.\w]+)>/);
if (emailMatch === null) {
return text;
} else {
return `${emailMatch[1]} ${emailMatch[2]}`;
}
}
switch (tag.name) {
case 'example':
return makeExampleTag(ts.displayPartsToString(tag.text));
case 'author':
return makeEmailTag(ts.displayPartsToString(tag.text));
case 'default':
return makeCodeblock(ts.displayPartsToString(tag.text));
}
return processInlineTags(ts.displayPartsToString(tag.text));
}
export function getTagDocumentation(tag: ts.JSDocTagInfo): string | undefined {
function getWithType() {
const body = (ts.displayPartsToString(tag.text) || '').split(/^(\S+)\s*-?\s*/);
if (body?.length === 3) {
const param = body[1];
const doc = body[2];
const label = `*@${tag.name}* \`${param}\``;
if (!doc) {
return label;
}
return label + (doc.match(/\r\n|\n/g) ? ' \n' + processInlineTags(doc) : `${processInlineTags(doc)}`);
}
}
switch (tag.name) {
case 'augments':
case 'extends':
case 'param':
case 'template':
return getWithType();
}
// Generic tag
const label = `*@${tag.name}*`;
const text = getTagBodyText(tag);
if (!text) {
return label;
}
return label + (text.match(/\r\n|\n/g) ? ' \n' + text : `${text}`);
}
export function plain(parts: ts.SymbolDisplayPart[] | string): string {
return processInlineTags(typeof parts === 'string' ? parts : ts.displayPartsToString(parts));
}
export function getMarkdownDocumentation(documentation: ts.SymbolDisplayPart[] | undefined, tags: ts.JSDocTagInfo[] | undefined) {
let result: Array<string | undefined> = [];
if (documentation) {
result.push(plain(documentation));
}
if (tags) {
result = result.concat(tags.map(getTagDocumentation));
}
return result.filter(isNotNullOrUndefined).join('\n\n');
}

View file

@ -178,7 +178,7 @@ export function isVirtualFilePath(filePath: string) {
}
export function toVirtualAstroFilePath(filePath: string) {
if(isVirtualFrameworkFilePath('astro', filePath)) {
if (isVirtualFrameworkFilePath('astro', filePath)) {
return filePath;
}
return `${filePath}.ts`;