/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import * as assert from 'assert'; import { Model } from 'vs/editor/common/model/model'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ViewLineToken } from 'vs/editor/common/core/viewLineToken'; import { ITokenizationSupport, TokenizationRegistry, LanguageId, LanguageIdentifier, MetadataConsts } from 'vs/editor/common/modes'; import { CharacterPair } from 'vs/editor/common/modes/languageConfiguration'; import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import { IFoundBracket } from 'vs/editor/common/editorCommon'; import { TextModel } from 'vs/editor/common/model/textModel'; import { TextModelWithTokens } from 'vs/editor/common/model/textModelWithTokens'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; import { RawTextSource } from 'vs/editor/common/model/textSource'; suite('TextModelWithTokens', () => { function testBrackets(contents: string[], brackets: CharacterPair[]): void { function toRelaxedFoundBracket(a: IFoundBracket) { if (!a) { return null; } return { range: a.range.toString(), open: a.open, close: a.close, isOpen: a.isOpen }; } let charIsBracket: { [char: string]: boolean } = {}; let charIsOpenBracket: { [char: string]: boolean } = {}; let openForChar: { [char: string]: string } = {}; let closeForChar: { [char: string]: string } = {}; brackets.forEach((b) => { charIsBracket[b[0]] = true; charIsBracket[b[1]] = true; charIsOpenBracket[b[0]] = true; charIsOpenBracket[b[1]] = false; openForChar[b[0]] = b[0]; closeForChar[b[0]] = b[1]; openForChar[b[1]] = b[0]; closeForChar[b[1]] = b[1]; }); let expectedBrackets: IFoundBracket[] = []; for (let lineIndex = 0; lineIndex < contents.length; lineIndex++) { let lineText = contents[lineIndex]; for (let charIndex = 0; charIndex < lineText.length; charIndex++) { let ch = lineText.charAt(charIndex); if (charIsBracket[ch]) { expectedBrackets.push({ open: openForChar[ch], close: closeForChar[ch], isOpen: charIsOpenBracket[ch], range: new Range(lineIndex + 1, charIndex + 1, lineIndex + 1, charIndex + 2) }); } } } const languageIdentifier = new LanguageIdentifier('testMode', LanguageId.PlainText); let registration = LanguageConfigurationRegistry.register(languageIdentifier, { brackets: brackets }); let model = new TextModelWithTokens( RawTextSource.fromString(contents.join('\n')), TextModel.DEFAULT_CREATION_OPTIONS, languageIdentifier ); // findPrevBracket { let expectedBracketIndex = expectedBrackets.length - 1; let currentExpectedBracket = expectedBracketIndex >= 0 ? expectedBrackets[expectedBracketIndex] : null; for (let lineNumber = contents.length; lineNumber >= 1; lineNumber--) { let lineText = contents[lineNumber - 1]; for (let column = lineText.length + 1; column >= 1; column--) { if (currentExpectedBracket) { if (lineNumber === currentExpectedBracket.range.startLineNumber && column < currentExpectedBracket.range.endColumn) { expectedBracketIndex--; currentExpectedBracket = expectedBracketIndex >= 0 ? expectedBrackets[expectedBracketIndex] : null; } } let actual = model.findPrevBracket({ lineNumber: lineNumber, column: column }); assert.deepEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findPrevBracket of ' + lineNumber + ', ' + column); } } } // findNextBracket { let expectedBracketIndex = 0; let currentExpectedBracket = expectedBracketIndex < expectedBrackets.length ? expectedBrackets[expectedBracketIndex] : null; for (let lineNumber = 1; lineNumber <= contents.length; lineNumber++) { let lineText = contents[lineNumber - 1]; for (let column = 1; column <= lineText.length + 1; column++) { if (currentExpectedBracket) { if (lineNumber === currentExpectedBracket.range.startLineNumber && column > currentExpectedBracket.range.startColumn) { expectedBracketIndex++; currentExpectedBracket = expectedBracketIndex < expectedBrackets.length ? expectedBrackets[expectedBracketIndex] : null; } } let actual = model.findNextBracket({ lineNumber: lineNumber, column: column }); assert.deepEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findNextBracket of ' + lineNumber + ', ' + column); } } } model.dispose(); registration.dispose(); } test('brackets', () => { testBrackets([ 'if (a == 3) { return (7 * (a + 5)); }' ], [ ['{', '}'], ['[', ']'], ['(', ')'] ]); }); }); suite('TextModelWithTokens - bracket matching', () => { function isNotABracket(model: Model, lineNumber: number, column: number) { let match = model.matchBracket(new Position(lineNumber, column)); assert.equal(match, null, 'is not matching brackets at ' + lineNumber + ', ' + column); } function isBracket2(model: Model, testPosition: Position, expected: [Range, Range]): void { let actual = model.matchBracket(testPosition); assert.deepEqual(actual, expected, 'matches brackets at ' + testPosition); } const languageIdentifier = new LanguageIdentifier('bracketMode1', LanguageId.PlainText); let registration: IDisposable = null; setup(() => { registration = LanguageConfigurationRegistry.register(languageIdentifier, { brackets: [ ['{', '}'], ['[', ']'], ['(', ')'], ] }); }); teardown(() => { registration.dispose(); registration = null; }); test('bracket matching 1', () => { let text = ')]}{[(' + '\n' + ')]}{[('; let model = Model.createFromString(text, undefined, languageIdentifier); isNotABracket(model, 1, 1); isNotABracket(model, 1, 2); isNotABracket(model, 1, 3); isBracket2(model, new Position(1, 4), [new Range(1, 4, 1, 5), new Range(2, 3, 2, 4)]); isBracket2(model, new Position(1, 5), [new Range(1, 5, 1, 6), new Range(2, 2, 2, 3)]); isBracket2(model, new Position(1, 6), [new Range(1, 6, 1, 7), new Range(2, 1, 2, 2)]); isBracket2(model, new Position(1, 7), [new Range(1, 6, 1, 7), new Range(2, 1, 2, 2)]); isBracket2(model, new Position(2, 1), [new Range(2, 1, 2, 2), new Range(1, 6, 1, 7)]); isBracket2(model, new Position(2, 2), [new Range(2, 2, 2, 3), new Range(1, 5, 1, 6)]); isBracket2(model, new Position(2, 3), [new Range(2, 3, 2, 4), new Range(1, 4, 1, 5)]); isBracket2(model, new Position(2, 4), [new Range(2, 3, 2, 4), new Range(1, 4, 1, 5)]); isNotABracket(model, 2, 5); isNotABracket(model, 2, 6); isNotABracket(model, 2, 7); model.dispose(); }); test('bracket matching 2', () => { let text = 'var bar = {' + '\n' + 'foo: {' + '\n' + '}, bar: {hallo: [{' + '\n' + '}, {' + '\n' + '}]}}'; let model = Model.createFromString(text, undefined, languageIdentifier); let brackets: [Position, Range, Range][] = [ [new Position(1, 11), new Range(1, 11, 1, 12), new Range(5, 4, 5, 5)], [new Position(1, 12), new Range(1, 11, 1, 12), new Range(5, 4, 5, 5)], [new Position(2, 6), new Range(2, 6, 2, 7), new Range(3, 1, 3, 2)], [new Position(2, 7), new Range(2, 6, 2, 7), new Range(3, 1, 3, 2)], [new Position(3, 1), new Range(3, 1, 3, 2), new Range(2, 6, 2, 7)], [new Position(3, 2), new Range(3, 1, 3, 2), new Range(2, 6, 2, 7)], [new Position(3, 9), new Range(3, 9, 3, 10), new Range(5, 3, 5, 4)], [new Position(3, 10), new Range(3, 9, 3, 10), new Range(5, 3, 5, 4)], [new Position(3, 17), new Range(3, 17, 3, 18), new Range(5, 2, 5, 3)], [new Position(3, 18), new Range(3, 18, 3, 19), new Range(4, 1, 4, 2)], [new Position(3, 19), new Range(3, 18, 3, 19), new Range(4, 1, 4, 2)], [new Position(4, 1), new Range(4, 1, 4, 2), new Range(3, 18, 3, 19)], [new Position(4, 2), new Range(4, 1, 4, 2), new Range(3, 18, 3, 19)], [new Position(4, 4), new Range(4, 4, 4, 5), new Range(5, 1, 5, 2)], [new Position(4, 5), new Range(4, 4, 4, 5), new Range(5, 1, 5, 2)], [new Position(5, 1), new Range(5, 1, 5, 2), new Range(4, 4, 4, 5)], [new Position(5, 2), new Range(5, 2, 5, 3), new Range(3, 17, 3, 18)], [new Position(5, 3), new Range(5, 3, 5, 4), new Range(3, 9, 3, 10)], [new Position(5, 4), new Range(5, 4, 5, 5), new Range(1, 11, 1, 12)], [new Position(5, 5), new Range(5, 4, 5, 5), new Range(1, 11, 1, 12)], ]; let isABracket: { [lineNumber: number]: { [col: number]: boolean; }; } = { 1: {}, 2: {}, 3: {}, 4: {}, 5: {} }; for (let i = 0, len = brackets.length; i < len; i++) { let [testPos, b1, b2] = brackets[i]; isBracket2(model, testPos, [b1, b2]); isABracket[testPos.lineNumber][testPos.column] = true; } for (let i = 1, len = model.getLineCount(); i <= len; i++) { let line = model.getLineContent(i); for (let j = 1, lenJ = line.length + 1; j <= lenJ; j++) { if (!isABracket[i].hasOwnProperty(j)) { isNotABracket(model, i, j); } } } model.dispose(); }); }); suite('TextModelWithTokens regression tests', () => { test('Microsoft/monaco-editor#122: Unhandled Exception: TypeError: Unable to get property \'replace\' of undefined or null reference', () => { function assertViewLineTokens(model: Model, lineNumber: number, forceTokenization: boolean, expected: ViewLineToken[]): void { if (forceTokenization) { model.forceTokenization(lineNumber); } let actual = model.getLineTokens(lineNumber).inflate(); let decode = (token: ViewLineToken) => { return { endIndex: token.endIndex, foreground: token.getForeground() }; }; assert.deepEqual(actual.map(decode), expected.map(decode)); } let _tokenId = 10; const LANG_ID1 = 'indicisiveMode1'; const LANG_ID2 = 'indicisiveMode2'; const languageIdentifier1 = new LanguageIdentifier(LANG_ID1, 3); const languageIdentifier2 = new LanguageIdentifier(LANG_ID2, 4); const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined, tokenize2: (line, state) => { let myId = ++_tokenId; let tokens = new Uint32Array(2); tokens[0] = 0; tokens[1] = ( myId << MetadataConsts.FOREGROUND_OFFSET ) >>> 0; return new TokenizationResult2(tokens, state); } }; let registration1 = TokenizationRegistry.register(LANG_ID1, tokenizationSupport); let registration2 = TokenizationRegistry.register(LANG_ID2, tokenizationSupport); let model = Model.createFromString('A model with\ntwo lines'); assertViewLineTokens(model, 1, true, [createViewLineToken(12, 1)]); assertViewLineTokens(model, 2, true, [createViewLineToken(9, 1)]); model.setMode(languageIdentifier1); assertViewLineTokens(model, 1, true, [createViewLineToken(12, 11)]); assertViewLineTokens(model, 2, true, [createViewLineToken(9, 12)]); model.setMode(languageIdentifier2); assertViewLineTokens(model, 1, false, [createViewLineToken(12, 1)]); assertViewLineTokens(model, 2, false, [createViewLineToken(9, 1)]); model.dispose(); registration1.dispose(); registration2.dispose(); function createViewLineToken(endIndex: number, foreground: number): ViewLineToken { let metadata = ( (foreground << MetadataConsts.FOREGROUND_OFFSET) ) >>> 0; return new ViewLineToken(endIndex, metadata); } }); test('Microsoft/monaco-editor#133: Error: Cannot read property \'modeId\' of undefined', () => { const languageIdentifier = new LanguageIdentifier('testMode', LanguageId.PlainText); let registration = LanguageConfigurationRegistry.register(languageIdentifier, { brackets: [ ['module', 'end module'], ['sub', 'end sub'] ] }); let model = Model.createFromString([ 'Imports System', 'Imports System.Collections.Generic', '', 'Module m1', '', '\tSub Main()', '\tEnd Sub', '', 'End Module', ].join('\n'), undefined, languageIdentifier); let actual = model.matchBracket(new Position(4, 1)); assert.deepEqual(actual, [new Range(4, 1, 4, 7), new Range(9, 1, 9, 11)]); model.dispose(); registration.dispose(); }); }); suite('TextModel.getLineIndentGuide', () => { function assertIndentGuides(lines: [number, string][]): void { let text = lines.map(l => l[1]).join('\n'); let model = Model.createFromString(text); let actualIndents = model.getLinesIndentGuides(1, model.getLineCount()); let actual: [number, string][] = []; for (let line = 1; line <= model.getLineCount(); line++) { actual[line - 1] = [actualIndents[line - 1], model.getLineContent(line)]; } // let expected = lines.map(l => l[0]); assert.deepEqual(actual, lines); model.dispose(); } test('getLineIndentGuide one level', () => { assertIndentGuides([ [0, 'A'], [1, ' A'], [1, ' A'], [1, ' A'], ]); }); test('getLineIndentGuide two levels', () => { assertIndentGuides([ [0, 'A'], [1, ' A'], [1, ' A'], [1, ' A'], [1, ' A'], ]); }); test('getLineIndentGuide three levels', () => { assertIndentGuides([ [0, 'A'], [1, ' A'], [1, ' A'], [2, ' A'], [0, 'A'], ]); }); test('getLineIndentGuide decreasing indent', () => { assertIndentGuides([ [1, ' A'], [1, ' A'], [0, 'A'], ]); }); test('getLineIndentGuide Java', () => { assertIndentGuides([ /* 1*/[0, 'class A {'], /* 2*/[1, ' void foo() {'], /* 3*/[1, ' console.log(1);'], /* 4*/[1, ' console.log(2);'], /* 5*/[1, ' }'], /* 6*/[1, ''], /* 7*/[1, ' void bar() {'], /* 8*/[1, ' console.log(3);'], /* 9*/[1, ' }'], /*10*/[0, '}'], /*11*/[0, 'interface B {'], /*12*/[1, ' void bar();'], /*13*/[0, '}'], ]); }); test('getLineIndentGuide Javadoc', () => { assertIndentGuides([ [0, '/**'], [1, ' * Comment'], [1, ' */'], [0, 'class A {'], [1, ' void foo() {'], [1, ' }'], [0, '}'], ]); }); test('getLineIndentGuide Whitespace', () => { assertIndentGuides([ [0, 'class A {'], [1, ''], [1, ' void foo() {'], [1, ' '], [2, ' return 1;'], [1, ' }'], [1, ' '], [0, '}'], ]); }); test('getLineIndentGuide Tabs', () => { assertIndentGuides([ [0, 'class A {'], [1, '\t\t'], [1, '\tvoid foo() {'], [2, '\t \t//hello'], [2, '\t return 2;'], [1, ' \t}'], [1, ' '], [0, '}'], ]); }); test('getLineIndentGuide checker.ts', () => { assertIndentGuides([ /* 1*/[0, '/// '], /* 2*/[0, ''], /* 3*/[0, '/* @internal */'], /* 4*/[0, 'namespace ts {'], /* 5*/[1, ' let nextSymbolId = 1;'], /* 6*/[1, ' let nextNodeId = 1;'], /* 7*/[1, ' let nextMergeId = 1;'], /* 8*/[1, ' let nextFlowId = 1;'], /* 9*/[1, ''], /*10*/[1, ' export function getNodeId(node: Node): number {'], /*11*/[2, ' if (!node.id) {'], /*12*/[3, ' node.id = nextNodeId;'], /*13*/[3, ' nextNodeId++;'], /*14*/[2, ' }'], /*15*/[2, ' return node.id;'], /*16*/[1, ' }'], /*17*/[0, '}'], ]); }); test('issue #8425 - Missing indentation lines for first level indentation', () => { assertIndentGuides([ [1, '\tindent1'], [2, '\t\tindent2'], [2, '\t\tindent2'], [1, '\tindent1'], ]); }); test('issue #8952 - Indentation guide lines going through text on .yml file', () => { assertIndentGuides([ [0, 'properties:'], [1, ' emailAddress:'], [2, ' - bla'], [2, ' - length:'], [3, ' max: 255'], [0, 'getters:'], ]); }); test('issue #11892 - Indent guides look funny', () => { assertIndentGuides([ [0, 'function test(base) {'], [1, '\tswitch (base) {'], [2, '\t\tcase 1:'], [3, '\t\t\treturn 1;'], [2, '\t\tcase 2:'], [3, '\t\t\treturn 2;'], [1, '\t}'], [0, '}'], ]); }); test('issue #12398 - Problem in indent guidelines', () => { assertIndentGuides([ [2, '\t\t.bla'], [3, '\t\t\tlabel(for)'], [0, 'include script'], ]); }); });