1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-23 04:10:05 +08:00

fix(snippet): fix key binding SPC i s

close https://github.com/SpaceVim/SpaceVim/issues/4571
This commit is contained in:
wsdjeg 2022-02-03 19:05:50 +08:00
parent 16a5c8caf2
commit 95e2352940
14 changed files with 799 additions and 9 deletions

View File

@ -205,12 +205,6 @@ function! SpaceVim#layers#autocomplete#config() abort
inoremap <silent> <M-/> <C-R>=UltiSnips#ExpandSnippetOrJump()<cr>
endif
let g:_spacevim_mappings_space.i = {'name' : '+Insertion'}
if g:spacevim_snippet_engine ==# 'neosnippet'
call SpaceVim#mapping#space#def('nnoremap', ['i', 's'], 'Unite neosnippet', 'insert snippets', 1)
elseif g:spacevim_snippet_engine ==# 'ultisnips'
call SpaceVim#mapping#space#def('nnoremap', ['i', 's'], 'Unite ultisnips', 'insert snippets', 1)
endif
if !empty(g:_spacevim_key_sequence) && g:_spacevim_key_sequence !=# 'nil'
if g:spacevim_escape_key_binding !=# g:_spacevim_key_sequence
augroup spacevim_layer_autocomplete

View File

@ -61,6 +61,16 @@ function! SpaceVim#layers#leaderf#plugins() abort
" use this repo unicode data
call add(plugins, ['SpaceVim/Unite-sources', {'merged' : 0}])
" snippet
if g:spacevim_snippet_engine ==# 'neosnippet'
call add(plugins, [g:_spacevim_root_dir . 'bundle/LeaderF-neosnippet', {
\ 'merged' : 0,
\ 'loadconf' : 1}])
elseif g:spacevim_snippet_engine ==# 'ultisnips'
call add(plugins, [g:_spacevim_root_dir . 'bundle/LeaderF-snippet', {
\ 'merged' : 0,
\ 'loadconf' : 1}])
endif
return plugins
endfunction
@ -205,6 +215,11 @@ function! SpaceVim#layers#leaderf#config() abort
let g:_spacevim_mappings_space.i = {'name' : '+Insertion'}
call SpaceVim#mapping#space#def('nnoremap', ['i', 'u'], 'Leaderf unicode', 'search-and-insert-unicode', 1)
if g:spacevim_snippet_engine ==# 'neosnippet'
call SpaceVim#mapping#space#def('nnoremap', ['i', 's'], 'Leaderf neosnippet', 'insert snippets', 1)
elseif g:spacevim_snippet_engine ==# 'ultisnips'
call SpaceVim#mapping#space#def('nnoremap', ['i', 's'], 'Leaderf snippet', 'insert snippets', 1)
endif
let lnum = expand('<slnum>') + s:lnum - 1
call SpaceVim#mapping#space#def('nnoremap', ['?'], 'call call('

80
bundle/LeaderF-neosnippet/.gitignore vendored Normal file
View File

@ -0,0 +1,80 @@
# For current directory only
# ----------------------------------------------------------------------------
# General
# ----------------------------------------------------------------------------
*.o
*.out
# log
*.log
# cache
*.cache
cache/
# Windows
# ----------------------------------------------------------------------------
Thumbs.db
Desktop.ini
# Tags
# -----------------------------------------------------------------------------
TAGS
!TAGS/
tags
tags-cn
!tags/
.tags
.tags1
tags.lock
tags.temp
gtags.files
GTAGS
GRTAGS
GPATH
cscope.files
cscope.out
cscope.in.out
cscope.po.out
# Vim
# ------------------------------------------------------------------------------
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
/.vim
# Test % Tmp
# -------------------------------------------------------------------------------
test.*
tmp.*
temp.*
# Java
# -------------------------------------------------------------------------------
*.class
# JavaScript
# -------------------------------------------------------------------------------
node_modules
# Python
# -------------------------------------------------------------------------------
*.pyc
.idea/
/.idea
build/
__pycache__
# Rust
# -------------------------------------------------------------------------------
target/
**/*.rs.bk
# C/Cpp
# -------------------------------------------------------------------------------
/cmake-build-debug/

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020 tamago324
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,29 @@
# Leaderf-neosnippet
[LeaderF](https://github.com/Yggdroot/LeaderF) support for [neosnippet](https://github.com/Shougo/neosnippet.vim)
## Installation
```vim
Plug 'Yggdroot/LeaderF'
Plug 'tamago324/LeaderF-neosnippet'
Plug 'Shougo/neosnippet.vim'
Plug 'Shougo/neosnippet-snippets'
```
## Usage
```
:Leaderf neosnippet
```
or
```
:LeaderfNeosnippet
```
## License
Apache-2.0

View File

@ -0,0 +1,4 @@
command! -bar -nargs=0 LeaderfNeosnippet call execute("Leaderf neosnippet")
" In order to be listed by :LeaderfSelf
call g:LfRegisterSelf('LeaderfNeosnippet', 'neosnippet')

View File

@ -0,0 +1,55 @@
scriptencoding utf-8
let s:info = {
\ 'source': {},
\ 'col': 0,
\ 'ft': '',
\ 'preview_bufnr': -1,
\}
function! lf_neosnippet#source(...)
let l:snippets = neosnippet#helpers#get_completion_snippets()
let s:info.source = l:snippets
return keys(l:snippets)
endfunction
function! lf_neosnippet#accept(line, arg) abort
" from neosnippet.vim
let l:cur_text = neosnippet#util#get_cur_text()
let l:cur_keyword_str = matchstr(l:cur_text, '\S\+$')
call neosnippet#view#_expand(
\ l:cur_text . a:line[len(l:cur_keyword_str)], s:info.col, a:line)
endfunction
function! lf_neosnippet#preview(orig_buf_nr, orig_cursor, line, args) abort
let l:info = get(s:info.source, a:line, {})
let l:lines = split(get(l:info, 'snip', ''), "\n")
silent call deletebufline(s:info.preview_bufnr, 1, '$')
silent call setbufline(s:info.preview_bufnr, 1, l:lines)
" [buf_number, line_num, jump_cmd]
return [s:info.preview_bufnr, 1, '']
endfunction
function! lf_neosnippet#before_enter(args) abort
" If you do it in the preview, it slows down the cursor movement.
let l:bufnr = bufadd('lf_neosnippet_preview')
silent! call bufload(l:bufnr)
try
" from instance.py
call setbufvar(l:bufnr, '&buflisted', 0)
call setbufvar(l:bufnr, '&buftype', 'nofile')
call setbufvar(l:bufnr, '&bufhidden', 'hide')
call setbufvar(l:bufnr, '&undolevels', -1)
call setbufvar(l:bufnr, '&swapfile', 0)
call setbufvar(l:bufnr, '&filetype', &filetype)
catch /*/
" pass
endtry
let s:info.col = col('.')
let s:info.preview_bufnr = l:bufnr
endfunction

View File

@ -0,0 +1,9 @@
scriptencoding utf-8
let g:Lf_Extensions = get(g:, 'Lf_Extensions', {})
let g:Lf_Extensions.neosnippet = {
\ 'source': string(function('lf_neosnippet#source'))[10:-3],
\ 'accept': string(function('lf_neosnippet#accept'))[10:-3],
\ 'preview': string(function('lf_neosnippet#preview'))[10:-3],
\ 'before_enter': string(function('lf_neosnippet#before_enter'))[10:-3],
\}

129
bundle/LeaderF-snippet/.gitignore vendored Normal file
View File

@ -0,0 +1,129 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Linwei
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,61 @@
# Leaderf-snippet
This plugin takes the advantage of the well-known fuzzy finder [Leaderf](https://github.com/Yggdroot/LeaderF) to provide an intuitive way to input snippets:
![](https://github.com/skywind3000/images/raw/master/p/snippet/snippet1.gif)
Snippet names are hard to remember, therefore, I made a Leaderf extension to help input snippets.
## Feature
- Read snippets from SnipMate or UltiSnips
- Display snippet descriptions in the fuzzy finder.
- Work in both INSERT mode and NORMAL mode.
## Installation
```VimL
" Leaderf-snippet
Plug 'Yggdroot/LeaderF'
Plug 'skywind3000/Leaderf-snippet'
```
A supported snippet engine, [UltiSnips](https://github.com/SirVer/ultisnips) (recommended) or [SnipMate](https://github.com/garbas/vim-snipmate), is required.
## Configuration
```VimL
" maps
inoremap <c-x><c-x> <c-\><c-o>:Leaderf snippet<cr>
" optional: preview
let g:Lf_PreviewResult = get(g:, 'Lf_PreviewResult', {})
let g:Lf_PreviewResult.snippet = 1
```
## Why Leaderf ?
vim-fzf has a `Snippets` command, but it doesn't provide enough information for each snippet and it can't work correctly in INSERT mode:
![](https://github.com/skywind3000/images/raw/master/p/snippet/fzf-snippets.png)
Compare to fzf, Leaderf has a NORMAL mode which allows me to browse my snippets more easily like in a normal vim window:
![](https://github.com/skywind3000/images/raw/master/p/snippet/snippet2.gif)
Browse my snippets with full of details. No worry about forgetting snippets.
## TODO
- [x] snipmate
- [x] ultisnips
- [x] snipmate preview
- [x] ultisnips preview
- [ ] minisnip
## Credit
- [Leaderf](https://github.com/Yggdroot/LeaderF): An efficient fuzzy finder that helps to locate files, buffers, mrus, gtags, etc. on the fly.

View File

@ -0,0 +1,86 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#======================================================================
#
# leaderf_snippet.py -
#
# Created by skywind on 2021/02/01
# Last Modified: 2021/02/01 17:48:09
#
#======================================================================
from __future__ import unicode_literals, print_function
import sys
import time
import re
import vim
def init():
# print('init2')
return 0
def usnip_query():
source = []
vim.eval('UltiSnips#SnippetsInCurrentScope()')
items = vim.eval('items(g:current_ulti_dict_info)')
from UltiSnips import UltiSnips_Manager
import UltiSnips
manager = UltiSnips.UltiSnips_Manager
snippets = UltiSnips.UltiSnips_Manager._snips('', True)
snippets_values = {}
for snippet in snippets:
key = snippet.trigger
desc = snippet._description
snippets_values[key] = snippet._value
size = 4
for item in items:
key = item[0]
info = item[1]
desc = info.get('description', '')
value = snippets_values.get(key, '<unknow>')
desc = desc.strip()
size = max(size, len(key))
if not desc:
desc = usnip_simplify(value)
# desc = ''
source.append([key, desc, '', usnip_clear(value)])
source.sort()
for item in source:
item[2] = item[0] + (' ' * (size - len(item[0])))
return source
def usnip_clear(text):
t = re.sub('`[^`]*`', '', text)
if t.strip() == '':
t = text
return t
def usnip_simplify(text):
t = re.sub('`[^`]*`', '', text)
if t.strip() == '':
t = text
text = '\n'.join(t.split("\n")[:5])
text = re.sub('\${[^{}]*}', '...', text)
text = re.sub('\${[^{}]*}', '...', text)
text = text.replace("\n", ' ; ')
text = re.sub('\s+', ' ', text)
return text[:100]
def usnip_digest(text):
return 0
def test():
source = usnip_query()
for item in source:
key = item[0]
if key == 'def' or 0:
value = item[3]
print(key, value)
print('---')
print(usnip_clear(value))
print('---')
print(usnip_simplify(value))
return 0

View File

@ -0,0 +1,282 @@
"======================================================================
"
" leaderf_snippet.vim -
"
" Created by skywind on 2021/02/01
" Last Modified: 2021/02/13 21:07:12
"
"======================================================================
"----------------------------------------------------------------------
" Query SnipMate Database
"----------------------------------------------------------------------
function! SnipMateQuery(word, exact) abort
let matches = snipMate#GetSnippetsForWordBelowCursor(a:word, a:exact)
let result = []
let size = 4
for [trigger, dict] in matches
let body = ''
for key in keys(dict)
let value = dict[key]
if type(value) == v:t_list
if len(value) > 0
let body = value[0]
break
endif
endif
endfor
if body != ''
let size = max([size, len(trigger)])
let result += [[trigger, body]]
endif
endfor
for item in result
let t = item[0] . repeat(' ', size - len(item[0]))
call extend(item, [t])
endfor
call sort(result)
return result
endfunc
"----------------------------------------------------------------------
" Simplify Snippet Body
"----------------------------------------------------------------------
function! SnipMateDescription(body, width) abort
let text = join(split(a:body, '\n')[:4], ' ; ')
let text = substitute(text, '^\s*\(.\{-}\)\s*$', '\1', '')
let text = substitute(text, '\${[^{}]*}', '...', 'g')
let text = substitute(text, '\${[^{}]*}', '...', 'g')
let text = substitute(text, '\s\+', ' ', 'g')
let text = strcharpart(text, 0, a:width)
return text
endfunc
"----------------------------------------------------------------------
" Query Snippets
"----------------------------------------------------------------------
function! UltiSnipsQuery()
call UltiSnips#SnippetsInCurrentScope(1)
let list = []
let size = 4
for [key, info] in items(g:current_ulti_dict_info)
let desc = info.description
if desc == ''
let desc = '...'
endif
let size = max([size, len(key)])
let list += [[key, desc]]
endfor
call sort(list)
for item in list
let t = item[0] . repeat(' ', size - len(item[0]))
call extend(item, [t])
endfor
return list
endfunc
function! UltiSnipsQuery2()
call s:init_python()
if g:Lf_PythonVersion == 2
let matches = pyeval('leaderf_snippet.usnip_query()')
else
let matches = py3eval('leaderf_snippet.usnip_query()')
endif
let width = 100
for item in matches
let desc = item[1]
if desc == ''
" let desc = SnipMateDescription(item[3], width)
" let item[1] = desc
endif
endfor
return matches
endfunc
"----------------------------------------------------------------------
" checks
"----------------------------------------------------------------------
function! s:check_snipmate()
return (exists(':SnipMateOpenSnippetFiles') == 2)
endfunc
function! s:check_ultisnips()
return (exists(':UltiSnipsEdit') == 2)
endfunc
"----------------------------------------------------------------------
" internal
"----------------------------------------------------------------------
let s:bufid = -1
let s:filetype = ''
let s:accept = ''
let s:snips = {}
let s:snip_engine = -1
let s:inited = 0
let g:Lf_Extensions = get(g:, 'Lf_Extensions', {})
let s:home = fnamemodify(resolve(expand('<sfile>:p')), ':h')
function! s:init_python()
if s:inited != 0
return 0
endif
if s:check_snipmate()
let s:snip_engine = 0
let s:inited = 1
return 0
elseif s:check_ultisnips()
let s:snip_engine = 1
call UltiSnips#SnippetsInCurrentScope(1)
else
let s:snip_engine = -1
let s:inited = 1
return 0
endif
exec g:Lf_py 'import sys, vim'
exec g:Lf_py '_pp = vim.eval("s:home")'
exec g:Lf_py 'if _pp not in sys.path: sys.path.append(_pp)'
exec g:Lf_py 'import leaderf_snippet'
if g:Lf_PythonVersion == 2
exec 'py2' 'import imp'
exec 'py2' 'imp.reload(leaderf_snippet)'
else
exec 'py3' 'import importlib'
exec 'py3' 'importlib.reload(leaderf_snippet)'
endif
exec g:Lf_py 'leaderf_snippet.init()'
let s:inited = 1
return 1
endfunc
function! s:lf_snippet_source(...)
let source = []
if s:inited == 0
call s:init_python()
let s:inited = 1
endif
if s:snip_engine == 0
let matches = SnipMateQuery('', 0)
elseif s:snip_engine == 1
" let matches = UltiSnipsQuery()
let matches = UltiSnipsQuery2()
else
let error = "ERROR: Require UltiSnip (recommended) or SnipMate !!"
redraw
echohl ErrorMsg
echom error
echohl None
let source += [error]
let source += [error]
let source += [error]
return source
endif
let snips = {}
let width = 100
for item in matches
let trigger = item[0]
if trigger =~ '^\u'
continue
endif
if s:snip_engine == 0
let desc = SnipMateDescription(item[1], width)
let snips[trigger] = item[1]
else
let desc = item[1]
let snips[trigger] = item[3]
endif
let text = item[2] . ' ' . ' : ' . desc
let source += [text]
endfor
let s:snips = snips
return source
endfunc
" echo s:lf_snippet_source()
function! s:lf_snippet_accept(line, arg)
let pos = stridx(a:line, ':')
if pos < 0
return
endif
let name = strpart(a:line, 0, pos)
let name = substitute(name, '^\s*\(.\{-}\)\s*$', '\1', '')
redraw
if name != ''
let s:accept = name . "\<Plug>snipMateTrigger"
if s:snip_engine == 0
if mode(1) =~ 'i'
call feedkeys(name . "\<Plug>snipMateTrigger", '!')
" call feedkeys(name . "\<c-r>=snipMate#TriggerSnippet(1)\<cr>", '!')
else
call feedkeys('a' . name . "\<Plug>snipMateTrigger", '!')
endif
elseif s:snip_engine == 1
if mode(1) =~ 'i'
call feedkeys("\<right>", '!')
" call feedkeys("" . name . "\<m-e>", '!')
call feedkeys(name . "\<c-r>=UltiSnips#ExpandSnippet()\<cr>", '!')
" unsilent echom "col: ". col('.')
else
call feedkeys('a' . name . "\<c-r>=UltiSnips#ExpandSnippet()\<cr>", '!')
endif
endif
endif
endfunc
function! s:lf_snippet_preview(orig_buf_nr, orig_cursor, line, args)
let text = a:line
let pos = stridx(text, ':')
if pos < 0
return []
endif
let name = strpart(text, 0, pos)
let name = substitute(name, '^\s*\(.\{-}\)\s*$', '\1', '')
let body = get(s:snips, name, '')
if body == ''
unsilent echom "SUCK"
return []
endif
if s:bufid < 0
let s:bufid = bufadd('')
let bid = s:bufid
call bufload(bid)
call setbufvar(bid, '&buflisted', 0)
call setbufvar(bid, '&bufhidden', 'hide')
call setbufvar(bid, '&modifiable', 1)
call deletebufline(bid, 1, '$')
call setbufvar(bid, '&modified', 0)
call setbufvar(bid, 'current_syntax', '')
call setbufvar(bid, '&filetype', '')
endif
let bid = s:bufid
let textlist = split(body, '\n')
call setbufvar(bid, '&modifiable', 1)
call setbufline(bid, 1, textlist)
call setbufvar(bid, '&modified', 0)
call setbufvar(bid, '&modifiable', 0)
return [bid, 1, '']
endfunc
function! s:lf_win_init(...)
setlocal nonumber nowrap
endfunc
let g:Lf_Extensions.snippet = {
\ 'source': string(function('s:lf_snippet_source'))[10:-3],
\ 'accept': string(function('s:lf_snippet_accept'))[10:-3],
\ 'preview': string(function('s:lf_snippet_preview'))[10:-3],
\ 'highlights_def': {
\ 'Lf_hl_funcScope': '^\S\+',
\ },
\ 'after_enter': string(function('s:lf_win_init'))[10:-3],
\ }

View File

@ -17,7 +17,7 @@ description: "Autocomplete code within SpaceVim, fuzzy find the candidates from
- [Show snippets in auto-completion popup](#show-snippets-in-auto-completion-popup)
- [Key bindings](#key-bindings)
- [Completion](#completion)
- [Neosnippet](#neosnippet)
- [Snippets](#snippets)
<!-- vim-markdown-toc -->
@ -172,9 +172,13 @@ To disable this feature, set the variable `auto_completion_enable_snippets_in_po
| `Shift-Tab` | select previous candidate |
| `<Return>` | based on `auto_completion_return_key_behavior` |
### Neosnippet
### Snippets
| Key Binding | Description |
| ----------- | -------------------------------------------------------------- |
| `M-/` | Expand a snippet if text before point is a prefix of a snippet |
| `SPC i s` | List all current yasnippets for inserting |
| `SPC i s` | List all current snippets for inserting |
NOTE: `SPC i s` requires that at least one fuzzy search layer be loaded.