feat(extmarks,ts,spell): full support for spelling

- Added 'spell' option to extmarks:

  Extmarks with this set will have the region spellchecked.

- Added 'noplainbuffer' option to 'spelloptions':

  This is used to tell Neovim not to spellcheck the buffer. The old
  behaviour was to spell check the whole buffer unless :syntax was set.

- Added spelling support to the treesitter highlighter:

  @spell captures in highlights.scm are used to define regions which
  should be spell checked.

- Added support for navigating spell errors for extmarks:

  Works for both ephemeral and static extmarks

- Added '_on_spell_nav' callback for decoration providers:

  Since ephemeral callbacks are only drawn for the visible screen,
  providers must implement this callback to instruct Neovim which
  regions in the buffer need can be spell checked.

  The callback takes a start position and an end position.

  Note: this callback is subject to change hence the _ prefix.

- Added spell captures for built-in support languages

Co-authored-by: Lewis Russell <lewis6991@gmail.com>
Co-authored-by: Björn Linse <bjorn.linse@gmail.com>
This commit is contained in:
Thomas Vigouroux 2022-07-18 14:21:40 +02:00 committed by Lewis Russell
parent 05893aea39
commit 75adfefc85
21 changed files with 313 additions and 95 deletions

View File

@ -2646,6 +2646,8 @@ nvim_buf_set_extmark({buffer}, {ns_id}, {line}, {col}, {*opts})
When a character is supplied it is used as |:syn-cchar|.
"hl_group" is used as highlight for the cchar if provided,
otherwise it defaults to |hl-Conceal|.
• spell: boolean indicating that spell checking should be
performed within this extmark
• ui_watched: boolean that indicates the mark should be
drawn by a UI. When set, the UI will receive win_extmark
events. Note: the mark is positioned by virt_text
@ -2677,7 +2679,7 @@ nvim_get_namespaces() *nvim_get_namespaces()*
dict that maps from names to namespace ids.
*nvim_set_decoration_provider()*
nvim_set_decoration_provider({ns_id}, {opts})
nvim_set_decoration_provider({ns_id}, {*opts})
Set or change decoration provider for a namespace
This is a very general purpose interface for having lua callbacks being
@ -2709,7 +2711,7 @@ nvim_set_decoration_provider({ns_id}, {opts})
Parameters: ~
{ns_id} Namespace id from |nvim_create_namespace()|
{opts} Callbacks invoked during redraw:
{opts} Table of callbacks:
• on_start: called first on each screen redraw ["start",
tick]
• on_buf: called for each buffer being redrawn (before window

View File

@ -5871,10 +5871,14 @@ A jump table for the options with a short description can be found at |Q_op|.
'spelloptions' 'spo' string (default "")
local to buffer
A comma-separated list of options for spell checking:
camel When a word is CamelCased, assume "Cased" is a
camel When a word is CamelCased, assume "Cased" is a
separate word: every upper-case character in a word
that comes after a lower case character indicates the
start of a new word.
noplainbuffer Only spellcheck a buffer when 'syntax' is enabled, or
or when extmarks are set within the buffer. Only
designated regions of the buffer are spellchecked in
this case.
*'spellsuggest'* *'sps'*
'spellsuggest' 'sps' string (default "best")

View File

@ -97,6 +97,7 @@ function TSHighlighter.new(tree, opts)
if vim.g.syntax_on ~= 1 then
vim.api.nvim_command('runtime! syntax/synload.vim')
end
vim.bo[self.bufnr].spelloptions = 'noplainbuffer'
self.tree:parse()
@ -156,7 +157,7 @@ function TSHighlighter:get_query(lang)
end
---@private
local function on_line_impl(self, buf, line)
local function on_line_impl(self, buf, line, spell)
self.tree:for_each_tree(function(tstree, tree)
if not tstree then
return
@ -193,7 +194,9 @@ local function on_line_impl(self, buf, line)
local start_row, start_col, end_row, end_col = node:range()
local hl = highlighter_query.hl_cache[capture]
if hl and end_row >= line then
local is_spell = highlighter_query:query().captures[capture] == 'spell'
if hl and end_row >= line and (not spell or is_spell) then
a.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
end_line = end_row,
end_col = end_col,
@ -201,6 +204,7 @@ local function on_line_impl(self, buf, line)
ephemeral = true,
priority = tonumber(metadata.priority) or 100, -- Low but leaves room below
conceal = metadata.conceal,
spell = is_spell,
})
end
if start_row > line then
@ -217,7 +221,21 @@ function TSHighlighter._on_line(_, _win, buf, line, _)
return
end
on_line_impl(self, buf, line)
on_line_impl(self, buf, line, false)
end
---@private
function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
local self = TSHighlighter.active[buf]
if not self then
return
end
self:reset_highlight_state()
for row = srow, erow do
on_line_impl(self, buf, row, true)
end
end
---@private
@ -244,6 +262,7 @@ a.nvim_set_decoration_provider(ns, {
on_buf = TSHighlighter._on_buf,
on_win = TSHighlighter._on_win,
on_line = TSHighlighter._on_line,
_on_spell_nav = TSHighlighter._on_spell_nav,
})
return TSHighlighter

View File

@ -101,6 +101,7 @@
[ "(" ")" "[" "]" "{" "}"] @punctuation.bracket
(string_literal) @string
(string_literal) @spell
(system_lib_string) @string
(null) @constant.builtin
@ -148,6 +149,7 @@
(comment) @comment
(comment) @spell
;; Parameters
(parameter_declaration

View File

@ -181,12 +181,14 @@
;; Others
(comment) @comment
(comment) @spell
(hash_bang_line) @comment
(number) @number
(string) @string
(string) @spell
;; Error
(ERROR) @error

View File

@ -162,9 +162,11 @@
;; Literals
(string_literal) @string
(string_literal) @spell
(integer_literal) @number
(float_literal) @float
(comment) @comment
(comment) @spell
(pattern) @string.special
(pattern_multi) @string.regex
(filename) @string

View File

@ -473,6 +473,8 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e
/// When a character is supplied it is used as |:syn-cchar|.
/// "hl_group" is used as highlight for the cchar if provided,
/// otherwise it defaults to |hl-Conceal|.
/// - spell: boolean indicating that spell checking should be
/// performed within this extmark
/// - ui_watched: boolean that indicates the mark should be drawn
/// by a UI. When set, the UI will receive win_extmark events.
/// Note: the mark is positioned by virt_text attributes. Can be
@ -719,6 +721,11 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer
bool ephemeral = false;
OPTION_TO_BOOL(ephemeral, ephemeral, false);
OPTION_TO_BOOL(decor.spell, spell, false);
if (decor.spell) {
has_decor = true;
}
OPTION_TO_BOOL(decor.ui_watched, ui_watched, false);
if (decor.ui_watched) {
has_decor = true;
@ -972,20 +979,21 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start,
/// for the moment.
///
/// @param ns_id Namespace id from |nvim_create_namespace()|
/// @param opts Callbacks invoked during redraw:
/// @param opts Table of callbacks:
/// - on_start: called first on each screen redraw
/// ["start", tick]
/// - on_buf: called for each buffer being redrawn (before window
/// callbacks)
/// - on_buf: called for each buffer being redrawn (before
/// window callbacks)
/// ["buf", bufnr, tick]
/// - on_win: called when starting to redraw a specific window.
/// - on_win: called when starting to redraw a
/// specific window.
/// ["win", winid, bufnr, topline, botline_guess]
/// - on_line: called for each buffer line being redrawn. (The
/// interaction with fold lines is subject to change)
/// - on_line: called for each buffer line being redrawn.
/// (The interaction with fold lines is subject to change)
/// ["win", winid, bufnr, row]
/// - on_end: called at the end of a redraw cycle
/// ["end", tick]
void nvim_set_decoration_provider(Integer ns_id, DictionaryOf(LuaRef) opts, Error *err)
void nvim_set_decoration_provider(Integer ns_id, Dict(set_decoration_provider) *opts, Error *err)
FUNC_API_SINCE(7) FUNC_API_LUA_ONLY
{
DecorProvider *p = get_decor_provider((NS)ns_id, true);
@ -997,37 +1005,32 @@ void nvim_set_decoration_provider(Integer ns_id, DictionaryOf(LuaRef) opts, Erro
struct {
const char *name;
Object *source;
LuaRef *dest;
} cbs[] = {
{ "on_start", &p->redraw_start },
{ "on_buf", &p->redraw_buf },
{ "on_win", &p->redraw_win },
{ "on_line", &p->redraw_line },
{ "on_end", &p->redraw_end },
{ "_on_hl_def", &p->hl_def },
{ NULL, NULL },
{ "on_start", &opts->on_start, &p->redraw_start },
{ "on_buf", &opts->on_buf, &p->redraw_buf },
{ "on_win", &opts->on_win, &p->redraw_win },
{ "on_line", &opts->on_line, &p->redraw_line },
{ "on_end", &opts->on_end, &p->redraw_end },
{ "_on_hl_def", &opts->_on_hl_def, &p->hl_def },
{ "_on_spell_nav", &opts->_on_spell_nav, &p->spell_nav },
{ NULL, NULL, NULL },
};
for (size_t i = 0; i < opts.size; i++) {
String k = opts.items[i].key;
Object *v = &opts.items[i].value;
size_t j;
for (j = 0; cbs[j].name && cbs[j].dest; j++) {
if (strequal(cbs[j].name, k.data)) {
if (v->type != kObjectTypeLuaRef) {
api_set_error(err, kErrorTypeValidation,
"%s is not a function", cbs[j].name);
goto error;
}
*(cbs[j].dest) = v->data.luaref;
v->data.luaref = LUA_NOREF;
break;
}
for (size_t i = 0; cbs[i].source && cbs[i].dest && cbs[i].name; i++) {
Object *v = cbs[i].source;
if (v->type == kObjectTypeNil) {
continue;
}
if (!cbs[j].name) {
api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data);
if (v->type != kObjectTypeLuaRef) {
api_set_error(err, kErrorTypeValidation,
"%s is not a function", cbs[i].name);
goto error;
}
*(cbs[i].dest) = v->data.luaref;
v->data.luaref = LUA_NOREF;
}
p->active = true;

View File

@ -2,6 +2,15 @@ return {
context = {
"types";
};
set_decoration_provider = {
"on_start";
"on_buf";
"on_win";
"on_line";
"on_end";
"_on_hl_def";
"_on_spell_nav";
};
set_extmark = {
"id";
"end_line";
@ -28,6 +37,7 @@ return {
"line_hl_group";
"cursorline_hl_group";
"conceal";
"spell";
"ui_watched";
};
keymap = {

View File

@ -462,6 +462,9 @@ typedef struct {
char *b_p_spf; // 'spellfile'
char *b_p_spl; // 'spelllang'
char *b_p_spo; // 'spelloptions'
#define SPO_CAMEL 0x1
#define SPO_NPBUFFER 0x2
unsigned b_p_spo_flags; // 'spelloptions' flags
int b_cjk; // all CJK letters as OK
uint8_t b_syn_chartab[32]; // syntax iskeyword option
char *b_syn_isk; // iskeyword option

View File

@ -69,7 +69,7 @@ void bufhl_add_hl_pos_offset(buf_T *buf, int src_id, int hl_id, lpos_T pos_start
void decor_redraw(buf_T *buf, int row1, int row2, Decoration *decor)
{
if (row2 >= row1) {
if (!decor || decor->hl_id || decor_has_sign(decor) || decor->conceal) {
if (!decor || decor->hl_id || decor_has_sign(decor) || decor->conceal || decor->spell) {
redraw_buf_range_later(buf, row1 + 1, row2 + 1);
}
}
@ -116,6 +116,11 @@ void decor_free(Decoration *decor)
}
}
void decor_state_free(DecorState *state)
{
xfree(state->active.items);
}
void clear_virttext(VirtText *text)
{
for (size_t i = 0; i < kv_size(*text); i++) {
@ -306,6 +311,7 @@ next_mark:
bool conceal = 0;
int conceal_char = 0;
int conceal_attr = 0;
bool spell = false;
for (size_t i = 0; i < kv_size(state->active); i++) {
DecorRange item = kv_A(state->active, i);
@ -339,6 +345,9 @@ next_mark:
conceal_attr = item.attr_id;
}
}
if (active && item.decor.spell) {
spell = true;
}
if ((item.start_row == state->row && item.start_col <= col)
&& decor_virt_pos(item.decor)
&& item.decor.virt_text_pos == kVTOverlay && item.win_col == -1) {
@ -355,6 +364,7 @@ next_mark:
state->conceal = conceal;
state->conceal_char = conceal_char;
state->conceal_attr = conceal_attr;
state->spell = spell;
return attr;
}

View File

@ -46,6 +46,7 @@ struct Decoration {
bool hl_eol;
bool virt_lines_above;
bool conceal;
bool spell;
// TODO(bfredl): style, etc
DecorPriority priority;
int col; // fixed col value, like win_col
@ -61,8 +62,8 @@ struct Decoration {
bool ui_watched; // watched for win_extmark
};
#define DECORATION_INIT { KV_INITIAL_VALUE, KV_INITIAL_VALUE, 0, kVTEndOfLine, \
kHlModeUnknown, false, false, false, false, DECOR_PRIORITY_BASE, \
0, 0, NULL, 0, 0, 0, 0, 0, false }
kHlModeUnknown, false, false, false, false, false, \
DECOR_PRIORITY_BASE, 0, 0, NULL, 0, 0, 0, 0, 0, false }
typedef struct {
int start_row;
@ -90,6 +91,8 @@ typedef struct {
bool conceal;
int conceal_char;
int conceal_attr;
bool spell;
} DecorState;
EXTERN DecorState decor_state INIT(= { 0 });

View File

@ -7,6 +7,7 @@
#include "nvim/decoration.h"
#include "nvim/decoration_provider.h"
#include "nvim/highlight.h"
#include "nvim/lib/kvec.h"
#include "nvim/lua/executor.h"
static kvec_t(DecorProvider) decor_providers = KV_INITIAL_VALUE;
@ -14,7 +15,7 @@ static kvec_t(DecorProvider) decor_providers = KV_INITIAL_VALUE;
#define DECORATION_PROVIDER_INIT(ns_id) (DecorProvider) \
{ ns_id, false, LUA_NOREF, LUA_NOREF, \
LUA_NOREF, LUA_NOREF, LUA_NOREF, \
LUA_NOREF, -1, false }
LUA_NOREF, -1, false, false }
static bool decor_provider_invoke(NS ns_id, const char *name, LuaRef ref, Array args,
bool default_true, char **perr)
@ -47,11 +48,33 @@ static bool decor_provider_invoke(NS ns_id, const char *name, LuaRef ref, Array
return false;
}
void decor_providers_invoke_spell(win_T *wp, int start_row, int start_col, int end_row, int end_col,
char **err)
{
for (size_t i = 0; i < kv_size(decor_providers); i++) {
DecorProvider *p = &kv_A(decor_providers, i);
if (!p->active) {
continue;
}
if (p->spell_nav != LUA_NOREF) {
MAXSIZE_TEMP_ARRAY(args, 6);
ADD_C(args, INTEGER_OBJ(wp->handle));
ADD_C(args, INTEGER_OBJ(wp->w_buffer->handle));
ADD_C(args, INTEGER_OBJ(start_row));
ADD_C(args, INTEGER_OBJ(start_col));
ADD_C(args, INTEGER_OBJ(end_row));
ADD_C(args, INTEGER_OBJ(end_col));
decor_provider_invoke(p->ns_id, "spell", p->spell_nav, args, true, err);
}
}
}
/// For each provider invoke the 'start' callback
///
/// @param[out] providers Decoration providers
/// @param[out] err Provider err
void decor_providers_start(DecorProviders *providers, int type, char **err)
void decor_providers_start(DecorProviders *providers, char **err)
{
kvi_init(*providers);
@ -65,7 +88,6 @@ void decor_providers_start(DecorProviders *providers, int type, char **err)
if (p->redraw_start != LUA_NOREF) {
MAXSIZE_TEMP_ARRAY(args, 2);
ADD_C(args, INTEGER_OBJ((int)display_tick));
ADD_C(args, INTEGER_OBJ(type));
active = decor_provider_invoke(p->ns_id, "start", p->redraw_start, args, true, err);
} else {
active = true;
@ -116,8 +138,8 @@ void decor_providers_invoke_win(win_T *wp, DecorProviders *providers,
/// @param row Row to invoke line callback for
/// @param[out] has_decor Set when at least one provider invokes a line callback
/// @param[out] err Provider error
void providers_invoke_line(win_T *wp, DecorProviders *providers, int row, bool *has_decor,
char **err)
void decor_providers_invoke_line(win_T *wp, DecorProviders *providers, int row, bool *has_decor,
char **err)
{
for (size_t k = 0; k < kv_size(*providers); k++) {
DecorProvider *p = kv_A(*providers, k);
@ -215,6 +237,7 @@ void decor_provider_clear(DecorProvider *p)
NLUA_CLEAR_REF(p->redraw_win);
NLUA_CLEAR_REF(p->redraw_line);
NLUA_CLEAR_REF(p->redraw_end);
NLUA_CLEAR_REF(p->spell_nav);
p->active = false;
}

View File

@ -12,6 +12,7 @@ typedef struct {
LuaRef redraw_line;
LuaRef redraw_end;
LuaRef hl_def;
LuaRef spell_nav;
int hl_valid;
bool hl_cached;
} DecorProvider;

View File

@ -11,9 +11,11 @@
#include "nvim/arabic.h"
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/charset.h"
#include "nvim/cursor.h"
#include "nvim/cursor_shape.h"
#include "nvim/decoration.h"
#include "nvim/diff.h"
#include "nvim/drawline.h"
#include "nvim/fold.h"
@ -654,7 +656,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool nochange,
has_decor = decor_redraw_line(buf, lnum - 1, &decor_state);
providers_invoke_line(wp, providers, lnum - 1, &has_decor, provider_err);
decor_providers_invoke_line(wp, providers, lnum - 1, &has_decor, provider_err);
if (*provider_err) {
provider_err_virt_text(lnum, *provider_err);
@ -1646,7 +1648,8 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool nochange,
ptr++;
if (extra_check) {
bool can_spell = true;
bool no_plain_buffer = (wp->w_s->b_p_spo_flags & SPO_NPBUFFER) != 0;
bool can_spell = !no_plain_buffer;
// Get syntax attribute, unless still at the start of the line
// (double-wide char that doesn't fit).
@ -1698,6 +1701,29 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool nochange,
char_attr = 0;
}
if (has_decor && v > 0) {
bool selected = (area_active || (area_highlighting && noinvcur
&& (colnr_T)vcol == wp->w_virtcol));
int extmark_attr = decor_redraw_col(wp->w_buffer, (colnr_T)v - 1, off,
selected, &decor_state);
if (extmark_attr != 0) {
if (!attr_pri) {
char_attr = hl_combine_attr(char_attr, extmark_attr);
} else {
char_attr = hl_combine_attr(extmark_attr, char_attr);
}
}
decor_conceal = decor_state.conceal;
if (decor_conceal && decor_state.conceal_char) {
decor_conceal = 2; // really??
}
if (decor_state.spell) {
can_spell = true;
}
}
// Check spelling (unless at the end of the line).
// Only do this when there is no syntax highlighting, the
// @Spell cluster is not used or the current syntax item
@ -1706,9 +1732,9 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool nochange,
if (has_spell && v >= word_end && v > cur_checked_col) {
spell_attr = 0;
if (!attr_pri) {
char_attr = syntax_attr;
char_attr = hl_combine_attr(char_attr, syntax_attr);
}
if (c != 0 && (!has_syntax || can_spell)) {
if (c != 0 && ((!has_syntax && !no_plain_buffer) || can_spell)) {
char_u *prev_ptr;
char_u *p;
int len;
@ -1781,25 +1807,6 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool nochange,
char_attr = hl_combine_attr(term_attrs[vcol], char_attr);
}
if (has_decor && v > 0) {
bool selected = (area_active || (area_highlighting && noinvcur
&& (colnr_T)vcol == wp->w_virtcol));
int extmark_attr = decor_redraw_col(wp->w_buffer, (colnr_T)v - 1, off,
selected, &decor_state);
if (extmark_attr != 0) {
if (!attr_pri) {
char_attr = hl_combine_attr(char_attr, extmark_attr);
} else {
char_attr = hl_combine_attr(extmark_attr, char_attr);
}
}
decor_conceal = decor_state.conceal;
if (decor_conceal && decor_state.conceal_char) {
decor_conceal = 2; // really??
}
}
// Found last space before word: check for line break.
if (wp->w_p_lbr && c0 == c && vim_isbreak(c)
&& !vim_isbreak((int)(*ptr))) {

View File

@ -539,7 +539,7 @@ int update_screen(int type)
ui_comp_set_screen_valid(true);
DecorProviders providers;
decor_providers_start(&providers, type, &provider_err);
decor_providers_start(&providers, &provider_err);
// "start" callback could have changed highlights for global elements
if (win_check_ns_hl(NULL)) {

View File

@ -70,7 +70,8 @@ void extmark_set(buf_T *buf, uint32_t ns_id, uint32_t *idp, int row, colnr_T col
|| kv_size(decor->virt_lines)
|| decor->conceal
|| decor_has_sign(decor)
|| decor->ui_watched) {
|| decor->ui_watched
|| decor->spell) {
decor_full = true;
decor = xmemdup(decor, sizeof *decor);
}

View File

@ -731,6 +731,7 @@ EXTERN char *p_spc; ///< 'spellcapcheck'
EXTERN char *p_spf; ///< 'spellfile'
EXTERN char *p_spl; ///< 'spelllang'
EXTERN char *p_spo; // 'spelloptions'
EXTERN unsigned int spo_flags;
EXTERN char *p_sps; // 'spellsuggest'
EXTERN int p_spr; // 'splitright'
EXTERN int p_sol; // 'startofline'

View File

@ -2355,6 +2355,7 @@ return {
secure=true,
expand=true,
varname='p_spo',
redraw={'current_buffer'},
defaults={if_true=""}
},
{

View File

@ -107,6 +107,7 @@ static char *(p_fdc_values[]) = { "auto", "auto:1", "auto:2", "auto:3", "auto:4"
"auto:6", "auto:7", "auto:8", "auto:9", "0", "1", "2", "3", "4",
"5", "6", "7", "8", "9", NULL };
static char *(p_cb_values[]) = { "unnamed", "unnamedplus", NULL };
static char *(p_spo_values[]) = { "camel", "noplainbuffer", NULL };
static char *(p_icm_values[]) = { "nosplit", "split", NULL };
static char *(p_jop_values[]) = { "stack", "view", NULL };
static char *(p_tpf_values[]) = { "BS", "HT", "FF", "ESC", "DEL", "C0", "C1", NULL };
@ -1125,7 +1126,8 @@ char *did_set_string_option(int opt_idx, char **varp, char *oldval, char *errbuf
// When 'spellcapcheck' is set compile the regexp program.
errmsg = compile_cap_prog(curwin->w_s);
} else if (varp == &(curwin->w_s->b_p_spo)) { // 'spelloptions'
if (**varp != NUL && STRCMP("camel", *varp) != 0) {
if (opt_strings_flags(curwin->w_s->b_p_spo, p_spo_values, &(curwin->w_s->b_p_spo_flags),
true) != OK) {
errmsg = e_invarg;
}
} else if (varp == &p_sps) { // 'spellsuggest'

View File

@ -71,6 +71,7 @@
#include "nvim/change.h" // for changed_bytes
#include "nvim/charset.h" // for skipwhite, getwhitecols, skipbin
#include "nvim/cursor.h" // for get_cursor_line_ptr
#include "nvim/decoration.h"
#include "nvim/drawscreen.h" // for NOT_VALID, redraw_later
#include "nvim/eval/typval.h" // for semsg
#include "nvim/ex_cmds.h" // for do_sub_msg
@ -220,7 +221,7 @@ size_t spell_check(win_T *wp, char_u *ptr, hlf_T *attrp, int *capcol, bool docou
size_t nrlen = 0; // found a number first
size_t wrongcaplen = 0;
bool count_word = docount;
bool use_camel_case = *wp->w_s->b_p_spo != NUL;
bool use_camel_case = (wp->w_s->b_p_spo_flags & SPO_CAMEL) != 0;
bool camel_case = false;
// A word never starts at a space or a control character. Return quickly
@ -1198,6 +1199,24 @@ bool no_spell_checking(win_T *wp)
return false;
}
static void decor_spell_nav_start(win_T *wp)
{
decor_state = (DecorState){ 0 };
decor_redraw_reset(wp->w_buffer, &decor_state);
}
static bool decor_spell_nav_col(win_T *wp, linenr_T lnum, linenr_T *decor_lnum, int col,
char **decor_error)
{
if (*decor_lnum != lnum) {
decor_providers_invoke_spell(wp, lnum - 1, col, lnum - 1, -1, decor_error);
decor_redraw_line(wp->w_buffer, lnum - 1, &decor_state);
*decor_lnum = lnum;
}
decor_redraw_col(wp->w_buffer, col, col, false, &decor_state);
return decor_state.spell;
}
/// Moves to the next spell error.
/// "curline" is false for "[s", "]s", "[S" and "]S".
/// "curline" is true to find word under/after cursor in the same line.
@ -1216,11 +1235,11 @@ size_t spell_move_to(win_T *wp, int dir, bool allwords, bool curline, hlf_T *att
hlf_T attr = HLF_COUNT;
size_t len;
int has_syntax = syntax_present(wp);
int col;
colnr_T col;
char_u *buf = NULL;
size_t buflen = 0;
int skip = 0;
int capcol = -1;
colnr_T capcol = -1;
bool found_one = false;
bool wrapped = false;
@ -1228,6 +1247,8 @@ size_t spell_move_to(win_T *wp, int dir, bool allwords, bool curline, hlf_T *att
return 0;
}
size_t ret = 0;
// Start looking for bad word at the start of the line, because we can't
// start halfway through a word, we don't know where it starts or ends.
//
@ -1240,6 +1261,19 @@ size_t spell_move_to(win_T *wp, int dir, bool allwords, bool curline, hlf_T *att
linenr_T lnum = wp->w_cursor.lnum;
clearpos(&found_pos);
char *decor_error = NULL;
// Ephemeral extmarks are currently stored in the global decor_state.
// When looking for spell errors, we need to:
// - temporarily reset decor_state
// - run the _on_spell_nav decor callback for each line we look at
// - detect if any spell marks are present
// - restore decor_state to the value saved here.
// TODO(lewis6991): un-globalize decor_state and allow ephemeral marks to be stored into a
// temporary DecorState.
DecorState saved_decor_start = decor_state;
linenr_T decor_lnum = -1;
decor_spell_nav_start(wp);
while (!got_int) {
char_u *line = ml_get_buf(wp->w_buffer, lnum, false);
@ -1258,10 +1292,10 @@ size_t spell_move_to(win_T *wp, int dir, bool allwords, bool curline, hlf_T *att
// For checking first word with a capital skip white space.
if (capcol == 0) {
capcol = (int)getwhitecols((char *)line);
capcol = (colnr_T)getwhitecols((char *)line);
} else if (curline && wp == curwin) {
// For spellbadword(): check if first word needs a capital.
col = (int)getwhitecols((char *)line);
col = (colnr_T)getwhitecols((char *)line);
if (check_need_cap(lnum, col)) {
capcol = col;
}
@ -1308,33 +1342,37 @@ size_t spell_move_to(win_T *wp, int dir, bool allwords, bool curline, hlf_T *att
|| ((colnr_T)(curline
? p - buf + (ptrdiff_t)len
: p - buf) > wp->w_cursor.col)) {
bool can_spell;
if (has_syntax) {
col = (int)(p - buf);
(void)syn_get_id(wp, lnum, (colnr_T)col,
false, &can_spell, false);
if (!can_spell) {
attr = HLF_COUNT;
}
} else {
can_spell = true;
col = (colnr_T)(p - buf);
bool can_spell = (wp->w_s->b_p_spo_flags & SPO_NPBUFFER) == 0;
if (!can_spell) {
can_spell = decor_spell_nav_col(wp, lnum, &decor_lnum, col, &decor_error);
}
if (!can_spell && has_syntax) {
(void)syn_get_id(wp, lnum, col, false, &can_spell, false);
}
if (!can_spell) {
attr = HLF_COUNT;
}
if (can_spell) {
found_one = true;
found_pos = (pos_T) {
.lnum = lnum,
.col = (int)(p - buf),
.col = col,
.coladd = 0
};
if (dir == FORWARD) {
// No need to search further.
wp->w_cursor = found_pos;
xfree(buf);
if (attrp != NULL) {
*attrp = attr;
}
return len;
ret = len;
goto theend;
} else if (curline) {
// Insert mode completion: put cursor after
// the bad word.
@ -1358,8 +1396,8 @@ size_t spell_move_to(win_T *wp, int dir, bool allwords, bool curline, hlf_T *att
if (dir == BACKWARD && found_pos.lnum != 0) {
// Use the last match in the line (before the cursor).
wp->w_cursor = found_pos;
xfree(buf);
return found_len;
ret = found_len;
goto theend;
}
if (curline) {
@ -1429,8 +1467,12 @@ size_t spell_move_to(win_T *wp, int dir, bool allwords, bool curline, hlf_T *att
line_breakcheck();
}
theend:
decor_state_free(&decor_state);
xfree(decor_error);
decor_state = saved_decor_start;
xfree(buf);
return 0;
return ret;
}
// For spell checking: concatenate the start of the following line "line" into

View File

@ -31,6 +31,8 @@ describe('decorations providers', function()
[12] = {foreground = tonumber('0x990000')};
[13] = {background = Screen.colors.LightBlue};
[14] = {background = Screen.colors.WebGray, foreground = Screen.colors.DarkBlue};
[15] = {special = Screen.colors.Blue1, undercurl = true},
[16] = {special = Screen.colors.Red, undercurl = true},
}
end)
@ -56,7 +58,7 @@ describe('decorations providers', function()
a.nvim_set_decoration_provider(_G.ns1, {
on_start = on_do; on_buf = on_do;
on_win = on_do; on_line = on_do;
on_end = on_do;
on_end = on_do; _on_spell_nav = on_do;
})
return _G.ns1
]])
@ -95,7 +97,7 @@ describe('decorations providers', function()
|
]]}
check_trace {
{ "start", 4, 40 };
{ "start", 4 };
{ "win", 1000, 1, 0, 8 };
{ "line", 1000, 1, 0 };
{ "line", 1000, 1, 1 };
@ -119,7 +121,7 @@ describe('decorations providers', function()
|
]]}
check_trace {
{ "start", 5, 10 };
{ "start", 5 };
{ "buf", 1 };
{ "win", 1000, 1, 0, 8 };
{ "line", 1000, 1, 6 };
@ -156,6 +158,84 @@ describe('decorations providers', function()
]]}
end)
it('can indicate spellchecked points', function()
exec [[
set spell
set spelloptions=noplainbuffer
syntax off
]]
insert [[
I am well written text.
i am not capitalized.
I am a speling mistakke.
]]
setup_provider [[
local ns = a.nvim_create_namespace "spell"
beamtrace = {}
local function on_do(kind, ...)
if kind == 'win' or kind == 'spell' then
a.nvim_buf_set_extmark(0, ns, 0, 0, { end_row = 2, end_col = 23, spell = true, ephemeral = true })
end
table.insert(beamtrace, {kind, ...})
end
]]
check_trace {
{ "start", 5 };
{ "win", 1000, 1, 0, 5 };
{ "line", 1000, 1, 0 };
{ "line", 1000, 1, 1 };
{ "line", 1000, 1, 2 };
{ "line", 1000, 1, 3 };
{ "end", 5 };
}
feed "gg0"
screen:expect{grid=[[
^I am well written text. |
{15:i} am not capitalized. |
I am a {16:speling} {16:mistakke}. |
|
{1:~ }|
{1:~ }|
{1:~ }|
|
]]}
feed "]s"
check_trace {
{ "spell", 1000, 1, 1, 0, 1, -1 };
}
screen:expect{grid=[[
I am well written text. |
{15:^i} am not capitalized. |
I am a {16:speling} {16:mistakke}. |
|
{1:~ }|
{1:~ }|
{1:~ }|
|
]]}
feed "]s"
check_trace {
{ "spell", 1000, 1, 2, 7, 2, -1 };
}
screen:expect{grid=[[
I am well written text. |
{15:i} am not capitalized. |
I am a {16:^speling} {16:mistakke}. |
|
{1:~ }|
{1:~ }|
{1:~ }|
|
]]}
end)
it('can predefine highlights', function()
screen:try_resize(40, 16)
insert(mulholland)