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|. When a character is supplied it is used as |:syn-cchar|.
"hl_group" is used as highlight for the cchar if provided, "hl_group" is used as highlight for the cchar if provided,
otherwise it defaults to |hl-Conceal|. 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 • ui_watched: boolean that indicates the mark should be
drawn by a UI. When set, the UI will receive win_extmark drawn by a UI. When set, the UI will receive win_extmark
events. Note: the mark is positioned by virt_text 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. dict that maps from names to namespace ids.
*nvim_set_decoration_provider()* *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 Set or change decoration provider for a namespace
This is a very general purpose interface for having lua callbacks being This is a very general purpose interface for having lua callbacks being
@@ -2709,7 +2711,7 @@ nvim_set_decoration_provider({ns_id}, {opts})
Parameters: ~ Parameters: ~
{ns_id} Namespace id from |nvim_create_namespace()| {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", • on_start: called first on each screen redraw ["start",
tick] tick]
• on_buf: called for each buffer being redrawn (before window • on_buf: called for each buffer being redrawn (before window

View File

@@ -5875,6 +5875,10 @@ A jump table for the options with a short description can be found at |Q_op|.
separate word: every upper-case character in a word separate word: every upper-case character in a word
that comes after a lower case character indicates the that comes after a lower case character indicates the
start of a new word. 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'*
'spellsuggest' 'sps' string (default "best") 'spellsuggest' 'sps' string (default "best")

View File

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

View File

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

View File

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

View File

@@ -162,9 +162,11 @@
;; Literals ;; Literals
(string_literal) @string (string_literal) @string
(string_literal) @spell
(integer_literal) @number (integer_literal) @number
(float_literal) @float (float_literal) @float
(comment) @comment (comment) @comment
(comment) @spell
(pattern) @string.special (pattern) @string.special
(pattern_multi) @string.regex (pattern_multi) @string.regex
(filename) @string (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|. /// When a character is supplied it is used as |:syn-cchar|.
/// "hl_group" is used as highlight for the cchar if provided, /// "hl_group" is used as highlight for the cchar if provided,
/// otherwise it defaults to |hl-Conceal|. /// 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 /// - ui_watched: boolean that indicates the mark should be drawn
/// by a UI. When set, the UI will receive win_extmark events. /// by a UI. When set, the UI will receive win_extmark events.
/// Note: the mark is positioned by virt_text attributes. Can be /// 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; bool ephemeral = false;
OPTION_TO_BOOL(ephemeral, 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); OPTION_TO_BOOL(decor.ui_watched, ui_watched, false);
if (decor.ui_watched) { if (decor.ui_watched) {
has_decor = true; has_decor = true;
@@ -972,20 +979,21 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start,
/// for the moment. /// for the moment.
/// ///
/// @param ns_id Namespace id from |nvim_create_namespace()| /// @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 /// - on_start: called first on each screen redraw
/// ["start", tick] /// ["start", tick]
/// - on_buf: called for each buffer being redrawn (before window /// - on_buf: called for each buffer being redrawn (before
/// callbacks) /// window callbacks)
/// ["buf", bufnr, tick] /// ["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] /// ["win", winid, bufnr, topline, botline_guess]
/// - on_line: called for each buffer line being redrawn. (The /// - on_line: called for each buffer line being redrawn.
/// interaction with fold lines is subject to change) /// (The interaction with fold lines is subject to change)
/// ["win", winid, bufnr, row] /// ["win", winid, bufnr, row]
/// - on_end: called at the end of a redraw cycle /// - on_end: called at the end of a redraw cycle
/// ["end", tick] /// ["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 FUNC_API_SINCE(7) FUNC_API_LUA_ONLY
{ {
DecorProvider *p = get_decor_provider((NS)ns_id, true); 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 { struct {
const char *name; const char *name;
Object *source;
LuaRef *dest; LuaRef *dest;
} cbs[] = { } cbs[] = {
{ "on_start", &p->redraw_start }, { "on_start", &opts->on_start, &p->redraw_start },
{ "on_buf", &p->redraw_buf }, { "on_buf", &opts->on_buf, &p->redraw_buf },
{ "on_win", &p->redraw_win }, { "on_win", &opts->on_win, &p->redraw_win },
{ "on_line", &p->redraw_line }, { "on_line", &opts->on_line, &p->redraw_line },
{ "on_end", &p->redraw_end }, { "on_end", &opts->on_end, &p->redraw_end },
{ "_on_hl_def", &p->hl_def }, { "_on_hl_def", &opts->_on_hl_def, &p->hl_def },
{ NULL, NULL }, { "_on_spell_nav", &opts->_on_spell_nav, &p->spell_nav },
{ NULL, NULL, NULL },
}; };
for (size_t i = 0; i < opts.size; i++) { for (size_t i = 0; cbs[i].source && cbs[i].dest && cbs[i].name; i++) {
String k = opts.items[i].key; Object *v = cbs[i].source;
Object *v = &opts.items[i].value; if (v->type == kObjectTypeNil) {
size_t j; continue;
for (j = 0; cbs[j].name && cbs[j].dest; j++) { }
if (strequal(cbs[j].name, k.data)) {
if (v->type != kObjectTypeLuaRef) { if (v->type != kObjectTypeLuaRef) {
api_set_error(err, kErrorTypeValidation, api_set_error(err, kErrorTypeValidation,
"%s is not a function", cbs[j].name); "%s is not a function", cbs[i].name);
goto error; goto error;
} }
*(cbs[j].dest) = v->data.luaref; *(cbs[i].dest) = v->data.luaref;
v->data.luaref = LUA_NOREF; v->data.luaref = LUA_NOREF;
break;
}
}
if (!cbs[j].name) {
api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data);
goto error;
}
} }
p->active = true; p->active = true;

View File

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

View File

@@ -462,6 +462,9 @@ typedef struct {
char *b_p_spf; // 'spellfile' char *b_p_spf; // 'spellfile'
char *b_p_spl; // 'spelllang' char *b_p_spl; // 'spelllang'
char *b_p_spo; // 'spelloptions' 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 int b_cjk; // all CJK letters as OK
uint8_t b_syn_chartab[32]; // syntax iskeyword option uint8_t b_syn_chartab[32]; // syntax iskeyword option
char *b_syn_isk; // 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) void decor_redraw(buf_T *buf, int row1, int row2, Decoration *decor)
{ {
if (row2 >= row1) { 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); 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) void clear_virttext(VirtText *text)
{ {
for (size_t i = 0; i < kv_size(*text); i++) { for (size_t i = 0; i < kv_size(*text); i++) {
@@ -306,6 +311,7 @@ next_mark:
bool conceal = 0; bool conceal = 0;
int conceal_char = 0; int conceal_char = 0;
int conceal_attr = 0; int conceal_attr = 0;
bool spell = false;
for (size_t i = 0; i < kv_size(state->active); i++) { for (size_t i = 0; i < kv_size(state->active); i++) {
DecorRange item = kv_A(state->active, i); DecorRange item = kv_A(state->active, i);
@@ -339,6 +345,9 @@ next_mark:
conceal_attr = item.attr_id; conceal_attr = item.attr_id;
} }
} }
if (active && item.decor.spell) {
spell = true;
}
if ((item.start_row == state->row && item.start_col <= col) if ((item.start_row == state->row && item.start_col <= col)
&& decor_virt_pos(item.decor) && decor_virt_pos(item.decor)
&& item.decor.virt_text_pos == kVTOverlay && item.win_col == -1) { && item.decor.virt_text_pos == kVTOverlay && item.win_col == -1) {
@@ -355,6 +364,7 @@ next_mark:
state->conceal = conceal; state->conceal = conceal;
state->conceal_char = conceal_char; state->conceal_char = conceal_char;
state->conceal_attr = conceal_attr; state->conceal_attr = conceal_attr;
state->spell = spell;
return attr; return attr;
} }

View File

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

View File

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

View File

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

View File

@@ -11,9 +11,11 @@
#include "nvim/arabic.h" #include "nvim/arabic.h"
#include "nvim/buffer.h" #include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/charset.h" #include "nvim/charset.h"
#include "nvim/cursor.h" #include "nvim/cursor.h"
#include "nvim/cursor_shape.h" #include "nvim/cursor_shape.h"
#include "nvim/decoration.h"
#include "nvim/diff.h" #include "nvim/diff.h"
#include "nvim/drawline.h" #include "nvim/drawline.h"
#include "nvim/fold.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); 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) { if (*provider_err) {
provider_err_virt_text(lnum, *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++; ptr++;
if (extra_check) { 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 // Get syntax attribute, unless still at the start of the line
// (double-wide char that doesn't fit). // (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; 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). // Check spelling (unless at the end of the line).
// Only do this when there is no syntax highlighting, the // Only do this when there is no syntax highlighting, the
// @Spell cluster is not used or the current syntax item // @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) { if (has_spell && v >= word_end && v > cur_checked_col) {
spell_attr = 0; spell_attr = 0;
if (!attr_pri) { 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 *prev_ptr;
char_u *p; char_u *p;
int len; 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); 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. // Found last space before word: check for line break.
if (wp->w_p_lbr && c0 == c && vim_isbreak(c) if (wp->w_p_lbr && c0 == c && vim_isbreak(c)
&& !vim_isbreak((int)(*ptr))) { && !vim_isbreak((int)(*ptr))) {

View File

@@ -539,7 +539,7 @@ int update_screen(int type)
ui_comp_set_screen_valid(true); ui_comp_set_screen_valid(true);
DecorProviders providers; DecorProviders providers;
decor_providers_start(&providers, type, &provider_err); decor_providers_start(&providers, &provider_err);
// "start" callback could have changed highlights for global elements // "start" callback could have changed highlights for global elements
if (win_check_ns_hl(NULL)) { 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) || kv_size(decor->virt_lines)
|| decor->conceal || decor->conceal
|| decor_has_sign(decor) || decor_has_sign(decor)
|| decor->ui_watched) { || decor->ui_watched
|| decor->spell) {
decor_full = true; decor_full = true;
decor = xmemdup(decor, sizeof *decor); decor = xmemdup(decor, sizeof *decor);
} }

View File

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

View File

@@ -2355,6 +2355,7 @@ return {
secure=true, secure=true,
expand=true, expand=true,
varname='p_spo', varname='p_spo',
redraw={'current_buffer'},
defaults={if_true=""} 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", "auto:6", "auto:7", "auto:8", "auto:9", "0", "1", "2", "3", "4",
"5", "6", "7", "8", "9", NULL }; "5", "6", "7", "8", "9", NULL };
static char *(p_cb_values[]) = { "unnamed", "unnamedplus", 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_icm_values[]) = { "nosplit", "split", NULL };
static char *(p_jop_values[]) = { "stack", "view", NULL }; static char *(p_jop_values[]) = { "stack", "view", NULL };
static char *(p_tpf_values[]) = { "BS", "HT", "FF", "ESC", "DEL", "C0", "C1", 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. // When 'spellcapcheck' is set compile the regexp program.
errmsg = compile_cap_prog(curwin->w_s); errmsg = compile_cap_prog(curwin->w_s);
} else if (varp == &(curwin->w_s->b_p_spo)) { // 'spelloptions' } 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; errmsg = e_invarg;
} }
} else if (varp == &p_sps) { // 'spellsuggest' } else if (varp == &p_sps) { // 'spellsuggest'

View File

@@ -71,6 +71,7 @@
#include "nvim/change.h" // for changed_bytes #include "nvim/change.h" // for changed_bytes
#include "nvim/charset.h" // for skipwhite, getwhitecols, skipbin #include "nvim/charset.h" // for skipwhite, getwhitecols, skipbin
#include "nvim/cursor.h" // for get_cursor_line_ptr #include "nvim/cursor.h" // for get_cursor_line_ptr
#include "nvim/decoration.h"
#include "nvim/drawscreen.h" // for NOT_VALID, redraw_later #include "nvim/drawscreen.h" // for NOT_VALID, redraw_later
#include "nvim/eval/typval.h" // for semsg #include "nvim/eval/typval.h" // for semsg
#include "nvim/ex_cmds.h" // for do_sub_msg #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 nrlen = 0; // found a number first
size_t wrongcaplen = 0; size_t wrongcaplen = 0;
bool count_word = docount; 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; bool camel_case = false;
// A word never starts at a space or a control character. Return quickly // 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; 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. /// Moves to the next spell error.
/// "curline" is false for "[s", "]s", "[S" and "]S". /// "curline" is false for "[s", "]s", "[S" and "]S".
/// "curline" is true to find word under/after cursor in the same line. /// "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; hlf_T attr = HLF_COUNT;
size_t len; size_t len;
int has_syntax = syntax_present(wp); int has_syntax = syntax_present(wp);
int col; colnr_T col;
char_u *buf = NULL; char_u *buf = NULL;
size_t buflen = 0; size_t buflen = 0;
int skip = 0; int skip = 0;
int capcol = -1; colnr_T capcol = -1;
bool found_one = false; bool found_one = false;
bool wrapped = 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; return 0;
} }
size_t ret = 0;
// Start looking for bad word at the start of the line, because we can't // 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. // 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; linenr_T lnum = wp->w_cursor.lnum;
clearpos(&found_pos); 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) { while (!got_int) {
char_u *line = ml_get_buf(wp->w_buffer, lnum, false); 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. // For checking first word with a capital skip white space.
if (capcol == 0) { if (capcol == 0) {
capcol = (int)getwhitecols((char *)line); capcol = (colnr_T)getwhitecols((char *)line);
} else if (curline && wp == curwin) { } else if (curline && wp == curwin) {
// For spellbadword(): check if first word needs a capital. // 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)) { if (check_need_cap(lnum, col)) {
capcol = 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 || ((colnr_T)(curline
? p - buf + (ptrdiff_t)len ? p - buf + (ptrdiff_t)len
: p - buf) > wp->w_cursor.col)) { : p - buf) > wp->w_cursor.col)) {
bool can_spell; col = (colnr_T)(p - buf);
if (has_syntax) {
col = (int)(p - buf); bool can_spell = (wp->w_s->b_p_spo_flags & SPO_NPBUFFER) == 0;
(void)syn_get_id(wp, lnum, (colnr_T)col,
false, &can_spell, false); 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) { if (!can_spell) {
attr = HLF_COUNT; attr = HLF_COUNT;
} }
} else {
can_spell = true;
}
if (can_spell) { if (can_spell) {
found_one = true; found_one = true;
found_pos = (pos_T) { found_pos = (pos_T) {
.lnum = lnum, .lnum = lnum,
.col = (int)(p - buf), .col = col,
.coladd = 0 .coladd = 0
}; };
if (dir == FORWARD) { if (dir == FORWARD) {
// No need to search further. // No need to search further.
wp->w_cursor = found_pos; wp->w_cursor = found_pos;
xfree(buf);
if (attrp != NULL) { if (attrp != NULL) {
*attrp = attr; *attrp = attr;
} }
return len; ret = len;
goto theend;
} else if (curline) { } else if (curline) {
// Insert mode completion: put cursor after // Insert mode completion: put cursor after
// the bad word. // 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) { if (dir == BACKWARD && found_pos.lnum != 0) {
// Use the last match in the line (before the cursor). // Use the last match in the line (before the cursor).
wp->w_cursor = found_pos; wp->w_cursor = found_pos;
xfree(buf); ret = found_len;
return found_len; goto theend;
} }
if (curline) { 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(); line_breakcheck();
} }
theend:
decor_state_free(&decor_state);
xfree(decor_error);
decor_state = saved_decor_start;
xfree(buf); xfree(buf);
return 0; return ret;
} }
// For spell checking: concatenate the start of the following line "line" into // 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')}; [12] = {foreground = tonumber('0x990000')};
[13] = {background = Screen.colors.LightBlue}; [13] = {background = Screen.colors.LightBlue};
[14] = {background = Screen.colors.WebGray, foreground = Screen.colors.DarkBlue}; [14] = {background = Screen.colors.WebGray, foreground = Screen.colors.DarkBlue};
[15] = {special = Screen.colors.Blue1, undercurl = true},
[16] = {special = Screen.colors.Red, undercurl = true},
} }
end) end)
@@ -56,7 +58,7 @@ describe('decorations providers', function()
a.nvim_set_decoration_provider(_G.ns1, { a.nvim_set_decoration_provider(_G.ns1, {
on_start = on_do; on_buf = on_do; on_start = on_do; on_buf = on_do;
on_win = on_do; on_line = 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 return _G.ns1
]]) ]])
@@ -95,7 +97,7 @@ describe('decorations providers', function()
| |
]]} ]]}
check_trace { check_trace {
{ "start", 4, 40 }; { "start", 4 };
{ "win", 1000, 1, 0, 8 }; { "win", 1000, 1, 0, 8 };
{ "line", 1000, 1, 0 }; { "line", 1000, 1, 0 };
{ "line", 1000, 1, 1 }; { "line", 1000, 1, 1 };
@@ -119,7 +121,7 @@ describe('decorations providers', function()
| |
]]} ]]}
check_trace { check_trace {
{ "start", 5, 10 }; { "start", 5 };
{ "buf", 1 }; { "buf", 1 };
{ "win", 1000, 1, 0, 8 }; { "win", 1000, 1, 0, 8 };
{ "line", 1000, 1, 6 }; { "line", 1000, 1, 6 };
@@ -156,6 +158,84 @@ describe('decorations providers', function()
]]} ]]}
end) 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() it('can predefine highlights', function()
screen:try_resize(40, 16) screen:try_resize(40, 16)
insert(mulholland) insert(mulholland)