import * as assert from 'assert' ; import { before } from 'mocha' ; import { expect } from 'chai' ; import * as vscode from 'vscode' ; import { URI } from 'vscode-uri' ; import { ProblemCompletionItemProvider } from '../../completion/ProblemCompletionItemProvider' ; import { parser , ProblemInfo } from 'pddl-workspace' ; import { CodeDocumentPositionResolver } from '../../workspace/CodeDocumentPositionResolver' ; import { UnknownPddlCompletionItemProvider } from '../../completion/UnknownPddlCompletionItemProvider' ; import { DomainCompletionItemProvider } from '../../completion/DomainCompletionItemProvider' ; suite ( 'PDDL Completion Item Provider' , ( ) => { before ( async ( ) => { vscode . window . showInformationMessage ( 'Start all tests.' ) ; } ) ; test ( 'should offer to declare domain or problem' , async ( ) => { const inputTextHead = ' ' ; const ch = '(' ; const inputTextTail = ')' ; const items = await testProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assert . strictEqual ( items . length , 2 , 'there should be N completion items' ) ; items . forEach ( item => expect ( item . label ) . to . startWith ( '(define' ) ) ; } ) ; test ( 'should offer to declare pre-parsing meta data instruction' , async ( ) => { const inputTextHead = '' ; const ch = ';' ; const inputTextTail = '\n(define (problem p) (:domain d) )' ; const items = await testProblemProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assert . strictEqual ( items . length , 3 , 'there should be N completion items' ) ; items . forEach ( item => expect ( item . label ) . to . startWith ( ';;' ) ) ; } ) ; test ( 'should suggest domain sections upon invoke' , async ( ) => { const inputTextHead = '(define (domain d) \n' ; const ch = '' ; const inputTextTail = '\n)' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '(:requirements' , '(:types' , '(:constants' , '(:predicates' , '(:functions' , '(:constraints' , '(:action' , '(:durative-action' , '(:process' , '(:event' ] ) ; items . forEach ( item => assert . strictEqual ( item . range , undefined , `Range of ${ item . label } should be undefined` ) ) ; } ) ; test ( 'should suggest domain sections upon ( trigger' , async ( ) => { const inputTextHead = '(define (domain d) \n' ; const ch = '(' ; const inputTextTail = ')\n)' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . TriggerCharacter , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '(:requirements' , '(:types' , '(:constants' , '(:predicates' , '(:functions' , '(:constraints' , '(:action' , '(:durative-action' , '(:process' , '(:event' ] ) ; items . forEach ( item => assert . deepStrictEqual ( item . range , new vscode . Range ( 1 , 0 , 1 , 2 ) , `Range of ${ item . label }` ) ) ; } ) ; test ( 'should suggest domain sections upon : trigger' , async ( ) => { const inputTextHead = '(define (domain d) \n(' ; const ch = ':' ; const inputTextTail = ')\n)' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . TriggerCharacter , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '(:requirements' , '(:types' , '(:constants' , '(:predicates' , '(:functions' , '(:constraints' , '(:action' , '(:durative-action' , '(:process' , '(:event' ] ) ; items . forEach ( item => assert . deepStrictEqual ( item . range , new vscode . Range ( 1 , 0 , 1 , 3 ) , `Range of '${ item . label }'` ) ) ; } ) ; test ( 'should suggest requirements upon invoke' , async ( ) => { const inputTextHead = '(define (domain d) (:requirements ' ; const ch = '' ; const inputTextTail = ')\n)' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; items . some ( item => ( item . filterText ?? item . label ) === ':strips' ) ; items . forEach ( item => assert . strictEqual ( item . range , undefined , `Range of ${ item . label } should be undefined` ) ) ; } ) ; test ( 'should suggest requirements upon : trigger' , async ( ) => { const inputTextHead = '(define (domain d) (:requirements ' ; const ch = ':' ; const inputTextTail = ')\n)' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . TriggerCharacter , triggerCharacter : ch } ) ; items . some ( item => ( item . filterText ?? item . label ) === ':strips' ) ; items . forEach ( item => assert . deepStrictEqual ( item . range , new vscode . Range ( 0 , inputTextHead . length , 0 , inputTextHead . length + 1 ) , `Range of '${ item . label }'` ) ) ; } ) ; test ( 'should suggest (:action sections upon invoke' , async ( ) => { const inputTextHead = '(define (domain d) (:action\n' ; const ch = '' ; const inputTextTail = '\n))' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ ':parameters' , ':precondition' , ':effect' , ] ) ; items . forEach ( item => assert . strictEqual ( item . range , undefined , `Range of ${ item . label } should be undefined` ) ) ; } ) ; test ( 'should suggest (:action sections upon : trigger' , async ( ) => { const inputTextHead = '(define (domain d) (:action\n' ; const ch = ':' ; const inputTextTail = '\n))' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ ':parameters' , ':precondition' , ':effect' , ] ) ; items . forEach ( item => assert . deepStrictEqual ( item . range , new vscode . Range ( 1 , 0 , 1 , 1 ) , `Range of '${ item . label }'` ) ) ; } ) ; test ( 'should suggest only :effect for partially completed (:action upon : trigger' , async ( ) => { const inputTextHead = '(define (domain d) (:action :parameters() :precondition()\n' ; const ch = ':' ; const inputTextTail = '\n))' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ ':effect' , ] ) ; items . forEach ( item => assert . deepStrictEqual ( item . range , new vscode . Range ( 1 , 0 , 1 , 1 ) , `Range of '${ item . label }'` ) ) ; } ) ; test ( 'should suggest (:durative-action sections upon invoke' , async ( ) => { const inputTextHead = '(define (domain d) (:durative-action\n' ; const ch = '' ; const inputTextTail = '\n))' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ ':parameters' , ':duration' , ':condition' , ':effect' , ] ) ; items . forEach ( item => assert . strictEqual ( item . range , undefined , `Range of ${ item . label } should be undefined` ) ) ; } ) ; test ( 'should suggest (:durative-action sections upon : trigger' , async ( ) => { const inputTextHead = '(define (domain d) (:durative-action\n' ; const ch = ':' ; const inputTextTail = '\n))' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ ':parameters' , ':duration' , ':condition' , ':effect' , ] ) ; items . forEach ( item => assert . deepStrictEqual ( item . range , new vscode . Range ( 1 , 0 , 1 , 1 ) , `Range of '${ item . label }'` ) ) ; } ) ; test ( 'should suggest (:action effects upon invoke' , async ( ) => { const inputTextHead = '(define (domain d) (:predicates (p1)(p2)) (:action :effect (and\n' ; const ch = '' ; const inputTextTail = '\n)))' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assertSnippetIncludes ( items , "(not" , 'p1,p2' ) ; assertSnippetIncludes ( items , "(assign" , '(assign (${1:new_function}) ${2:0})$0' ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '(not' , '(assign' , '(increase' , '(decrease' , '(forall' , '(when' , ] ) ; items . forEach ( item => assert . strictEqual ( item . range , undefined , `Range of '${ item . label }' should be undefined` ) ) ; } ) ; test ( 'should suggest (:action effects upon ( trigger' , async ( ) => { const inputTextHead = '(define (domain d) (:predicates (p1)(p2)) (:action :effect (and\n' ; const ch = '(' ; const inputTextTail = ')\n)))' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . TriggerCharacter , triggerCharacter : ch } ) ; assertSnippetIncludes ( items , "(not" , 'p1,p2' ) ; assertSnippetIncludes ( items , "(assign" , '(assign (${1:new_function}) ${2:0})$0' ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '(not' , '(assign' , '(increase' , '(decrease' , '(forall' , '(when' , ] ) ; const expectedRange = new vscode . Range ( 1 , 0 , 1 , 2 ) ; items . forEach ( item => assert . deepStrictEqual ( item . range , expectedRange , `Range of '${ item . label }' should be ...` ) ) ; } ) ; test ( 'should suggest (:durative-action continuous effects and time-qualifiers upon invoke' , async ( ) => { const inputTextHead = '(define (domain d) (:functions (f1)) (:durative-action :effect (and\n' ; const ch = '' ; const inputTextTail = '\n)))' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assertSnippetIncludes ( items , "(increase" , 'f1' ) ; assertSnippetIncludes ( items , "(decrease" , '(decrease (${1|f1|}) (* #t ${2:1.0}))$0' ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '(at start' , '(at end' , '(increase' , '(decrease' , '(forall' , ] ) ; items . forEach ( item => assert . strictEqual ( item . range , undefined , `Range of '${ item . label }' should be undefined` ) ) ; } ) ; test ( 'should suggest (:process effects upon ( trigger' , async ( ) => { const inputTextHead = '(define (domain d) (:functions (f1)) (:process :effect (and\n' ; const ch = '(' ; const inputTextTail = ')\n)))' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . TriggerCharacter , triggerCharacter : ch } ) ; assertSnippetIncludes ( items , "(increase" , 'f1' ) ; assertSnippetIncludes ( items , "(decrease" , '(decrease (${1|f1|}) (* #t ${2:1.0}))$0' ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '(increase' , '(decrease' , '(forall' , ] ) ; const expectedRange = new vscode . Range ( 1 , 0 , 1 , 2 ) ; items . forEach ( item => assert . deepStrictEqual ( item . range , expectedRange , `Range of '${ item . label }' should be ...` ) ) ; } ) ; test ( 'should suggest (:durative-action condition time-qualifiers upon invoke' , async ( ) => { const inputTextHead = '(define (domain d) (:durative-action :condition (and\n' ; const ch = '' ; const inputTextTail = '\n)))' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '(at start' , '(at end' , '(over all' , ] ) ; items . forEach ( item => assert . strictEqual ( item . range , undefined , `Range of '${ item . label }' should be undefined` ) ) ; } ) ; test ( 'should suggest action and forall parameters upon ? trigger' , async ( ) => { const inputTextHead = '(define (domain d) (:action :parameters (?pa-1 ?pa_2 - type1) :condition (forall (?pfa - type2) \n' ; const ch = '?' ; const inputTextTail = '\n)))' ; const items = await testDomainProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . TriggerCharacter , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '?pfa' , '?pa-1' , '?pa_2' , ] ) ; const expectedRange = new vscode . Range ( 1 , 0 , 1 , 1 ) ; items . forEach ( item => assert . deepStrictEqual ( item . range , expectedRange , `Range of '${ item . label }' should be ...` ) ) ; } ) ; test ( 'should suggest problem sections upon invoke' , async ( ) => { const inputTextHead = '(define \n' ; const ch = '' ; const inputTextTail = '\n)' ; const items = await testProblemProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . Invoke , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '(problem' , '(:domain' , '(:requirements' , '(:objects' , '(:init' , '(:goal' , '(:constraints' , '(:metric' ] ) ; items . forEach ( item => assert . strictEqual ( item . range , undefined , `Range of ${ item . label } should be undefined` ) ) ; } ) ; test ( 'should suggest problem sections upon ( trigger' , async ( ) => { const inputTextHead = '(define \n' ; const ch = '(' ; const inputTextTail = ')\n)' ; const items = await testProblemProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . TriggerCharacter , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '(problem' , '(:domain' , '(:requirements' , '(:objects' , '(:init' , '(:goal' , '(:constraints' , '(:metric' ] ) ; items . forEach ( item => assert . deepStrictEqual ( item . range , new vscode . Range ( 1 , 0 , 1 , 2 ) , `Range of ${ item . label }` ) ) ; } ) ; test ( 'should suggest problem sections upon : trigger' , async ( ) => { const inputTextHead = '(define \n(' ; const ch = ':' ; const inputTextTail = ')\n)' ; const items = await testProblemProvider ( inputTextHead , ch , inputTextTail , { triggerKind : vscode . CompletionTriggerKind . TriggerCharacter , triggerCharacter : ch } ) ; assert . deepStrictEqual ( items . map ( i => i . filterText ?? i . label ) , [ '(:domain' , '(:requirements' , '(:objects' , '(:init' , '(:goal' , '(:constraints' , '(:metric' ] ) ; items . forEach ( item => assert . deepStrictEqual ( item . range , new vscode . Range ( 1 , 0 , 1 , 3 ) , `Range of '${ item . label }'` ) ) ; } ) ; } ) ; function assertSnippetIncludes ( items , filterText , needle ) { const item = items . find ( item => item . filterText === filterText ) ; assert . ok ( item , `Item '${ filterText }' should be included` ) ; const snippet = ( item ! . insertText as vscode . SnippetString ) ; assert . ok ( snippet . value . includes ( needle ) , `snippet '${ snippet . value }' should include ${ needle }` ) ; } async function testProvider ( inputTextHead , ch , inputTextTail , context ) { const initialText = inputTextHead + ch + inputTextTail ; const doc = await vscode . workspace . openTextDocument ( { language : 'pddl-do-not-load-extension' , content : initialText } ) ; const editor = await vscode . window . showTextDocument ( doc ) ; const position = doc . positionAt ( ( inputTextHead + ch ) . length ) ; editor . selection = new vscode . Selection ( position , position ) ; return await new UnknownPddlCompletionItemProvider ( ) . provide ( doc , position , context ) ; } async function testProblemProvider ( inputTextHead , ch , inputTextTail , context ) { const initialText = inputTextHead + ch + inputTextTail ; const doc = await vscode . workspace . openTextDocument ( { language : 'pddl-do-not-load-extension' , content : initialText } ) ; const editor = await vscode . window . showTextDocument ( doc ) ; const position = doc . positionAt ( ( inputTextHead + ch ) . length ) ; editor . selection = new vscode . Selection ( position , position ) ; const tree = new parser . PddlSyntaxTreeBuilder ( initialText ) . getTree ( ) ; const problemInfo = new ProblemInfo ( URI . parse ( 'file:///testProblem.pddl' ) , 1 , 'p' , 'd' , tree , new CodeDocumentPositionResolver ( doc ) ) ; return await new ProblemCompletionItemProvider ( ) . provide ( doc , problemInfo , position , context ) ; } async function testDomainProvider ( inputTextHead , ch , inputTextTail , context ) { const initialText = inputTextHead + ch + inputTextTail ; const doc = await vscode . workspace . openTextDocument ( { language : 'pddl-do-not-load-extension' , content : initialText } ) ; const editor = await vscode . window . showTextDocument ( doc ) ; const position = doc . positionAt ( ( inputTextHead + ch ) . length ) ; editor . selection = new vscode . Selection ( position , position ) ; const tree = new parser . PddlSyntaxTreeBuilder ( initialText ) . getTree ( ) ; const domainNode = tree . getDefineNodeOrThrow ( ) . getFirstOpenBracketOrThrow ( 'domain' ) ; const positionResolver = new CodeDocumentPositionResolver ( doc ) ; const domainInfo = new parser . PddlDomainParser ( ) . parse ( URI . parse ( 'file:///testProblem.pddl' ) , 1 , initialText , domainNode , tree , positionResolver ) ; if ( ! domainInfo ) { throw new Error ( `Unable to parse test domain.` ) ; } return await new DomainCompletionItemProvider ( ) . provide ( doc , domainInfo , position , context ) ; }