UNPKG

alm

Version:

The best IDE for TypeScript

207 lines (181 loc) 8.83 kB
/** * The syntax classifier works on the bases of a *file* with sections of the file being queried for classification * * This file exists to cache the contents a file * and apply its edits (similar to our server fileModel) so that we can do classifications on particular lines * * Since the ts classifier works on a language service we have a *single* language service to hold all files */ import {TypedEvent} from "../../../common/events"; import * as lsh from "../../../languageServiceHost/languageServiceHost"; const languageServiceHost = new lsh.LanguageServiceHost(undefined); const languageService = ts.createLanguageService(languageServiceHost); /** * ts syntax highlighter doesn't handle bom correctly so fix that */ const BOM_CHAR_CODE = 65279; const hasBom: { [filePath: string]: boolean } = Object.create(null); export function addFile(filePath: string, contents: string) { hasBom[filePath] = contents.charCodeAt(0) === BOM_CHAR_CODE; languageServiceHost.addScript(filePath, contents); } export function removeFile(filePath: string){ languageServiceHost.removeFile(filePath); } export function editFile(filePath: string, codeEdit: CodeEdit) { languageServiceHost.applyCodeEdit(filePath, codeEdit.from, codeEdit.to, codeEdit.newText); } export function setContents(filePath: string, contents: string) { hasBom[filePath] = contents.charCodeAt(0) === BOM_CHAR_CODE; languageServiceHost.setContents(filePath,contents); } export function getLineAndCharacterOfPosition(filePath: string, pos: number): EditorPosition { return languageServiceHost.getLineAndCharacterOfPosition(filePath, pos); } export function getPositionOfLineAndCharacter(filePath: string, line: number, ch: number): number { return languageServiceHost.getPositionOfLineAndCharacter(filePath, line, ch); } export function getSourceFile(filePath: string) { return languageService.getNonBoundSourceFile(filePath); } export function getClassificationsForLine(filePath: string, lineStart: number, string: string): ClassifiedSpan[] { const offsetForBom = hasBom[filePath] ? -1 : 0; // don't need this for monaco! /** * Protect against code mirror optimized rendering. * If string does not match expected line contents tokenize as whitespace till the precise call is made. */ // const trueLineContents = languageService.getNonBoundSourceFile(filePath).text.substr(lineStart); // if (!trueLineContents.startsWith(string)){ // // console.log({ trueLineContents, string, filePath }); // DEBUG // const cantDoIt = [{ // textSpan: { // start:0, // length: string.length // }, // startInLine: 0, // string, // classificationType: ts.ClassificationType.whiteSpace, // classificationTypeName: ClassificationTypeNames.whiteSpace, // }]; // return cantDoIt; // } let lineLength = string.length; let encodedClassifications = languageService.getEncodedSyntacticClassifications(filePath, { start: lineStart, length: lineLength }); let classifications: ClassifiedSpan[] = unencodeClassifications(encodedClassifications); /** for some reason we have dupes on first token sometimes. this helps remove them */ let lastStartSet = false; let lastStart = 0; // Trim to the query region classifications = classifications .map((c, i) => { // Compensate each token for bom c.textSpan.start += offsetForBom; // completely outside the range on the left if ((c.textSpan.start + c.textSpan.length) <= lineStart) { return null; } // completely outside the range on the right if (c.textSpan.start > (lineStart + lineLength)) { return null; } // trim the left if (c.textSpan.start < lineStart) { c.textSpan.length = c.textSpan.start + c.textSpan.length - lineStart; c.textSpan.start = lineStart; } // trim the right if ((c.textSpan.start + c.textSpan.length) > (lineStart + lineLength)) { c.textSpan.length = (lineStart + lineLength) - (c.textSpan.start); } // dedupe...first token only if (!lastStartSet) { lastStartSet = true; lastStart = c.textSpan.start; } else { if (c.textSpan.start == lastStart) { return null; } } return c; }) .filter(c=> !!c); // Add a string for easier debugging + startInLine for creating a 'map' of positions later on classifications.forEach((c: ClassifiedSpan) => { c.startInLine = c.textSpan.start - lineStart; c.string = string.substr(c.startInLine, c.textSpan.length) }); return classifications; } export function getIndentationAtPosition(filePath: string, lineStart: number, options: ts.EditorOptions) { return languageService.getIndentationAtPosition(filePath, lineStart, options); } export function getFormattingEditsAfterKeystroke(filePath: string, position: number, key: string, options: ts.FormatCodeOptions) { return languageService.getFormattingEditsAfterKeystroke(filePath, position, key, options); } /** * Just a convinient wrapper around ts.ClassifiedSpan * that keeps the enum `classificationType` intact **/ export interface ClassifiedSpan { textSpan: ts.TextSpan; classificationType: ts.ClassificationType; classificationTypeName: string; // Stuff we load for debugging string?: string; startInLine?: number; } /** * ported from services.ts convertClassifications * encoding is [[start,lenght,type]......] * also added whitespace support */ function unencodeClassifications(classifications: ts.Classifications): ClassifiedSpan[] { let dense = classifications.spans; let result: ClassifiedSpan[] = []; let expectedStart = 0; // used for whitespace for (let i = 0, n = dense.length; i < n; i += 3) { if (dense[i] > expectedStart){ result.push({ textSpan: ts.createTextSpan(expectedStart,dense[i]-expectedStart), classificationType: ts.ClassificationType.whiteSpace, classificationTypeName: ts.ClassificationTypeNames.whiteSpace, }); } result.push({ textSpan: ts.createTextSpan(dense[i], dense[i + 1]), classificationType: dense[i + 2], classificationTypeName: getClassificationTypeName(dense[i + 2]), }); expectedStart = dense[i] + dense[i + 1]; } return result; } /** brought in as it is */ import ClassificationType = ts.ClassificationType; let ClassificationTypeNames = ts.ClassificationTypeNames; function getClassificationTypeName(type: ClassificationType) { switch (type) { case ClassificationType.comment: return ClassificationTypeNames.comment; case ClassificationType.identifier: return ClassificationTypeNames.identifier; case ClassificationType.keyword: return ClassificationTypeNames.keyword; case ClassificationType.numericLiteral: return ClassificationTypeNames.numericLiteral; case ClassificationType.operator: return ClassificationTypeNames.operator; case ClassificationType.stringLiteral: return ClassificationTypeNames.stringLiteral; case ClassificationType.whiteSpace: return ClassificationTypeNames.whiteSpace; case ClassificationType.text: return ClassificationTypeNames.text; case ClassificationType.punctuation: return ClassificationTypeNames.punctuation; case ClassificationType.className: return ClassificationTypeNames.className; case ClassificationType.enumName: return ClassificationTypeNames.enumName; case ClassificationType.interfaceName: return ClassificationTypeNames.interfaceName; case ClassificationType.moduleName: return ClassificationTypeNames.moduleName; case ClassificationType.typeParameterName: return ClassificationTypeNames.typeParameterName; case ClassificationType.typeAliasName: return ClassificationTypeNames.typeAliasName; case ClassificationType.parameterName: return ClassificationTypeNames.parameterName; case ClassificationType.docCommentTagName: return ClassificationTypeNames.docCommentTagName; case ClassificationType.jsxOpenTagName: return ClassificationTypeNames.jsxOpenTagName; case ClassificationType.jsxCloseTagName: return ClassificationTypeNames.jsxCloseTagName; case ClassificationType.jsxSelfClosingTagName: return ClassificationTypeNames.jsxSelfClosingTagName; } }