import { Buffer, Neovim } from '@chemzqm/neovim' import { CodeAction, CodeActionKind, Disposable, DocumentSymbol, Range, SymbolKind, SymbolTag, TextEdit } from 'vscode-languageserver-protocol' import events from '../../events' import Symbols from '../../handler/symbols/index' import languages from '../../languages' import { ProviderResult } from '../../provider' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' import Parser from './parser' let nvim: Neovim let symbols: Symbols let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim symbols = helper.plugin.getHandler().symbols }) beforeEach(() => { disposables.push(languages.registerDocumentSymbolProvider([{ language: 'javascript' }], { provideDocumentSymbols: document => { let content = document.getText() let showDetail = content.includes('detail') let parser = new Parser(content, showDetail) let res: DocumentSymbol[] = parser.parse() if (res.length) { res[0].tags = [SymbolTag.Deprecated] } return Promise.resolve(res) } })) }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) disposables = [] await helper.reset() await nvim.command(`let w:cocViewId = ''`) }) async function getOutlineBuffer(): Promise { let winid = await nvim.call('coc#window#find', ['cocViewId', 'OUTLINE']) if (winid == -1) return undefined let bufnr = await nvim.call('winbufnr', [winid]) if (bufnr == -1) return undefined return nvim.createBuffer(bufnr) } describe('symbols outline', () => { let defaultCode = `class myClass { fun1() { } fun2() {} }` async function createBuffer(code = defaultCode): Promise { await helper.edit() let buf = await nvim.buffer await nvim.command('setf javascript') await buf.setLines(code.split('\n'), { start: 0, end: -1, strictIndexing: false }) let doc = await workspace.document await doc.synchronize() return buf } describe('configuration', () => { it('should follow cursor', async () => { await createBuffer() let curr = await nvim.call('bufnr', ['%']) await symbols.showOutline(0) let bufnr = await nvim.call('bufnr', ['%']) await nvim.command('wincmd p') await nvim.command('exe 3') await events.fire('CursorHold', [curr]) await helper.wait(50) let buf = nvim.createBuffer(bufnr) let lines = await buf.getLines() expect(lines.slice(1)).toEqual([ '- c myClass 1', ' m fun1 2', ' m fun2 3' ]) let signs = await buf.getSigns({ group: 'CocTree' }) expect(signs.length).toBe(1) expect(signs[0]).toEqual({ lnum: 2, id: 3001, name: 'CocTreeSelected', priority: 10, group: 'CocTree' }) }) it('should not follow cursor', async () => { workspace.configurations.updateUserConfig({ 'outline.followCursor': false, }) await createBuffer() let curr = await nvim.call('bufnr', ['%']) await symbols.showOutline(0) let bufnr = await nvim.call('bufnr', ['%']) await nvim.command('wincmd p') await nvim.command('exe 3') await events.fire('CursorHold', [curr]) await helper.wait(50) let buf = nvim.createBuffer(bufnr) let signs = await buf.getSigns({ group: 'CocTree' }) expect(signs.length).toBe(0) }) it('should keep current window', async () => { workspace.configurations.updateUserConfig({ 'outline.keepWindow': true, }) await createBuffer() let curr = await nvim.call('bufnr', ['%']) await symbols.showOutline() let bufnr = await nvim.call('bufnr', ['%']) expect(curr).toBe(bufnr) }) it('should check on buffer switch', async () => { workspace.configurations.updateUserConfig({ 'outline.checkBufferSwitch': true, }) await createBuffer() await symbols.showOutline(1) await helper.edit('unnamed') await helper.wait(200) let buf = await getOutlineBuffer() let lines = await buf.lines expect(lines[0]).toMatch('Document symbol provider not found') }) it('should not check on buffer switch', async () => { workspace.configurations.updateUserConfig({ 'outline.checkBufferSwitch': false }) await helper.wait(30) await createBuffer() await symbols.showOutline(1) await helper.edit('unnamed') await helper.wait(100) let buf = await getOutlineBuffer() let lines = await buf.lines expect(lines.slice(1)).toEqual([ '- c myClass 1', ' m fun1 2', ' m fun2 3' ]) }) it('should not check on buffer reload', async () => { workspace.configurations.updateUserConfig({ 'outline.checkBufferSwitch': false }) await symbols.showOutline(1) await helper.wait(50) await createBuffer() await helper.wait(50) let buf = await getOutlineBuffer() expect(buf).toBeDefined() }) it('should sort by position', async () => { let code = `class myClass { fun2() { } fun1() {} }` workspace.configurations.updateUserConfig({ 'outline.sortBy': 'position', }) await createBuffer(code) await symbols.showOutline(1) let buf = await getOutlineBuffer() let lines = await buf.lines expect(lines).toEqual([ 'OUTLINE Position', '- c myClass 1', ' m fun2 2', ' m fun1 3' ]) }) it('should sort by name', async () => { let code = `class myClass { fun2() {} fun1() {} }` workspace.configurations.updateUserConfig({ 'outline.sortBy': 'name', }) await createBuffer(code) await symbols.showOutline(1) let buf = await getOutlineBuffer() let lines = await buf.lines expect(lines).toEqual([ 'OUTLINE Name', '- c myClass 1', ' m fun1 3', ' m fun2 2' ]) }) it('should change sort method', async () => { workspace.configurations.updateUserConfig({ 'outline.detailAsDescription': false }) let code = `class detail { fun2() {} fun1() {} }` await createBuffer(code) await symbols.showOutline(0) await helper.wait(30) await nvim.input('') await helper.waitFloat() await nvim.input('') await helper.wait(30) await nvim.input('') await helper.waitFloat() await nvim.input('3') await helper.waitFor('getline', [1], 'OUTLINE Position') }) it('should show detail as description', async () => { workspace.configurations.updateUserConfig({ 'outline.detailAsDescription': true }) let code = `class detail { fun2() {} }` await createBuffer(code) await symbols.showOutline(1) let buf = await getOutlineBuffer() let lines = await buf.lines expect(lines.slice(1)).toEqual([ '- c detail 1', ' m fun2 () 2' ]) }) }) describe('events', () => { it('should not close TreeView on buffer reload', async () => { await createBuffer() await symbols.showOutline(0) await nvim.command('edit') await helper.wait(30) let winid = await nvim.call('coc#window#find', ['cocViewId', 'OUTLINE']) expect(winid).toBeGreaterThan(0) }) it('should dispose on buffer unload', async () => { await createBuffer() let curr = await nvim.call('bufnr', ['%']) await symbols.showOutline(0) await nvim.command('tabe') await nvim.command(`bd! ${curr}`) await helper.wait(30) let buf = await getOutlineBuffer() expect(buf).toBeUndefined() }) it('should check current window on BufEnter', async () => { await createBuffer() await symbols.showOutline(1) let winid = await nvim.call('win_getid', []) await nvim.command('enew') await helper.wait(100) let win = await nvim.window expect(win.id).toBe(winid) }) it('should recreated when original window exists', async () => { await symbols.showOutline(1) await helper.wait(50) await createBuffer() await helper.wait(50) let buf = await getOutlineBuffer() expect(buf).toBeDefined() }) it('should keep old outline when new buffer not attached', async () => { await createBuffer() await symbols.showOutline(1) await nvim.command(`vnew +setl\\ buftype=nofile`) await helper.wait(50) let buf = await getOutlineBuffer() expect(buf).toBeDefined() let lines = await buf.lines expect(lines.slice(1)).toEqual([ '- c myClass 1', ' m fun1 2', ' m fun2 3' ]) }) it('should not reload when switch to original buffer', async () => { await createBuffer() await symbols.showOutline(0) let buf = await getOutlineBuffer() let name = await buf.name await nvim.command('wincmd p') await helper.wait(50) buf = await getOutlineBuffer() let curr = await buf.name expect(curr).toBe(name) }) }) describe('show()', () => { it('should not throw when document not attached', async () => { await nvim.command(`edit +setl\\ buftype=nofile t`) await workspace.document await symbols.showOutline(1) }) it('should not throw when provider does not exist', async () => { await symbols.showOutline(1) let buf = await getOutlineBuffer() expect(buf).toBeDefined() }) it('should not throw when symbols is empty', async () => { await createBuffer('') await symbols.showOutline(1) let buf = await getOutlineBuffer() expect(buf).toBeDefined() }) it('should jump to selected symbol', async () => { await createBuffer() let bufnr = await nvim.call('bufnr', ['%']) await symbols.showOutline(0) await helper.waitFor('getline', [3], ' m fun1 2') await nvim.command('exe 3') await nvim.input('') await helper.wait(50) let curr = await nvim.call('bufnr', ['%']) expect(curr).toBe(bufnr) let cursor = await nvim.call('coc#cursor#position') expect(cursor).toEqual([1, 2]) }) it('should update symbols', async () => { await createBuffer() let doc = await workspace.document let bufnr = await nvim.call('bufnr', ['%']) await symbols.showOutline(1) await helper.wait(10) let buf = nvim.createBuffer(bufnr) let code = 'class foo{}' await buf.setLines(code.split('\n'), { start: 0, end: -1, strictIndexing: false }) await doc.synchronize() buf = await getOutlineBuffer() await helper.waitFor('eval', [`getbufline(${buf.id},1)[0]`], /No\sresults/) let lines = await buf.lines expect(lines).toEqual([ 'No results', '', 'OUTLINE Category' ]) }) it('should show label in description', async () => { disposables.push(languages.registerDocumentSymbolProvider([{ language: 'vim' }], { meta: { label: 'vimlsp' }, provideDocumentSymbols: _ => { let res: DocumentSymbol[] = [{ name: 'let', range: Range.create(0, 0, 0, 3), kind: SymbolKind.Constant, selectionRange: Range.create(0, 0, 0, 3), tags: [SymbolTag.Deprecated] }] return Promise.resolve(res) } })) let doc = await helper.createDocument('t.vim') await nvim.command('setf vim') let buf = await nvim.buffer await buf.setLines(['let'], { start: 0, end: -1, strictIndexing: false }) await doc.synchronize() await symbols.showOutline(0) await helper.waitFor('getline', [1], 'OUTLINE vimlsp') }) }) describe('actions', () => { it('should invoke visual select', async () => { await createBuffer() let bufnr = await nvim.call('bufnr', ['%']) await symbols.showOutline(0) await helper.waitFor('getline', [3], /fun1/) await nvim.command('exe 3') await nvim.input('') await helper.waitFloat() await nvim.input('') await helper.waitFor('mode', [], 'v') let buf = await nvim.buffer expect(buf.id).toBe(bufnr) }) it('should invoke selected code action', async () => { const codeAction = CodeAction.create('my action', CodeActionKind.Refactor) let uri: string disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { provideCodeActions: () => [codeAction], resolveCodeAction: (action): ProviderResult => { action.edit = { changes: { [uri]: [TextEdit.del(Range.create(0, 0, 0, 5))] } } return action } }, undefined)) await createBuffer() let bufnr = await nvim.call('bufnr', ['%']) let doc = workspace.getDocument(bufnr) uri = doc.uri await symbols.showOutline(0) await helper.wait(200) await nvim.command('exe 3') await nvim.input('') await helper.wait(50) await nvim.input('') await helper.wait(200) let buf = await nvim.buffer let lines = await buf.lines expect(lines[0]).toBe(' myClass {') }) }) describe('hide()', () => { it('should hide outline', async () => { await createBuffer('') await symbols.showOutline(1) await helper.wait(50) await symbols.hideOutline() let buf = await getOutlineBuffer() expect(buf).toBeUndefined() }) it('should not throw when outline does not exist', async () => { await symbols.hideOutline() let buf = await getOutlineBuffer() expect(buf).toBeUndefined() }) }) describe('dispose', () => { it('should dispose provider and views', async () => { await createBuffer('') let bufnr = await nvim.call('bufnr', ['%']) await symbols.showOutline(1) symbols.dispose() await helper.wait(50) expect(symbols.hasOutline(bufnr)).toBe(false) let buf = await getOutlineBuffer() expect(buf).toBeUndefined() }) }) })