From 85a97a1aab13dc9acb19715e8bc58c6f8c3bcd51 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Tue, 25 Aug 2020 15:55:27 +0200 Subject: introduce new syntax aware spellchecking mechanism Fallback to the old mechanism if no lexer is active. Algorithm: 1. Wrap the active lexer's lex function and save it original value to restore it 2. Obtain the original token stream 3. Filter the tokens for visibility and if we want to spellcheck them (lexers.COMMENT and lexers.STRING by default) 4. Spellcheck the selected token. This is why this is unusably slow we shell out to our spellchecker for each token to check. 5. Split up the selected token into its correct and misspelled parts 6. Insert the correct parts unmodified and the misspelled ones as lexers.ERROR tokens into the token stream Drawbacks: 1. It is unusably slow for now 2. The problem to decide which tokens should be spellchecked still remains. A lexer using different token names than we expect will prevent our spellchecking TODO: 1. Introduce a way to disable syntax aware spellchecking and always use the old check full viewport approach 2. Speedup the syntax aware spellchecking (Maybe by splitting it into two iterations: First collect all toens to spellcheck and shell out once to spellcheck them all Second build up the token stream using the typo information we got) --- spellcheck.lua | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 132 insertions(+), 9 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index b14aec2..5a62b84 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -21,14 +21,19 @@ else end spellcheck.typo_style = "fore:red" -spellcheck.enabled = {} +spellcheck.check_full_viewport = {} + +spellcheck.check_tokens = { + [vis.lexers.STRING] = true, + [vis.lexers.COMMENT] = true +} local ignored = {} local last_viewport, last_typos = nil, "" vis.events.subscribe(vis.events.WIN_HIGHLIGHT, function(win) - if not spellcheck.enabled[win] or not win:style_define(42, spellcheck.typo_style) then + if not spellcheck.check_full_viewport[win] or not win:style_define(42, spellcheck.typo_style) then return false end local viewport = win.viewport @@ -63,26 +68,144 @@ vis.events.subscribe(vis.events.WIN_HIGHLIGHT, function(win) return true end) +local wrapped_lex_funcs = {} + +local wrap_lex_func = function(old_lex_func) + return function(lexer, data, index, redrawtime_max) + -- vis:info("hooked lexer.lex") + local tokens, timedout = old_lex_func(lexer, data, index, redrawtime_max) + + -- quit early if the lexer already took to long + -- TODO: investigate further if timedout is actually set by the lexer. + -- As I understand lpeg.match used by lexer.lex timedout will always be nil + if timeout then + return tokens, timedout + end + + local log = nil + local win = vis.win + local cmd = spellcheck.list_cmd:format(spellcheck.lang) + local new_tokens = {} + + -- get file position we lex + -- duplicated code with vis-std.lua + local viewport = win.viewport + local horizon_max = win.horizon or 32768 + local horizon = viewport.start < horizon_max and viewport.start or horizon_max + local view_start = viewport.start + local lex_start = viewport.start - horizon + local token_end = lex_start + (tokens[#tokens] or 1) - 1 + + for i = 1, #tokens - 1, 2 do + local token_start = lex_start + (tokens[i-1] or 1) - 1 + local token_end = tokens[i+1] + local token_range = {start = token_start, finish = token_end - 1} + + -- check if token is visable + if token_start >= view_start or token_end > view_start then + local token_name = tokens[i] + -- token is not listed for spellchecking just add it to the token stream + if not spellcheck.check_tokens[token_name] then + table.insert(new_tokens, tokens[i]) + table.insert(new_tokens, token_end) + -- spellcheck the token + else + local ret, stdout, stderr = vis:pipe(win.file, token_range, cmd) + if ret ~= 0 then + vis:info("calling cmd: `" .. cmd .. "` failed ("..ret..")") + -- we got misspellings + elseif stdout then + local typo_iter = stdout:gmatch("(.-)\n") + local token_content = win.file:content(token_range) + + -- current position in token_content + local index = 1 + for typo in typo_iter do + if not ignored[typo] then + local start, finish = token_content:find(typo, index, true) + -- split token + local pre_typo_end = start - 1 + -- correct part before typo + if pre_typo_end > index then + table.insert(new_tokens, token_name) + table.insert(new_tokens, token_start + pre_typo_end) + end + -- typo + -- TODO make style configurable + table.insert(new_tokens, vis.lexers.ERROR) + table.insert(new_tokens, token_start + finish + 1) + index = finish + end + end + -- rest which is not already inserted into the token stream + table.insert(new_tokens, token_name) + table.insert(new_tokens, token_end) + -- found no misspellings just add it to the token stream + else + table.insert(new_tokens, token_name) + table.insert(new_tokens, token_end) + end + -- comment with mispellings anf oter stuf + end + end + end + return new_tokens, timedout + end +end + +local enable_spellcheck = function() + -- prevent wrapping the lex function multiple times + if wrapped_lex_funcs[vis.win] then + return + end + + if vis.win.syntax and vis.lexers.load then + local lexer = vis.lexers.load(vis.win.syntax, nil, true) + if lexer and lexer.lex then + local old_lex_func = lexer.lex + wrapped_lex_funcs[vis.win] = old_lex_func + lexer.lex = wrap_lex_func(old_lex_func) + return + end + end + + -- fallback check spellcheck the full viewport + spellcheck.check_full_viewport[vis.win] = true +end + +local is_spellcheck_enabled = function() + return spellcheck.check_full_viewport[vis.win] or wrapped_lex_funcs[vis.win] +end + vis:map(vis.modes.NORMAL, "e", function(keys) - spellcheck.enabled[vis.win] = true - return 0 + enable_spellcheck() end, "Enable spellchecking in the current window") +local disable_spellcheck = function() + local old_lex_func = wrapped_lex_funcs[vis.win] + if old_lex_func then + local lexer = vis.lexers.load(vis.win.syntax, nil, true) + lexer.lex = old_lex_func + wrapped_lex_funcs[vis.win] = nil + else + spellcheck.check_full_viewport[vis.win] = nil + end +end + vis:map(vis.modes.NORMAL, "d", function(keys) - spellcheck.enabled[vis.win] = nil + disable_spellcheck() -- force new highlight vis.win:draw() - return 0 end, "Disable spellchecking in the current window") -- toggle spellchecking on -- is used by some word processors (LibreOffice) for spellchecking -- Thanks to @leorosa for the hint. vis:map(vis.modes.NORMAL, "", function(keys) - if not spellcheck.enabled[vis.win] then - spellcheck.enabled[vis.win] = true + if not is_spellcheck_enabled() then + enable_spellcheck() else - spellcheck.enabled[vis.win] = nil + disable_spellcheck() vis.win:draw() end return 0 -- cgit v1.2.3 From 8093caf55ba0df77fe297f682c2a327fba8ccb28 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 26 Aug 2020 01:27:23 +0200 Subject: extract generic spellchecking functionality from the full viewport function Add get_typos(range) to retrieve a string of misspelled word in a specific file range by calling the spellchecker's list command. It return nil or a string with each misspell followed by a newline. Introduce typo_iter(text, typos, ignored) to iterate over all not ignored typos and their positions in text. It returns an statefull iterator function closure. Which will return the next typo and ist start and finish in the text, starting by 1. --- spellcheck.lua | 53 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 15 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 5a62b84..29718aa 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -28,8 +28,43 @@ spellcheck.check_tokens = { [vis.lexers.COMMENT] = true } + +-- Return nil or a string of misspelled word in a specific file range +-- by calling the spellchecker's list command. +-- The returned string consists of each misspell followed by a newline. +local function get_typos(range) + local cmd = spellcheck.list_cmd:format(spellcheck.lang) + local ret, so, se = vis:pipe(vis.win.file, range, cmd) + if ret ~= 0 then + vis:info("calling " .. cmd .. " failed ("..ret..")") + return nil + end + return so +end + +-- plugin global list of ignored typos local ignored = {} + +-- Return an iterator over all not ignored typos and their positions in text. +-- The returned iterator is a seelf contained statefull iterator function closure. +-- Which will return the next typo and ist start and finish in the text, starting by 1. +local function typo_iter(text, typos, ignored) + local index = 0 + local unfiltered_iterator, iter_state = typos:gmatch("(.-)\n") + return function(foo, bar) + repeat + typo = unfiltered_iterator(iter_state) + until(not typo or not ignored[typo]) + + if typo then + local start, finish = text:find(typo, index, true) + index = finish + return typo, start, finish + end + end +end + local last_viewport, last_typos = nil, "" vis.events.subscribe(vis.events.WIN_HIGHLIGHT, function(win) @@ -44,23 +79,11 @@ vis.events.subscribe(vis.events.WIN_HIGHLIGHT, function(win) if last_viewport == viewport_text then typos = last_typos else - local cmd = spellcheck.list_cmd:format(spellcheck.lang) - local ret, so, se = vis:pipe(win.file, viewport, cmd) - if ret ~= 0 then - vis:message("calling " .. cmd .. " failed ("..se..")") - return false - end - typos = so or "" + typos = get_typos(viewport) or "" end - local corrections_iter = typos:gmatch("(.-)\n") - local index = 1 - for typo in corrections_iter do - if not ignored[typo] then - local start, finish = viewport_text:find(typo, index, true) - win:style(42, viewport.start + start - 1, viewport.start + finish) - index = finish - end + for typo, start, finish in typo_iter(viewport_text, typos, ignored) do + win:style(42, viewport.start + start - 1, viewport.start + finish) end last_viewport = viewport_text -- cgit v1.2.3 From b90ae91b24a214fb603f90855a3247138e8c9422 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 26 Aug 2020 14:28:10 +0200 Subject: separate supress_output into redirects for stdout and stderr --- spellcheck.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 29718aa..6ecaec0 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -3,17 +3,19 @@ local spellcheck = {} spellcheck.lang = os.getenv("LANG"):sub(0,5) or "en_US" -local supress_output = ">/dev/null 2>/dev/null" -if os.execute("type enchant "..supress_output) then +local supress_stdout = " >/dev/null" +local supress_stderr = " 2>/dev/null" +local supress_output = supress_stdout .. supress_stderr +if os.execute("type enchant"..supress_output) then spellcheck.cmd = "enchant -d %s -a" spellcheck.list_cmd = "enchant -l -d %s -a" -elseif os.execute("type enchant-2 "..supress_output) then +elseif os.execute("type enchant-2"..supress_output) then spellcheck.cmd = "enchant-2 -d %s -a" spellcheck.list_cmd = "enchant-2 -l -d %s -a" -elseif os.execute("type aspell "..supress_output) then +elseif os.execute("type aspell"..supress_output) then spellcheck.cmd = "aspell -l %s -a" spellcheck.list_cmd = "aspell list -l %s -a" -elseif os.execute("type hunspell "..supress_output) then +elseif os.execute("type hunspell"..supress_output) then spellcheck.cmd = "hunspell -d %s" spellcheck.list_cmd = "hunspell -l -d %s" else -- cgit v1.2.3 From 6619dd4570be1b00f5611138ce32fda414e69ba7 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 26 Aug 2020 14:30:51 +0200 Subject: extent get_typo() to handle string inputs This allows us to be independent of a vis.window and only use the text passed to lexer.lex for our spellchecking. This is helpfull when the active window is for example the command prompt but the original source file still gets lexed. --- spellcheck.lua | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 6ecaec0..52a2e54 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -31,23 +31,49 @@ spellcheck.check_tokens = { } --- Return nil or a string of misspelled word in a specific file range +-- Return nil or a string of misspelled word in a specific file range or text -- by calling the spellchecker's list command. +-- If given a range we will use vis:pipe to get our typos from the spellchecker. +-- If a string was passed we call the spellchecker ourself and redirect its stdout +-- to a temporary file. See http://lua-users.org/lists/lua-l/2007-10/msg00189.html. -- The returned string consists of each misspell followed by a newline. -local function get_typos(range) +local function get_typos(range_or_text) local cmd = spellcheck.list_cmd:format(spellcheck.lang) - local ret, so, se = vis:pipe(vis.win.file, range, cmd) - if ret ~= 0 then - vis:info("calling " .. cmd .. " failed ("..ret..")") - return nil + local typos = nil + if type(range_or_text) == "string" then + local text = range_or_text + local tmp_name = os.tmpname() + local full_cmd = cmd .. "> " .. tmp_name .. supress_stderr + local proc = assert(io.popen(full_cmd, "w")) + proc:write(text) + -- this error detection may need lua5.2 + local success, reason, exit_code = proc:close() + if not success then + vis:info("calling " .. cmd .. " failed ("..exit_code..")") + return nil + end + + local tmp_file = io.open(tmp_name, "r") + typos = tmp_file:read("*a") + tmp_file:close() + os.remove(tmp_name) + else + local range = range_or_text + local ret, so, se = vis:pipe(vis.win.file, range, cmd) + + if ret ~= 0 then + vis:info("calling " .. cmd .. " failed ("..ret..")") + return nil + end + typos = so end - return so + + return typos end -- plugin global list of ignored typos local ignored = {} - -- Return an iterator over all not ignored typos and their positions in text. -- The returned iterator is a seelf contained statefull iterator function closure. -- Which will return the next typo and ist start and finish in the text, starting by 1. -- cgit v1.2.3 From e0c59627c5adf95a2da57867f82c81970d123a93 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 26 Aug 2020 14:52:21 +0200 Subject: remove unused variable definitions --- spellcheck.lua | 3 --- 1 file changed, 3 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 52a2e54..73b4650 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -123,7 +123,6 @@ local wrapped_lex_funcs = {} local wrap_lex_func = function(old_lex_func) return function(lexer, data, index, redrawtime_max) - -- vis:info("hooked lexer.lex") local tokens, timedout = old_lex_func(lexer, data, index, redrawtime_max) -- quit early if the lexer already took to long @@ -133,9 +132,7 @@ local wrap_lex_func = function(old_lex_func) return tokens, timedout end - local log = nil local win = vis.win - local cmd = spellcheck.list_cmd:format(spellcheck.lang) local new_tokens = {} -- get file position we lex -- cgit v1.2.3 From bf63501ac14207e687b85eba2aa585fc103111f7 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 26 Aug 2020 14:54:49 +0200 Subject: implement a new algorithm which calls the external spellchecker at most once We collect all typos for the current viewport (This is not sound because the viewport we get from vis.win.viewport must not be from the window we currently lex). Then we iterate over both the token stream and the typo stream, adding the token end to the new token stream if it is smaller than the start of the current typo or not a token we spellcheck and advance the token stream. Otherwise we add the token part before our current typo if present and the highlight token representing the typo and advance our typo stream. After we handled all typos we add each leftover token to the new token stream. Typos are cached and reused if the possible viewport and the data we lex are unchanged. (This is sound because, either data is unchanged then we may call the external spellchecker for nothing, or data is the same as last time then the typos haven't changed) --- spellcheck.lua | 113 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 62 insertions(+), 51 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 73b4650..fe6c976 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -122,6 +122,10 @@ end) local wrapped_lex_funcs = {} local wrap_lex_func = function(old_lex_func) + local old_viewport = vis.win.viewport + local old_viewport_text = "" + local old_typos = {} + return function(lexer, data, index, redrawtime_max) local tokens, timedout = old_lex_func(lexer, data, index, redrawtime_max) @@ -135,68 +139,75 @@ local wrap_lex_func = function(old_lex_func) local win = vis.win local new_tokens = {} - -- get file position we lex + -- get possible file position we lex -- duplicated code with vis-std.lua + -- this is totally broken and unsound + -- to be sound we have to spellcheck all data that was passed to us + -- investigate if a stateless approach is much slower local viewport = win.viewport local horizon_max = win.horizon or 32768 local horizon = viewport.start < horizon_max and viewport.start or horizon_max local view_start = viewport.start local lex_start = viewport.start - horizon - local token_end = lex_start + (tokens[#tokens] or 1) - 1 - - for i = 1, #tokens - 1, 2 do - local token_start = lex_start + (tokens[i-1] or 1) - 1 - local token_end = tokens[i+1] - local token_range = {start = token_start, finish = token_end - 1} - - -- check if token is visable - if token_start >= view_start or token_end > view_start then - local token_name = tokens[i] - -- token is not listed for spellchecking just add it to the token stream - if not spellcheck.check_tokens[token_name] then - table.insert(new_tokens, tokens[i]) + + local viewport_text = data:sub(view_start) + + local typos = "" + if old_viewport.start ~= view_start + or old_viewport.finish ~= viewport.finish + or old_viewport_text ~= viewport_text + then + typos = get_typos(viewport_text) + old_typos = typos + old_viewport = viewport + old_viewport_text = viewport_text + else + typos = old_typos + end + + local i = 1 + for typo, start, finish in typo_iter(viewport_text, typos, ignored) do + local typo_start = view_start + start + local typo_end = view_start + finish + repeat + -- no tokens left + if i > #tokens -1 then + break + end + + local token_type = tokens[i] + local token_start = lex_start + (tokens[i-1] or 1) - 1 + local token_end = tokens[i+1] + + -- the current token ends before our typo -> append to new stream + -- or is not spellchecked + if token_end < typo_start or not spellcheck.check_tokens[token_type] then + table.insert(new_tokens, token_type) table.insert(new_tokens, token_end) - -- spellcheck the token + + -- done with this token -> advance token stream + i = i + 2 + -- typo and checked token overlap else - local ret, stdout, stderr = vis:pipe(win.file, token_range, cmd) - if ret ~= 0 then - vis:info("calling cmd: `" .. cmd .. "` failed ("..ret..")") - -- we got misspellings - elseif stdout then - local typo_iter = stdout:gmatch("(.-)\n") - local token_content = win.file:content(token_range) - - -- current position in token_content - local index = 1 - for typo in typo_iter do - if not ignored[typo] then - local start, finish = token_content:find(typo, index, true) - -- split token - local pre_typo_end = start - 1 - -- correct part before typo - if pre_typo_end > index then - table.insert(new_tokens, token_name) - table.insert(new_tokens, token_start + pre_typo_end) - end - -- typo - -- TODO make style configurable - table.insert(new_tokens, vis.lexers.ERROR) - table.insert(new_tokens, token_start + finish + 1) - index = finish - end - end - -- rest which is not already inserted into the token stream - table.insert(new_tokens, token_name) - table.insert(new_tokens, token_end) - -- found no misspellings just add it to the token stream - else - table.insert(new_tokens, token_name) - table.insert(new_tokens, token_end) + local pre_typo_end = typo_start - 1 + -- unchanged token part before typo + if pre_typo_end > token_start then + table.insert(new_tokens, token_type) + table.insert(new_tokens, pre_typo_end + 1) end - -- comment with mispellings anf oter stuf + + -- highlight typo + table.insert(new_tokens, vis.lexers.ERROR) + table.insert(new_tokens, typo_end + 1) end - end + until(not token_end or token_end > typo_end) end + + -- add tokens left after we handled all typos + for i = i, #tokens, 1 do + table.insert(new_tokens, tokens[i]) + end + return new_tokens, timedout end end -- cgit v1.2.3 From aed935dc991874acce1571edd070902f435bd82a Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 26 Aug 2020 16:05:29 +0200 Subject: wrap io.open or io.proc calls in assert --- spellcheck.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index fe6c976..e9e3839 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -53,7 +53,7 @@ local function get_typos(range_or_text) return nil end - local tmp_file = io.open(tmp_name, "r") + local tmp_file = assert(io.open(tmp_name, "r")) typos = tmp_file:read("*a") tmp_file:close() os.remove(tmp_name) @@ -304,7 +304,7 @@ vis:map(vis.modes.NORMAL, "w", function(keys) -- select a correction local cmd = 'printf "' .. suggestions:gsub(", ", "\\n") .. '\\n" | vis-menu' - local f = io.popen(cmd) + local f = assert(io.popen(cmd)) local correction = f:read("*all") f:close() -- trim correction -- cgit v1.2.3 From 6de1905b60282d1215dd2eed029295f13877868f Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 26 Aug 2020 16:06:23 +0200 Subject: fix typos in typo_iter documentation --- spellcheck.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index e9e3839..59c5630 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -75,8 +75,8 @@ end local ignored = {} -- Return an iterator over all not ignored typos and their positions in text. --- The returned iterator is a seelf contained statefull iterator function closure. --- Which will return the next typo and ist start and finish in the text, starting by 1. +-- The returned iterator is a self contained statefull iterator function closure. +-- Which will return the next typo and its start and finish in the text, starting by 1. local function typo_iter(text, typos, ignored) local index = 0 local unfiltered_iterator, iter_state = typos:gmatch("(.-)\n") -- cgit v1.2.3 From 0d224104468c332b3057c92ff297dfb90d8e35e2 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 26 Aug 2020 16:30:49 +0200 Subject: change typo_iter to not find typos in correct spelled words When naivingly searching for the typo in our text we might find it before its actual appearense in a correct word containing the typo (e.g. "broken ok" would find the typo "ok" in the middle of the word "broken"). To prevent this we now only find a typo if its enclosed in non-letter characters, this prevents typos from being found in regular words. Typos starting with '"' for example are still found correctly. --- spellcheck.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 59c5630..fe7d8a3 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -86,9 +86,14 @@ local function typo_iter(text, typos, ignored) until(not typo or not ignored[typo]) if typo then - local start, finish = text:find(typo, index, true) + -- to prevent typos from being found in correct words before them + -- ("stuff stuf", "broken ok", ...) + -- we match typos only when they are enclosed in non-letter characters. + local start, finish = text:find("[%A]" .. typo .. "[%A]", index) index = finish - return typo, start, finish + + -- ignore the first and last non letter character + return typo, start + 1, finish - 1 end end end -- cgit v1.2.3 From 63afcec9a2f1d1588cc824f2c22284e9c4d66ad2 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 26 Aug 2020 16:36:22 +0200 Subject: fix a off-by-one errors If the end of a typo equals the end of a token we didn't advance the typo stream entering an infinite loop. --- spellcheck.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index fe7d8a3..f8c4e8e 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -205,7 +205,7 @@ local wrap_lex_func = function(old_lex_func) table.insert(new_tokens, vis.lexers.ERROR) table.insert(new_tokens, typo_end + 1) end - until(not token_end or token_end > typo_end) + until(not token_end or token_end >= typo_end) end -- add tokens left after we handled all typos -- cgit v1.2.3 From e5bceacb31238b2580321bc5afb7607764820fb4 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 26 Aug 2020 16:42:34 +0200 Subject: remove all the unsound viewport code and cache the full token_stream instead of typos This should make our spellchecking independent of the current window. --- spellcheck.lua | 38 +++++++++----------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index f8c4e8e..aacdd94 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -127,9 +127,8 @@ end) local wrapped_lex_funcs = {} local wrap_lex_func = function(old_lex_func) - local old_viewport = vis.win.viewport - local old_viewport_text = "" - local old_typos = {} + local old_data = "" + local old_new_tokens = "" return function(lexer, data, index, redrawtime_max) local tokens, timedout = old_lex_func(lexer, data, index, redrawtime_max) @@ -141,39 +140,19 @@ local wrap_lex_func = function(old_lex_func) return tokens, timedout end - local win = vis.win local new_tokens = {} - -- get possible file position we lex - -- duplicated code with vis-std.lua - -- this is totally broken and unsound - -- to be sound we have to spellcheck all data that was passed to us - -- investigate if a stateless approach is much slower - local viewport = win.viewport - local horizon_max = win.horizon or 32768 - local horizon = viewport.start < horizon_max and viewport.start or horizon_max - local view_start = viewport.start - local lex_start = viewport.start - horizon - - local viewport_text = data:sub(view_start) - local typos = "" - if old_viewport.start ~= view_start - or old_viewport.finish ~= viewport.finish - or old_viewport_text ~= viewport_text + if old_data ~= data then - typos = get_typos(viewport_text) - old_typos = typos - old_viewport = viewport - old_viewport_text = viewport_text + typos = get_typos(data) + old_data = data else - typos = old_typos + return old_new_tokens end local i = 1 - for typo, start, finish in typo_iter(viewport_text, typos, ignored) do - local typo_start = view_start + start - local typo_end = view_start + finish + for typo, typo_start, typo_end in typo_iter(data, typos, ignored) do repeat -- no tokens left if i > #tokens -1 then @@ -181,7 +160,7 @@ local wrap_lex_func = function(old_lex_func) end local token_type = tokens[i] - local token_start = lex_start + (tokens[i-1] or 1) - 1 + local token_start = (tokens[i-1] or 1) - 1 local token_end = tokens[i+1] -- the current token ends before our typo -> append to new stream @@ -213,6 +192,7 @@ local wrap_lex_func = function(old_lex_func) table.insert(new_tokens, tokens[i]) end + old_new_tokens = new_tokens return new_tokens, timedout end end -- cgit v1.2.3 From ebdd1360935b8c27eae6929042b0386af3e2e43b Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 26 Aug 2020 19:11:14 +0200 Subject: add the option 'disable_syntax_awareness' to always check the full viewport --- spellcheck.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index aacdd94..782fe8f 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -24,6 +24,7 @@ end spellcheck.typo_style = "fore:red" spellcheck.check_full_viewport = {} +spellcheck.disable_syntax_awareness = false spellcheck.check_tokens = { [vis.lexers.STRING] = true, @@ -203,7 +204,7 @@ local enable_spellcheck = function() return end - if vis.win.syntax and vis.lexers.load then + if not spellcheck.disable_syntax_awareness and vis.win.syntax and vis.lexers.load then local lexer = vis.lexers.load(vis.win.syntax, nil, true) if lexer and lexer.lex then local old_lex_func = lexer.lex -- cgit v1.2.3 From c0eef2f85a44e4bbaac1af27f428dfd22ebd06ab Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 27 Aug 2020 14:10:09 +0200 Subject: fix highlighting of typos at the end of the text Our pattern [%A][%A] does not match typos at the end or start of the text because there is no leading / trailing non letter character. So if we dind't find the typo with our normal pattern it must be either the start or the end of the text. If not raise a warning that this must be a bug. --- spellcheck.lua | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 782fe8f..89d2bd7 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -91,6 +91,13 @@ local function typo_iter(text, typos, ignored) -- ("stuff stuf", "broken ok", ...) -- we match typos only when they are enclosed in non-letter characters. local start, finish = text:find("[%A]" .. typo .. "[%A]", index) + -- the typo was not found by our pattern this means it must be the last word in the text + if not start then + start, finish = text:find("[%A]" .. typo .. "$", index) + if not start then + vis:info(string.format("typo %s not found after %d this must be a bug please report", typo, index)) + end + end index = finish -- ignore the first and last non letter character -- cgit v1.2.3 From 819f67b78f2bc64f39fc0a48613908c36c674621 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 27 Aug 2020 15:42:01 +0200 Subject: special case typos at the beginning of text as well --- spellcheck.lua | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 89d2bd7..0627d4d 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -79,7 +79,7 @@ local ignored = {} -- The returned iterator is a self contained statefull iterator function closure. -- Which will return the next typo and its start and finish in the text, starting by 1. local function typo_iter(text, typos, ignored) - local index = 0 + local index = 1 local unfiltered_iterator, iter_state = typos:gmatch("(.-)\n") return function(foo, bar) repeat @@ -91,17 +91,30 @@ local function typo_iter(text, typos, ignored) -- ("stuff stuf", "broken ok", ...) -- we match typos only when they are enclosed in non-letter characters. local start, finish = text:find("[%A]" .. typo .. "[%A]", index) - -- the typo was not found by our pattern this means it must be the last word in the text + -- typo was not found by our pattern this means it must be either + -- the first or last word in the text if not start then - start, finish = text:find("[%A]" .. typo .. "$", index) - if not start then - vis:info(string.format("typo %s not found after %d this must be a bug please report", typo, index)) + -- check start of text + if index == 1 then + start = index + finish = #typo + -- must be the end of text + else + start = #text - #typo + 1 + finish = start + #typo - 1 + end + + if text:sub(start, finish) ~= typo then + vis:info(string.format("typo %s not where expected after %d at %d %d found: %s", + typo, index, start, finish, text:sub(start, finish))) end + else + start = start + 1 -- ignore leading non letter char + finish = finish - 1 -- ignore trailing non letter char end index = finish - -- ignore the first and last non letter character - return typo, start + 1, finish - 1 + return typo, start, finish end end end -- cgit v1.2.3 From 04ab455907fe7da054aeefe05b0be0b534affccd Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 27 Aug 2020 15:43:34 +0200 Subject: don't loop infinitely if a typo spans multiple tokens This happens because the "default" token gets emitted for each not matched char resulting in lots of small "default" tokens. This markdown: > typu will result in the token stream {"default", 2, "default", 3, "default", 4, "default", 5} and a typo stream with a single entry spanning all of the tokens {{"typu", 1, 5}}. Not checking if the typo is longer then the current token leads to an infinite loop where the typo is always inserted in the new token stream without advancing the token stream. --- spellcheck.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 0627d4d..ebfe660 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -203,7 +203,13 @@ local wrap_lex_func = function(old_lex_func) -- highlight typo table.insert(new_tokens, vis.lexers.ERROR) - table.insert(new_tokens, typo_end + 1) + -- the typo spans multiple tokens + if token_end < typo_end then + table.insert(new_tokens, token_end + 1) + i = i + 2 + else + table.insert(new_tokens, typo_end + 1) + end end until(not token_end or token_end >= typo_end) end -- cgit v1.2.3 From e23acde38af03a1610dacfb236dc7a2b303faac0 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 27 Aug 2020 15:53:43 +0200 Subject: add vis.lexers.DEFAULT to the tokens we spellcheck This activates spellchecking for most "text focused" languages like latex or markdown. --- spellcheck.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index ebfe660..3d53d56 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -28,10 +28,10 @@ spellcheck.disable_syntax_awareness = false spellcheck.check_tokens = { [vis.lexers.STRING] = true, - [vis.lexers.COMMENT] = true + [vis.lexers.COMMENT] = true, + [vis.lexers.DEFAULT] = true, } - -- Return nil or a string of misspelled word in a specific file range or text -- by calling the spellchecker's list command. -- If given a range we will use vis:pipe to get our typos from the spellchecker. -- cgit v1.2.3 From 96bf9b1b50c641671390ef4e9d84206c69137150 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Fri, 28 Aug 2020 13:01:41 +0200 Subject: force a redraw for syntax aware highlighting when spelllang changes --- spellcheck.lua | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 3d53d56..aa0f662 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -119,7 +119,7 @@ local function typo_iter(text, typos, ignored) end end -local last_viewport, last_typos = nil, "" +local last_viewport, last_data, last_typos = nil, "", "" vis.events.subscribe(vis.events.WIN_HIGHLIGHT, function(win) if not spellcheck.check_full_viewport[win] or not win:style_define(42, spellcheck.typo_style) then @@ -148,8 +148,7 @@ end) local wrapped_lex_funcs = {} local wrap_lex_func = function(old_lex_func) - local old_data = "" - local old_new_tokens = "" + local old_new_tokens = {} return function(lexer, data, index, redrawtime_max) local tokens, timedout = old_lex_func(lexer, data, index, redrawtime_max) @@ -164,10 +163,10 @@ local wrap_lex_func = function(old_lex_func) local new_tokens = {} local typos = "" - if old_data ~= data + if last_data ~= data then typos = get_typos(data) - old_data = data + last_data = data else return old_new_tokens end @@ -351,8 +350,10 @@ end, "Ignore misspelled word") vis:option_register("spelllang", "string", function(value, toggle) spellcheck.lang = value vis:info("Spellchecking language is now "..value) - -- force new highlight + -- force new highlight for full viewport last_viewport = nil + -- force new highlight for syntax aware + last_data = nil return true end, "The language used for spellchecking") -- cgit v1.2.3 From bf3badc73dc5d43df3aab9070cf9faef37b96326 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Fri, 28 Aug 2020 13:11:26 +0200 Subject: Don't fail if the first and only typo is at the end of the text. Before this change we assumed that when we are at the beginning of the text (index == 1) and don't find the typo is must be the beginning of the text but it can also be the end of the text. We now also check the end if the beginning was not what we were lookig for. --- spellcheck.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index aa0f662..5b374c2 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -95,19 +95,19 @@ local function typo_iter(text, typos, ignored) -- the first or last word in the text if not start then -- check start of text - if index == 1 then - start = index - finish = #typo - -- must be the end of text - else + start = 1 + finish = #typo + -- typo is not the beginning must be the end of text + if text:sub(start, finish) ~= typo then start = #text - #typo + 1 finish = start + #typo - 1 end if text:sub(start, finish) ~= typo then - vis:info(string.format("typo %s not where expected after %d at %d %d found: %s", - typo, index, start, finish, text:sub(start, finish))) + vis:info(string.format("can't find typo %s after %d. Please report this bug.", + typo, index)) end + -- our pettern [%A]typo[%A] found it else start = start + 1 -- ignore leading non letter char finish = finish - 1 -- ignore trailing non letter char -- cgit v1.2.3 From 85bfeb5b9f5dc8b9f0cd504282c4bd56afc14e99 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Fri, 28 Aug 2020 13:16:06 +0200 Subject: return early if we can't get the typos from our external spellchecker --- spellcheck.lua | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 5b374c2..252fbf1 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -134,6 +134,9 @@ vis.events.subscribe(vis.events.WIN_HIGHLIGHT, function(win) typos = last_typos else typos = get_typos(viewport) or "" + if not typos then + return false + end end for typo, start, finish in typo_iter(viewport_text, typos, ignored) do @@ -166,6 +169,9 @@ local wrap_lex_func = function(old_lex_func) if last_data ~= data then typos = get_typos(data) + if not typos then + return tokens, timedout + end last_data = data else return old_new_tokens -- cgit v1.2.3 From a8212fda377d6527c7b76547aa7fb816c5cc62cb Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Fri, 28 Aug 2020 14:31:40 +0200 Subject: reset last_data when enabling spellcheck to enforce new highlighting --- spellcheck.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 252fbf1..3d2888b 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -241,6 +241,8 @@ local enable_spellcheck = function() local old_lex_func = lexer.lex wrapped_lex_funcs[vis.win] = old_lex_func lexer.lex = wrap_lex_func(old_lex_func) + -- reset last data to enforce new highlighting + last_data = "" return end end -- cgit v1.2.3 From 500a12c36f8112a7b66a5e10f0733b3374e3c1f0 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Fri, 28 Aug 2020 15:21:51 +0200 Subject: ignore empty typos apparently in the typo list produced by aspell there is a blank line which messes with our search for typos. --- spellcheck.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 3d2888b..de04cea 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -86,7 +86,7 @@ local function typo_iter(text, typos, ignored) typo = unfiltered_iterator(iter_state) until(not typo or not ignored[typo]) - if typo then + if typo and typo ~= "" then -- to prevent typos from being found in correct words before them -- ("stuff stuf", "broken ok", ...) -- we match typos only when they are enclosed in non-letter characters. -- cgit v1.2.3 From 8af2f0f18350078ae8d70a62c95e161b2d43a0af Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Fri, 28 Aug 2020 16:42:52 +0200 Subject: don't stop iterating over typos when an empty one is encountered An iterator in lua stops if it returns nil. This change will get a new typo from the unfiltered_iterator if we encounter an empty one instead of returning nil. --- spellcheck.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index de04cea..6a20172 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -84,9 +84,9 @@ local function typo_iter(text, typos, ignored) return function(foo, bar) repeat typo = unfiltered_iterator(iter_state) - until(not typo or not ignored[typo]) + until(not typo or (typo ~= "" and not ignored[typo])) - if typo and typo ~= "" then + if typo then -- to prevent typos from being found in correct words before them -- ("stuff stuf", "broken ok", ...) -- we match typos only when they are enclosed in non-letter characters. -- cgit v1.2.3 From 4b08e0a08ca2b3d776186e71a743eb4a7fd2652c Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Fri, 28 Aug 2020 16:45:06 +0200 Subject: escape magic characters in typos If the typo itself contains magic pattern characters we can't reliable find it in the text. --- spellcheck.lua | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) (limited to 'spellcheck.lua') diff --git a/spellcheck.lua b/spellcheck.lua index 6a20172..31b3387 100644 --- a/spellcheck.lua +++ b/spellcheck.lua @@ -81,6 +81,31 @@ local ignored = {} local function typo_iter(text, typos, ignored) local index = 1 local unfiltered_iterator, iter_state = typos:gmatch("(.-)\n") + +-- see https://stackoverflow.com/questions/6705872/how-to-escape-a-variable-in-lua + local escape_lua_pattern + do + local matches = + { + ["^"] = "%^"; + ["$"] = "%$"; + ["("] = "%("; + [")"] = "%)"; + ["%"] = "%%"; + ["."] = "%."; + ["["] = "%["; + ["]"] = "%]"; + ["*"] = "%*"; + ["+"] = "%+"; + ["-"] = "%-"; + ["?"] = "%?"; + } + + escape_lua_pattern = function(s) + return (s:gsub(".", matches)) + end + end + return function(foo, bar) repeat typo = unfiltered_iterator(iter_state) @@ -90,7 +115,7 @@ local function typo_iter(text, typos, ignored) -- to prevent typos from being found in correct words before them -- ("stuff stuf", "broken ok", ...) -- we match typos only when they are enclosed in non-letter characters. - local start, finish = text:find("[%A]" .. typo .. "[%A]", index) + local start, finish = text:find("[%A]" .. escape_lua_pattern(typo) .. "[%A]", index) -- typo was not found by our pattern this means it must be either -- the first or last word in the text if not start then -- cgit v1.2.3