defineDescribe('Code Mirror Mode', [ './SequenceDiagram', 'cm/lib/codemirror-real', ], ( SequenceDiagram, CodeMirror ) => { 'use strict'; SequenceDiagram.registerCodeMirrorMode(CodeMirror); const cm = new CodeMirror(null, { value: '', mode: 'sequence', globals: { themes: ['Theme', 'Other Theme'], }, }); function getTokens(line) { return cm.getLineTokens(line).map((token) => ({ v: token.string, type: token.type, })); } describe('colouring', () => { it('highlights comments', () => { cm.getDoc().setValue('# foo'); expect(getTokens(0)).toEqual([ {v: '# foo', type: 'comment'}, ]); }); it('highlights valid keywords', () => { cm.getDoc().setValue('terminators cross'); expect(getTokens(0)).toEqual([ {v: 'terminators', type: 'keyword'}, {v: ' cross', type: 'keyword'}, ]); }); it('highlights comments after content', () => { cm.getDoc().setValue('terminators cross # foo'); expect(getTokens(0)).toEqual([ {v: 'terminators', type: 'keyword'}, {v: ' cross', type: 'keyword'}, {v: ' # foo', type: 'comment'}, ]); }); it('highlights invalid lines', () => { cm.getDoc().setValue('terminators huh'); expect(getTokens(0)[1].type).toContain('line-error'); }); it('highlights incomplete lines', () => { cm.getDoc().setValue('terminators'); expect(getTokens(0)[0].type).toContain('line-error'); }); it('highlights free text', () => { cm.getDoc().setValue('title my free text'); expect(getTokens(0)).toEqual([ {v: 'title', type: 'keyword'}, {v: ' my', type: 'string'}, {v: ' free', type: 'string'}, {v: ' text', type: 'string'}, ]); }); it('highlights quoted text', () => { cm.getDoc().setValue('title "my free text"'); expect(getTokens(0)).toEqual([ {v: 'title', type: 'keyword'}, {v: ' "my free text"', type: 'string'}, ]); }); it('highlights agent names', () => { cm.getDoc().setValue('A -> B'); expect(getTokens(0)).toEqual([ {v: 'A', type: 'variable'}, {v: ' ->', type: 'keyword'}, {v: ' B', type: 'variable'}, ]); }); it('does not consider quoted tokens as keywords', () => { cm.getDoc().setValue('A "->" -> B'); expect(getTokens(0)).toEqual([ {v: 'A', type: 'variable'}, {v: ' "->"', type: 'variable'}, {v: ' ->', type: 'keyword'}, {v: ' B', type: 'variable'}, ]); }); it('highlights agent aliasing syntax', () => { cm.getDoc().setValue('define A as B, C as D'); expect(getTokens(0)).toEqual([ {v: 'define', type: 'keyword'}, {v: ' A', type: 'variable'}, {v: ' as', type: 'keyword'}, {v: ' B', type: 'variable'}, {v: ',', type: 'operator'}, {v: ' C', type: 'variable'}, {v: ' as', type: 'keyword'}, {v: ' D', type: 'variable'}, ]); }); it('highlights multi-word agent names', () => { cm.getDoc().setValue('Foo Bar -> Zig Zag'); expect(getTokens(0)).toEqual([ {v: 'Foo', type: 'variable'}, {v: ' Bar', type: 'variable'}, {v: ' ->', type: 'keyword'}, {v: ' Zig', type: 'variable'}, {v: ' Zag', type: 'variable'}, ]); }); it('highlights connection operators without spaces', () => { cm.getDoc().setValue('abc->xyz'); expect(getTokens(0)).toEqual([ {v: 'abc', type: 'variable'}, {v: '->', type: 'keyword'}, {v: 'xyz', type: 'variable'}, ]); }); it('highlights the lost message operator without spaces', () => { cm.getDoc().setValue('abc-xxyz'); expect(getTokens(0)).toEqual([ {v: 'abc', type: 'variable'}, {v: '-x', type: 'keyword'}, {v: 'xyz', type: 'variable'}, ]); }); it('recognises agent flags on the right', () => { cm.getDoc().setValue('Foo -> *Bar'); expect(getTokens(0)).toEqual([ {v: 'Foo', type: 'variable'}, {v: ' ->', type: 'keyword'}, {v: ' *', type: 'operator'}, {v: 'Bar', type: 'variable'}, ]); }); it('recognises agent flags on the left', () => { cm.getDoc().setValue('*Foo -> Bar'); expect(getTokens(0)).toEqual([ {v: '*', type: 'operator'}, {v: 'Foo', type: 'variable'}, {v: ' ->', type: 'keyword'}, {v: ' Bar', type: 'variable'}, ]); }); it('rejects missing agent names', () => { cm.getDoc().setValue('+ -> Bar'); expect(getTokens(0)[2].type).toContain('line-error'); cm.getDoc().setValue('Bar -> +'); expect(getTokens(0)[2].type).toContain('line-error'); }); it('recognises found messages', () => { cm.getDoc().setValue('* -> Bar'); expect(getTokens(0)[2].type).not.toContain('line-error'); cm.getDoc().setValue('Bar <- *'); expect(getTokens(0)[2].type).not.toContain('line-error'); }); it('recognises combined agent flags', () => { cm.getDoc().setValue('Foo -> +*Bar'); expect(getTokens(0)).toEqual([ {v: 'Foo', type: 'variable'}, {v: ' ->', type: 'keyword'}, {v: ' +', type: 'operator'}, {v: '*', type: 'operator'}, {v: 'Bar', type: 'variable'}, ]); }); it('allows messages after connections', () => { cm.getDoc().setValue('Foo -> Bar: hello'); expect(getTokens(0)).toEqual([ {v: 'Foo', type: 'variable'}, {v: ' ->', type: 'keyword'}, {v: ' Bar', type: 'variable'}, {v: ':', type: 'operator'}, {v: ' hello', type: 'string'}, ]); }); it('recognises invalid agent flag combinations', () => { cm.getDoc().setValue('Foo -> *!Bar'); expect(getTokens(0)[3].type).toContain('line-error'); cm.getDoc().setValue('Foo -> +-Bar'); expect(getTokens(0)[3].type).toContain('line-error'); cm.getDoc().setValue('Foo -> +*-Bar'); expect(getTokens(0)[4].type).toContain('line-error'); }); it('highlights delayed message syntax', () => { cm.getDoc().setValue('A -> ...x\n...x -> B: hello'); expect(getTokens(0)).toEqual([ {v: 'A', type: 'variable'}, {v: ' ->', type: 'keyword'}, {v: ' ...', type: 'operator'}, {v: 'x', type: 'variable'}, ]); expect(getTokens(1)).toEqual([ {v: '...', type: 'operator'}, {v: 'x', type: 'variable'}, {v: ' ->', type: 'keyword'}, {v: ' B', type: 'variable'}, {v: ':', type: 'operator'}, {v: ' hello', type: 'string'}, ]); }); it('recognises invalid delayed messages', () => { cm.getDoc().setValue('A -> ...x: hello'); expect(getTokens(0)[4].type).toContain('line-error'); }); it('highlights block statements', () => { cm.getDoc().setValue( 'if\n' + 'if something\n' + 'else if another thing\n' + 'else\n' + 'end\n' + 'repeat a few times\n' + 'group foo\n' ); expect(getTokens(0)).toEqual([ {v: 'if', type: 'keyword'}, ]); expect(getTokens(1)).toEqual([ {v: 'if', type: 'keyword'}, {v: ' something', type: 'string'}, ]); expect(getTokens(2)).toEqual([ {v: 'else', type: 'keyword'}, {v: ' if', type: 'keyword'}, {v: ' another', type: 'string'}, {v: ' thing', type: 'string'}, ]); expect(getTokens(3)).toEqual([ {v: 'else', type: 'keyword'}, ]); expect(getTokens(4)).toEqual([ {v: 'end', type: 'keyword'}, ]); expect(getTokens(5)).toEqual([ {v: 'repeat', type: 'keyword'}, {v: ' a', type: 'string'}, {v: ' few', type: 'string'}, {v: ' times', type: 'string'}, ]); expect(getTokens(6)).toEqual([ {v: 'group', type: 'keyword'}, {v: ' foo', type: 'string'}, ]); }); it('allows colons in block statements', () => { cm.getDoc().setValue( 'if: something\n' + 'else if: another thing\n' + 'repeat: a few times' ); expect(getTokens(0)).toEqual([ {v: 'if', type: 'keyword'}, {v: ':', type: 'operator'}, {v: ' something', type: 'string'}, ]); expect(getTokens(1)).toEqual([ {v: 'else', type: 'keyword'}, {v: ' if', type: 'keyword'}, {v: ':', type: 'operator'}, {v: ' another', type: 'string'}, {v: ' thing', type: 'string'}, ]); expect(getTokens(2)).toEqual([ {v: 'repeat', type: 'keyword'}, {v: ':', type: 'operator'}, {v: ' a', type: 'string'}, {v: ' few', type: 'string'}, {v: ' times', type: 'string'}, ]); }); it('highlights note statements', () => { cm.getDoc().setValue( 'note over A: hi\n' + 'note over A, B: hi\n' + 'note left of A, B: hi\n' + 'note right of A, B: hi\n' + 'note between A, B: hi' ); expect(getTokens(0)).toEqual([ {v: 'note', type: 'keyword'}, {v: ' over', type: 'keyword'}, {v: ' A', type: 'variable'}, {v: ':', type: 'operator'}, {v: ' hi', type: 'string'}, ]); expect(getTokens(1)).toEqual([ {v: 'note', type: 'keyword'}, {v: ' over', type: 'keyword'}, {v: ' A', type: 'variable'}, {v: ',', type: 'operator'}, {v: ' B', type: 'variable'}, {v: ':', type: 'operator'}, {v: ' hi', type: 'string'}, ]); expect(getTokens(2)).toEqual([ {v: 'note', type: 'keyword'}, {v: ' left', type: 'keyword'}, {v: ' of', type: 'keyword'}, {v: ' A', type: 'variable'}, {v: ',', type: 'operator'}, {v: ' B', type: 'variable'}, {v: ':', type: 'operator'}, {v: ' hi', type: 'string'}, ]); expect(getTokens(3)).toEqual([ {v: 'note', type: 'keyword'}, {v: ' right', type: 'keyword'}, {v: ' of', type: 'keyword'}, {v: ' A', type: 'variable'}, {v: ',', type: 'operator'}, {v: ' B', type: 'variable'}, {v: ':', type: 'operator'}, {v: ' hi', type: 'string'}, ]); expect(getTokens(4)).toEqual([ {v: 'note', type: 'keyword'}, {v: ' between', type: 'keyword'}, {v: ' A', type: 'variable'}, {v: ',', type: 'operator'}, {v: ' B', type: 'variable'}, {v: ':', type: 'operator'}, {v: ' hi', type: 'string'}, ]); }); it('rejects notes between a single agent', () => { cm.getDoc().setValue('note between A: hi'); expect(getTokens(0)[3].type).toContain('line-error'); }); it('highlights state statements', () => { cm.getDoc().setValue('state over A: hi'); expect(getTokens(0)).toEqual([ {v: 'state', type: 'keyword'}, {v: ' over', type: 'keyword'}, {v: ' A', type: 'variable'}, {v: ':', type: 'operator'}, {v: ' hi', type: 'string'}, ]); }); it('rejects state over multiple agents', () => { cm.getDoc().setValue('state over A, B: hi'); expect(getTokens(0)[3].type).toContain('line-error'); }); it('highlights divider statements', () => { cm.getDoc().setValue('divider tear with height 60: stuff'); expect(getTokens(0)).toEqual([ {v: 'divider', type: 'keyword'}, {v: ' tear', type: 'keyword'}, {v: ' with', type: 'keyword'}, {v: ' height', type: 'keyword'}, {v: ' 60', type: 'number'}, {v: ':', type: 'operator'}, {v: ' stuff', type: 'string'}, ]); }); it('highlights agent info statements', () => { cm.getDoc().setValue('A is a red database'); expect(getTokens(0)).toEqual([ {v: 'A', type: 'variable'}, {v: ' is', type: 'keyword'}, {v: ' a', type: 'keyword'}, {v: ' red', type: 'keyword'}, {v: ' database', type: 'keyword'}, ]); }); it('rejects unknown info statements', () => { cm.getDoc().setValue('A is a foobar'); expect(getTokens(0)[3].type).toContain('line-error'); }); }); describe('autocomplete', () => { function getHints(pos, {completeSingle = true} = {}) { const hintFn = cm.getHelpers(pos, 'hint')[0]; cm.setCursor(pos); return hintFn(cm, Object.assign({completeSingle}, cm.options)); } function getHintTexts(pos, options) { const hints = getHints(pos, options); return hints.list.map((hint) => hint.text); } it('suggests commands when used at the start of a line', () => { cm.getDoc().setValue(''); const hints = getHintTexts({line: 0, ch: 0}); expect(hints).toContain('theme '); expect(hints).toContain('title '); expect(hints).toContain('headers '); expect(hints).toContain('terminators '); expect(hints).toContain('divider '); expect(hints).toContain('define '); expect(hints).toContain('begin '); expect(hints).toContain('end '); expect(hints).toContain('if '); expect(hints).toContain('else\n'); expect(hints).toContain('else if: '); expect(hints).toContain('repeat '); expect(hints).toContain('group '); expect(hints).toContain('note '); expect(hints).toContain('state over '); expect(hints).toContain('text '); expect(hints).toContain('autolabel '); expect(hints).toContain('simultaneously '); }); it('ignores indentation', () => { cm.getDoc().setValue(' '); const hints = getHintTexts({line: 0, ch: 2}); expect(hints).toContain('theme '); expect(hints).toContain('title '); }); it('suggests known header types', () => { cm.getDoc().setValue('headers '); const hints = getHintTexts({line: 0, ch: 8}); expect(hints).toEqual([ 'none\n', 'cross\n', 'box\n', 'fade\n', 'bar\n', ]); }); it('suggests known terminator types', () => { cm.getDoc().setValue('terminators '); const hints = getHintTexts({line: 0, ch: 12}); expect(hints).toEqual([ 'none\n', 'cross\n', 'box\n', 'fade\n', 'bar\n', ]); }); it('suggests divider types', () => { cm.getDoc().setValue('divider '); const hints = getHintTexts({line: 0, ch: 8}); expect(hints).toEqual([ 'line ', 'space ', 'delay ', 'tear ', '\n', ': ', 'with height ', ]); }); it('suggests divider sizes', () => { cm.getDoc().setValue('divider space with height '); const hints = getHintTexts({line: 0, ch: 26}); expect(hints).toEqual([ '6 ', '30 ', ]); }); it('suggests useful autolabel values', () => { cm.getDoc().setValue('autolabel '); const hints = getHintTexts({line: 0, ch: 10}); expect(hints).toContain('off\n'); expect(hints).toContain('"