Skip to content

Commit

Permalink
feat(completion): source using vim9script module
Browse files Browse the repository at this point in the history
Related #5263
  • Loading branch information
chemzqm committed Feb 23, 2025
1 parent dca0049 commit 78c7f47
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 15 deletions.
8 changes: 5 additions & 3 deletions autoload/coc.vim
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ let s:error_sign = get(g:, 'coc_status_error_sign', has('mac') && s:utf ? "\u274
let s:warning_sign = get(g:, 'coc_status_warning_sign', has('mac') && s:utf ? "\u26a0\ufe0f " : 'W ')
let s:select_api = exists('*nvim_select_popupmenu_item')
let s:callbacks = {}
let s:fns = ['init', 'complete', 'should_complete', 'refresh', 'get_startcol', 'on_complete', 'on_enter']
let s:all_fns = s:fns + map(copy(s:fns), 'toupper(strpart(v:val, 0, 1)) . strpart(v:val, 1)')

function! coc#expandable() abort
return coc#rpc#request('snippetCheck', [1, 0])
Expand Down Expand Up @@ -151,9 +153,8 @@ function! coc#_suggest_variables() abort
endfunction

function! coc#_remote_fns(name)
let fns = ['init', 'complete', 'should_complete', 'refresh', 'get_startcol', 'on_complete', 'on_enter']
let res = []
for fn in fns
for fn in s:all_fns
if exists('*coc#source#'.a:name.'#'.fn)
call add(res, fn)
endif
Expand All @@ -162,7 +163,8 @@ function! coc#_remote_fns(name)
endfunction

function! coc#_do_complete(name, opt, cb) abort
let handler = 'coc#source#'.a:name.'#complete'
let method = get(a:opt, 'vim9', v:false) ? 'Complete' : 'complete'
let handler = 'coc#source#'.a:name.'#'.method
let l:Cb = {res -> a:cb(v:null, res)}
let args = [a:opt, l:Cb]
call call(handler, args)
Expand Down
17 changes: 17 additions & 0 deletions doc/coc-api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ folder. With code:
`init` and `complete` are required functions for vim sources, error message
will be shown when not exists.

vim9script can be also used on vim9 (not supported on neovim), the function
first letter need to be uppercased, like:
>
vim9script
export def Init(): dict<any>
return {
priority: 9,
shortcut: 'Email',
triggerCharacters: ['@']
}
enddef
export def Complete(option: dict<any>, Callback: func(list<any>))
const items = ['[email protected]', '[email protected]']
Callback(items)
enddef
<
Source option: ~

The source option object is returned by `coc#source#{name}#init`
Expand Down
1 change: 1 addition & 0 deletions history.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Notable changes of coc.nvim:

- All global properties works with extensions #5222.
- Return true or false for boolean option on vim (same as neovim).
- Support completion sources using vim9sciprt module.

## 2025-02-22

Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/autoload/coc/source/vim9.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
vim9script
export def Init(): dict<any>
return {
priority: 9,
shortcut: 'Email',
triggerCharacters: ['@']
}
enddef

export def Complete(option: dict<any>, Callback: func(list<any>))
const items = ['[email protected]', '[email protected]']
Callback(items)
enddef
13 changes: 12 additions & 1 deletion src/__tests__/completion/sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Around } from '../../completion/native/around'
import { Buffer } from '../../completion/native/buffer'
import { File, filterFiles, getDirectory, getFileItem, getLastPart, getItemsFromRoot, resolveEnvVariables } from '../../completion/native/file'
import Source, { firstMatchFuzzy } from '../../completion/source'
import VimSource from '../../completion/source-vim'
import VimSource, { getMethodName, checkInclude } from '../../completion/source-vim'
import sources, { Sources, logError, getSourceType } from '../../completion/sources'
import { CompleteOption, ExtendedCompleteItem, SourceConfig, SourceType } from '../../completion/types'
import { disposeAll } from '../../util'
Expand Down Expand Up @@ -500,4 +500,15 @@ describe('native sources', () => {
let items = await helper.items()
expect(items.map(o => o.word)).toEqual(['foo', 'bar'])
})

it('should get method name', () => {
expect(getMethodName('f', ['f', 'o'])).toBe('f')
expect(getMethodName('foo', ['Foo', 'Bar'])).toBe('Foo')
expect(() => {
getMethodName('foo', ['Bar'])
}).toThrow()
expect(checkInclude('f', ['f', 'o'])).toBe(true)
expect(checkInclude('b', ['f', 'o'])).toBe(false)
expect(checkInclude('foo', ['Foo', 'Bar'])).toBe(true)
})
})
25 changes: 19 additions & 6 deletions src/completion/source-vim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,32 @@ import Source from './source'
import * as Is from '../util/is'
import { CompleteOption, CompleteResult, ExtendedCompleteItem } from './types'

export function getMethodName(name: string, names: ReadonlyArray<string>): string | undefined {
if (names.includes(name)) return name
let key = name[0].toUpperCase() + name.slice(1)
if (names.includes(key)) return key
throw new Error(`${name} not exists`)
}

export function checkInclude(name: string, fns: ReadonlyArray<string>): boolean {
if (fns.includes(name)) return true
let key = name[0].toUpperCase() + name.slice(1)
return fns.includes(key)
}

export default class VimSource extends Source {

private async callOptionalFunc(fname: string, args: any[]): Promise<any> {
let exists = this.optionalFns.includes(fname)
let exists = checkInclude(fname, this.optionalFns)
if (!exists) return null
let name = `coc#source#${this.name}#${fname}`
let name = `coc#source#${this.name}#${getMethodName(fname, this.optionalFns)}`
return await this.nvim.call(name, args)
}

public async checkComplete(opt: CompleteOption): Promise<boolean> {
let shouldRun = await super.checkComplete(opt)
if (!shouldRun) return false
if (!this.optionalFns.includes('should_complete')) return true
if (!checkInclude('should_complete', this.optionalFns)) return true
let res = await this.callOptionalFunc('should_complete', [opt])
return !!res
}
Expand All @@ -41,15 +54,15 @@ export default class VimSource extends Source {
}

public async onCompleteDone(item: ExtendedCompleteItem, opt: CompleteOption): Promise<void> {
if (this.optionalFns.includes('on_complete')) {
if (checkInclude('on_complete', this.optionalFns)) {
await this.callOptionalFunc('on_complete', [item])
} else if (item.isSnippet && item.insertText) {
await this.insertSnippet(item.insertText, opt)
}
}

public onEnter(bufnr: number): void {
if (!this.optionalFns.includes('on_enter')) return
if (!checkInclude('on_enter', this.optionalFns)) return
let doc = workspace.getDocument(bufnr)
if (!doc) return
let { filetypes } = this
Expand All @@ -75,7 +88,7 @@ export default class VimSource extends Source {
input
})
}
let vimItems = await this.nvim.callAsync('coc#_do_complete', [this.name, opt]) as (ExtendedCompleteItem | string)[]
let vimItems = await this.nvim.callAsync('coc#_do_complete', [this.name, { ...opt, vim9: this.isVim9 }]) as (ExtendedCompleteItem | string)[]
if (!vimItems || vimItems.length == 0 || token.isCancellationRequested) return null
let checkFirst = this.firstMatch && input.length > 0
let inputFirst = checkFirst ? input[0].toLowerCase() : ''
Expand Down
2 changes: 2 additions & 0 deletions src/completion/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default class Source implements ISource<ExtendedCompleteItem> {
public readonly sourceType: SourceType
public readonly isSnippet: boolean
public readonly documentSelector: DocumentSelector | undefined
public readonly isVim9: boolean
/**
* Words that not match during session
* The word that not match previous input would not match further input
Expand All @@ -46,6 +47,7 @@ export default class Source implements ISource<ExtendedCompleteItem> {
constructor(option: Partial<SourceConfig>) {
// readonly properties
this.name = option.name
this.isVim9 = option.isVim9 === true
this.filepath = option.filepath || ''
this.sourceType = option.sourceType || SourceType.Native
this.isSnippet = !!option.isSnippet
Expand Down
19 changes: 14 additions & 5 deletions src/completion/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import BufferSync from '../model/bufferSync'
import type { CompletionItemProvider, DocumentSelector } from '../provider'
import { disposeAll } from '../util'
import { intersect, isFalsyOrEmpty, toArray } from '../util/array'
import { statAsync } from '../util/fs'
import { readFileLines, statAsync } from '../util/fs'
import * as Is from '../util/is'
import { fs, path, promisify } from '../util/node'
import { Disposable } from '../util/protocol'
Expand All @@ -17,7 +17,7 @@ import workspace from '../workspace'
import { KeywordsBuffer } from './keywords'
import Source from './source'
import LanguageSource from './source-language'
import VimSource from './source-vim'
import VimSource, { getMethodName } from './source-vim'
import { CompleteItem, CompleteOption, ExtendedCompleteItem, ISource, SourceConfig, SourceStat, SourceType } from './types'
import { getPriority } from './util'
const logger = createLogger('sources')
Expand Down Expand Up @@ -129,12 +129,14 @@ export class Sources {
let name = path.basename(filepath, '.vim')
await nvim.command(`source ${filepath}`)
let fns = await nvim.call('coc#_remote_fns', name) as string[]
let lowercased = fns.map(fn => fn[0].toLowerCase() + fn.slice(1))
for (let fn of ['init', 'complete']) {
if (!fns.includes(fn)) {
if (!lowercased.includes(fn)) {
throw new Error(`function "coc#source#${name}#${fn}" not found`)
}
}
let props = await nvim.call(`coc#source#${name}#init`, []) as VimSourceConfig
const isVim9 = fns.includes('Complete')
let props = await nvim.call(`coc#source#${name}#${getMethodName('init', fns)}`, []) as VimSourceConfig
let packageJSON = {
name: `coc-vim-source-${name}`,
engines: {
Expand Down Expand Up @@ -195,10 +197,11 @@ export class Sources {
let source = new VimSource({
name,
filepath,
isVim9,
isSnippet: props.isSnippet,
sourceType: SourceType.Remote,
triggerOnly: !!props.triggerOnly,
optionalFns: fns.filter(n => !['init', 'complete'].includes(n))
optionalFns: fns.filter(n => !['init', 'complete', 'Init', 'Complete'].includes(n))
})
this.addSource(source)
return Promise.resolve()
Expand All @@ -212,7 +215,13 @@ export class Sources {
this.removeSource(name)
})
} catch (e) {
if (!this.nvim.isVim) {
let lines = await readFileLines(filepath, 0, 1)
if (lines.length > 0 && lines[0].startsWith('vim9script')) return
}
void window.showErrorMessage(`Error on create vim source from ${filepath}: ${e}`)
// logError(err)
logger.error(`Error on create vim source from ${filepath}`, e)
}
}

Expand Down
1 change: 1 addition & 0 deletions src/completion/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export interface SourceConfig<T extends ExtendedCompleteItem = ExtendedCompleteI
documentSelector?: DocumentSelector
firstMatch?: boolean
optionalFns?: string[]
isVim9?: boolean
refresh?(): Promise<void>
toggle?(): void
onEnter?(bufnr: number): void
Expand Down

0 comments on commit 78c7f47

Please sign in to comment.