/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import dedent from 'ts-dedent'; import { createTextDocument } from '../../test/textDocument'; import { TextDocumentManager } from '../../textDocumentManager'; import { BlockPositionType, BlockTrimmer, getBlockPositionType, TerseBlockTrimmer, VerboseBlockTrimmer, } from '../blockTrimmer'; const x = TextDocumentManager; console.log(x); suite('VerboseBlockTrimmer', function () { test('.getCompletionTrimOffset() returns undefined when it is under the line limit', async function () { await testCompletionTrimming(dedent` function twoWayMerge(sortedList1: T[], sortedList2: T[]): T[] { let mergedList: T[] = []; let i = 0; let j = 0; ❚while (i < sortedList1.length && j < sortedList2.length) { if (compareNumbers(sortedList1[i], sortedList2[j]) <= 0) { mergedList.push(sortedList1[i]); i++; } else { mergedList.push(sortedList2[j]); j++; } } `); }); test('.getCompletionTrimOffset() does not trim trailing newlines', async function () { await testCompletionTrimming( dedent` function twoWayMerge(sortedList1: T[], sortedList2: T[]): T[] { let mergedList: T[] = []; let i = 0; let j = 0; ❚while (i < sortedList1.length && j < sortedList2.length) { if (compareNumbers(sortedList1[i], sortedList2[j]) <= 0) { mergedList.push(sortedList1[i]); i++; } else { mergedList.push(sortedList2[j]); j++; } } ` + '\n' ); }); test('.getCompletionTrimOffset() trims to the containing block even if under the limit', async function () { await testCompletionTrimming(dedent` function twoWayMerge(sortedList1: T[], sortedList2: T[]): T[] { ❚let mergedList: T[] = []; let i = 0; let j = 0; }✂️ const merged = twoWayMerge([1, 2, 3], [4, 5, 6]); `); }); test('.getCompletionTrimOffset() trims at a blank line when one is found', async function () { await testCompletionTrimming(dedent` function twoWayMerge(sortedList1: T[], sortedList2: T[]): T[] { ❚let mergedList: T[] = []; let i = 0; let j = 0;✂️ while (i < sortedList1.length && j < sortedList2.length) { if (compareNumbers(sortedList1[i], sortedList2[j]) <= 0) { mergedList.push(sortedList1[i]); i++; } else { mergedList.push(sortedList2[j]); j++; } } while (i < sortedList1.length) { mergedList.push(sortedList1[i]); i++; } `); }); test('.getCompletionTrimOffset() trims at a statement when no blank lines are present', async function () { await testCompletionTrimming(dedent` function twoWayMerge(sortedList1: T[], sortedList2: T[]): T[] { ❚let mergedList: T[] = []; let i = 0; let j = 0;✂️ while (i < sortedList1.length && j < sortedList2.length) { if (compareNumbers(sortedList1[i], sortedList2[j]) <= 0) { mergedList.push(sortedList1[i]); i++; } else { mergedList.push(sortedList2[j]); j++; } } `); }); test('.getCompletionTrimOffset() trims at a child statement when the first statement is over the limit', async function () { await testCompletionTrimming(dedent` function twoWayMerge(sortedList1: T[], sortedList2: T[]): T[] { let mergedList: T[] = []; let i = 0; let j = 0; ❚while (i < sortedList1.length && j < sortedList2.length) { // compareNumbers has the following return values: // -1 if sortedList1[i] < sortedList2[j] // 0 if sortedList1[i] === sortedList2[j] // 1 if sortedList1[i] > sortedList2[j] if (compareNumbers(sortedList1[i], sortedList2[j]) <= 0) { // sortedList1[i] is less than or equal to sortedList2[j] mergedList.push(sortedList1[i]); i++; }✂️ else { // sortedList1[i] is greater than sortedList2[j] mergedList.push(sortedList2[j]); j++; } } `); }); test('.getCompletionTrimOffset() trims at a top-level statement when no containing block is present', async function () { await testCompletionTrimming(dedent` const a = 1; ❚const b = 2; const c = 3; const d = 4; const e = 5; const f = 6; const g = 7; const h = 8; const i = 9; const j = 10; const k = 11;✂️ const l = 12; `); }); test('.getCompletionTrimOffset() trims before a statement that begins past the line limit', async function () { await testCompletionTrimming(dedent` const a = 1; ❚const b = 2; const c = 3; const d = 4; const e = 5; const f = 6; const g = 7;✂️ // comment 1 // comment 2 // comment 3 // comment 4 // comment 5 const h = 8; `); }); test('.getCompletionTrimOffset() trims trailing, non-statement text that goes over the line limit', async function () { await testCompletionTrimming(dedent` const a = 1; ❚const b = 2; const c = 3; const d = 4;✂️ // comment 1 // comment 2 // comment 3 // comment 4 // comment 5 // comment 6 // comment 7 // comment 8 `); }); test('.getCompletionTrimOffset() trims to the first statement when it is unsplittable even if over the limit', async function () { await testCompletionTrimming(dedent` function foo(arg: boolean) { ❚if (arg) { // comment 1 // comment 2 // comment 3 // comment 4 // comment 5 // comment 6 // comment 7 // comment 8 // comment 9 // comment 10 }✂️ return; `); }); test('.getCompletionTrimOffset() trims to the first statement when it begins past the limit', async function () { await testCompletionTrimming(dedent` function foo(arg: boolean) { ❚// comment 1 // comment 2 // comment 3 // comment 4 // comment 5 // comment 6 // comment 7 // comment 8 // comment 9 // comment 10 // comment 11 const str = arg ? 'true' : 'false';✂️ console.log(str); `); }); test('.getCompletionTrimOffset() trims to the first statement when it begins past the limit even if unsplittable', async function () { await testCompletionTrimming(dedent` function foo(arg: boolean) { ❚// comment 1 // comment 2 // comment 3 // comment 4 // comment 5 // comment 6 // comment 7 // comment 8 // comment 9 // comment 10 // comment 11 if (arg) { // comment 12 }✂️ return; `); }); test('.getCompletionTrimOffset() trims to the first statement before non-statement content', async function () { await testCompletionTrimming(dedent` function ex❚ample(flag) { flag = !flag;✂️ if (flag) { flag = !flag; if (!flag) { flag = !flag; if (flag) { flag = !flag; if (!flag) { flag = !flag; if (flag) { flag = !flag; `); }); async function testCompletionTrimming(textWithCompletion: string): Promise { await testCompletionTrimmingWithTrimmer(textWithCompletion, VerboseBlockTrimmer); } }); suite('TerseBlockTrimmer', function () { test('.getCompletionTrimOffset() returns undefined for a single statement under the line limit', async function () { await testCompletionTrimming(dedent` function example() { ❚let result = []; `); }); test('.getCompletionTrimOffset() trims to the containing block', async function () { await testCompletionTrimming(dedent` function example() { ❚return; }✂️ function example2() { return; } `); }); test('.getCompletionTrimOffset() trims at a blank line', async function () { await testCompletionTrimming(dedent` function example() { ❚let result = [];✂️ let i = 0; `); }); test('.getCompletionTrimOffset() trims at non-statement content between statements', async function () { await testCompletionTrimming(dedent` function example() { ❚let result = [];✂️ // comment let i = 0; `); }); test('.getCompletionTrimOffset() trims at the start of a new compound statement', async function () { await testCompletionTrimming(dedent` function example() { ❚let result = []; let i = 0;✂️ for (i = 0; i < 10; i++) { `); }); test('.getCompletionTrimOffset() trims after a single compound statement', async function () { await testCompletionTrimming(dedent` function reverseFind(haystack, needle) { ❚for (let i = haystack.length - 1; i >= 0; i--) { if (haystack[i] === needle) return i; }✂️ return -1; `); }); test('.getCompletionTrimOffset() trims to the line limit once the look-ahead size is exceeded', async function () { await testCompletionTrimming(dedent` function example() { ❚// line 1 // line 2 // line 3✂️ // line 4 // line 5 // line 6 // line 7 // line 8 // line 9 // line 10 // line 11 `); }); test('.getCompletionTrimOffset() allows a single section to fill up to the look-ahead size if it is complete', async function () { await testCompletionTrimming(dedent` function example() { ❚const a = 1; const b = 2; const c = 3; const d = 4; const e = 5; const f = 6;✂️ while (true) { `); }); test('.getCompletionTrimOffset() supports Python', async function () { await testCompletionTrimming( dedent` def reverse_find(haystack, needle): ❚result = [] i = 0✂️ while i < len(haystack): `, 'python' ); }); test('.getCompletionTrimOffset() trims to a containing block in Python', async function () { await testCompletionTrimming( dedent` def example(a, b): if a > b: ❚c = a - b return c✂️ else: `, 'python' ); }); test('.getCompletionTrimOffset() supports Go', async function () { await testCompletionTrimming( dedent` package main func reverseFind(haystack []int, needle int) int { ❚result := []int{} i := 0✂️ for i < len(haystack) { if haystack[i] == needle { return i `, 'go' ); }); test('.getCompletionTrimOffset() supports PHP', async function () { await testCompletionTrimming( dedent` $item) { if ($item === $needle) { return $index; } } `, 'php' ); }); test('.getCompletionTrimOffset() supports Ruby', async function () { await testCompletionTrimming( dedent` def reverse_find(haystack, needle) ❚len = haystack.length i = len - 1✂️ while i >= 0 do return i if haystack[i] == needle i -= 1 end `, 'ruby' ); }); test('.getCompletionTrimOffset() supports Java', async function () { await testCompletionTrimming( dedent` public class Main { public static int reverseFind(int[] haystack, int needle) { ❚int end = haystack.length - 1;✂️ for (int i = end; i >= 0; i--) { if (haystack[i] == needle) { return i; } } `, 'java' ); }); test('.getCompletionTrimOffset() supports C#', async function () { await testCompletionTrimming( dedent` class Program { static int ReverseFind(int[] haystack, int needle) { ❚int end = haystack.Length - 1;✂️ for (int i = end; i >= 0; i--) { if (haystack[i] == needle) { return i; } } `, 'csharp' ); }); test('.getCompletionTrimOffset() supports C', async function () { await testCompletionTrimming( dedent` #include int reverse_find(int haystack[], int needle, int size) { ❚int i = size - 1;✂️ while (i >= 0) { if (haystack[i] == needle) { return i; } i--; } `, 'c' ); }); test('.getCompletionTrimOffset() supports C++', async function () { await testCompletionTrimming( dedent` #include using namespace std; template class ReverseFind { public: static int find(T haystack[], T needle, int size) { ❚int i = size - 1;✂️ while (i >= 0) { if (haystack[i] == needle) { return i; } i--; } } }; `, 'cpp' ); }); async function testCompletionTrimming(textWithCompletion: string, languageId = 'typescript'): Promise { await testCompletionTrimmingWithTrimmer(textWithCompletion, TerseBlockTrimmer, languageId); } }); interface BlockTrimmerConstructor { new(languageId: string, prefix: string, completion: string): BlockTrimmer; } async function testCompletionTrimmingWithTrimmer( textWithCompletion: string, blockTrimmerType: BlockTrimmerConstructor, languageId = 'typescript' ): Promise { const cursorMarker = '❚'; const trimMarker = '✂️'; const cursorPos = textWithCompletion.indexOf(cursorMarker); const trimPos = textWithCompletion.indexOf(trimMarker); const prefix = textWithCompletion.substring(0, cursorPos); const trimmed = textWithCompletion.substring(cursorPos + cursorMarker.length, trimPos === -1 ? undefined : trimPos); const completion = trimmed + (trimPos === -1 ? '' : textWithCompletion.substring(trimPos + trimMarker.length)); const expectedOffset = trimPos === -1 ? undefined : trimmed.length; const trimmer = new blockTrimmerType(languageId, prefix, completion); const actualOffset = await trimmer.getCompletionTrimOffset(); assert.strictEqual( actualOffset, expectedOffset, dedent` Expected an offset of ${expectedOffset} but got ${actualOffset} ${trimmed === completion.substring(0, actualOffset) ? 'true' : 'false'} expected completion: ${JSON.stringify(completion.substring(0, expectedOffset))} actual completion: ${JSON.stringify(completion.substring(0, actualOffset))} ` ); } suite('getBlockPositionType()', function () { test('empty document returns NonBlock', async function () { await testPositionType(BlockPositionType.NonBlock, '❚'); }); test('on a simple expression returns NonBlock', async function () { await testPositionType(BlockPositionType.NonBlock, '❚const x = 1;'); }); test('with an empty block returns EmptyBlock', async function () { await testPositionType(BlockPositionType.EmptyBlock, 'while (true) { ❚ }'); await testPositionType(BlockPositionType.EmptyBlock, 'function example() { ❚ }'); }); test('at the end of a non-empty block returns BlockEnd', async function () { await testPositionType(BlockPositionType.BlockEnd, 'while (true) { x += 1; ❚ }'); }); test('mid-statement at the end of a non-empty block returns BlockEnd', async function () { await testPositionType(BlockPositionType.BlockEnd, 'while (true) { x += 1❚; }'); }); test('between statements within a block returns MidBlock', async function () { await testPositionType(BlockPositionType.MidBlock, 'while (true) { last = x; ❚ x += 1; }'); }); test('on a statement before the last within a block returns MidBlock', async function () { await testPositionType(BlockPositionType.MidBlock, 'while (true) { last = x❚; x += 1; }'); }); test('on a multi-line simple statement within a block before the last line returns MidBlock', async function () { await testPositionType( BlockPositionType.MidBlock, dedent` if (true) { someFunction( arg1, arg2, ❚ arg3 ); } ` ); }); test('on a multi-line simple statement within a block on the last line returns BlockEnd', async function () { await testPositionType( BlockPositionType.BlockEnd, dedent` if (true) { someFunction( arg1, arg2, arg3 ❚); } ` ); }); // confirm single-line if statement behavior in JS given the special treatment by StatementTree: test('inside an empty block of a single-line if statement in JS returns EmptyBlock', async function () { await testPositionType(BlockPositionType.EmptyBlock, 'if (true) { ❚ }'); }); test('supports Python', async function () { await testPositionType( BlockPositionType.MidBlock, dedent` def example(): ❚ pass `, 'python' ); }); test('supports Go', async function () { await testPositionType( BlockPositionType.EmptyBlock, dedent` package main func main() { ❚ } `, 'go' ); }); test('supports PHP', async function () { await testPositionType( BlockPositionType.EmptyBlock, dedent` int main() { ❚ } `, 'cpp' ); }); test('supports C++', async function () { await testPositionType( BlockPositionType.EmptyBlock, dedent` #include class Main { ❚ } `, 'cpp' ); }); async function testPositionType( expectedType: BlockPositionType, textWithCursor: string, languageId = 'typescript' ): Promise { const cursorMarker = '❚'; const cursorPos = textWithCursor.indexOf(cursorMarker); const prefix = textWithCursor.substring(0, cursorPos); const suffix = textWithCursor.substring(cursorPos + cursorMarker.length); const doc = createTextDocument('file:///test.ts', languageId, 0, prefix + suffix); const pos = doc.positionAt(cursorPos); const actualType = await getBlockPositionType(doc, pos); assert.strictEqual(actualType, expectedType); } });