From bbaccb05bcf5e6c132cc67313176c809d73aed6d Mon Sep 17 00:00:00 2001 From: actionless Date: Fri, 11 Jun 2021 00:36:37 +0200 Subject: [PATCH 1/2] fix(prompt): handle multibyte character in Backspace, ^h, ^b and ^f --- lib/awful/prompt.lua | 62 +++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/lib/awful/prompt.lua b/lib/awful/prompt.lua index 42719913..280a83a8 100644 --- a/lib/awful/prompt.lua +++ b/lib/awful/prompt.lua @@ -114,7 +114,6 @@ local io = io local table = table local math = math local ipairs = ipairs -local pcall = pcall local capi = { selection = selection @@ -258,6 +257,11 @@ local function history_add(id, command) end +local function have_multibyte_char_at(text, position) + return text:sub(position, position):wlen() == -1 +end + + --- Draw the prompt text with a cursor. -- @tparam table args The table of arguments. -- @field text The text. @@ -285,10 +289,14 @@ local function prompt_text_with_cursor(args) text_start = gstring.xml_escape(text) text_end = "" else - char = gstring.xml_escape(text:sub(args.cursor_pos, args.cursor_pos)) + local offset = 0 + if have_multibyte_char_at(text, args.cursor_pos) then + offset = 1 + end + char = gstring.xml_escape(text:sub(args.cursor_pos, args.cursor_pos + offset)) spacer = " " text_start = gstring.xml_escape(text:sub(1, args.cursor_pos - 1)) - text_end = gstring.xml_escape(text:sub(args.cursor_pos + 1)) + text_end = gstring.xml_escape(text:sub(args.cursor_pos + 1 + offset)) end local cursor_color = gcolor.ensure_pango_color(args.cursor_color) @@ -544,9 +552,9 @@ function prompt.run(args, textbox, exe_callback, completion_callback, local function update() textbox:set_font(font) textbox:set_markup(prompt_text_with_cursor{ - text = command, text_color = inv_col, cursor_color = cur_col, - cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall, - prompt = prettyprompt, highlighter = highlighter }) + text = command, text_color = inv_col, cursor_color = cur_col, + cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall, + prompt = prettyprompt, highlighter = highlighter }) end grabber = keygrabber.run( @@ -663,6 +671,9 @@ function prompt.run(args, textbox, exe_callback, completion_callback, elseif key == "b" then if cur_pos > 1 then cur_pos = cur_pos - 1 + if have_multibyte_char_at(command, cur_pos) then + cur_pos = cur_pos - 1 + end end elseif key == "d" then if cur_pos <= #command then @@ -711,12 +722,20 @@ function prompt.run(args, textbox, exe_callback, completion_callback, end elseif key == "f" then if cur_pos <= #command then - cur_pos = cur_pos + 1 + if have_multibyte_char_at(command, cur_pos) then + cur_pos = cur_pos + 2 + else + cur_pos = cur_pos + 1 + end end elseif key == "h" then if cur_pos > 1 then - command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos) - cur_pos = cur_pos - 1 + local offset = 0 + if have_multibyte_char_at(command, cur_pos - 1) then + offset = 1 + end + command = command:sub(1, cur_pos - 2 - offset) .. command:sub(cur_pos) + cur_pos = cur_pos - 1 - offset end elseif key == "k" then command = command:sub(1, cur_pos - 1) @@ -844,8 +863,12 @@ function prompt.run(args, textbox, exe_callback, completion_callback, cur_pos = #command + 1 elseif key == "BackSpace" then if cur_pos > 1 then - command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos) - cur_pos = cur_pos - 1 + local offset = 0 + if have_multibyte_char_at(command, cur_pos - 1) then + offset = 1 + end + command = command:sub(1, cur_pos - 2 - offset) .. command:sub(cur_pos) + cur_pos = cur_pos - 1 - offset end elseif key == "Delete" then command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1) @@ -889,22 +912,7 @@ function prompt.run(args, textbox, exe_callback, completion_callback, selectall = nil end - local success = pcall(update) - while not success do - -- TODO UGLY HACK TODO - -- Setting the text failed. Most likely reason is that the user - -- entered a multibyte character and pressed backspace which only - -- removed the last byte. Let's remove another byte. - if cur_pos <= 1 then - -- No text left?! - break - end - - command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos) - cur_pos = cur_pos - 1 - success = pcall(update) - end - + update() if changed_callback then changed_callback(command) end From 87fb3d755387a875f259fa36f57c534586caca5c Mon Sep 17 00:00:00 2001 From: actionless Date: Fri, 11 Jun 2021 03:17:00 +0200 Subject: [PATCH 2/2] test(spec: prompt): add for backspace, ^h, ^f, ^b and fix shim :wlen() implementation --- spec/awful/prompt_spec.lua | 126 ++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 2 deletions(-) diff --git a/spec/awful/prompt_spec.lua b/spec/awful/prompt_spec.lua index 2c0f6522..132593da 100644 --- a/spec/awful/prompt_spec.lua +++ b/spec/awful/prompt_spec.lua @@ -41,9 +41,20 @@ describe('helper functions', function() assert.are_equal('', get_tag(sample_markup)) end) end) +describe('helper functions multibyte', function() + local sample_markup = 'СперваВысокоосвещенныйКонечный' + it('main', function() + assert.are_equal('Сперва', get_first_part(sample_markup)) + assert.are_equal('Высокоосвещенный', get_highlighted_part(sample_markup)) + assert.are_equal('Конечный', get_last_part(sample_markup)) + assert.are_equal('СперваВысокоосвещенныйКонечный', get_prompt_text(sample_markup)) + assert.are_equal('', get_tag(sample_markup)) + end) +end) + local function enter_text(callback, text) - for char in string.gmatch(text, '.') do + for char in string.gmatch(text, '([%z\1-\127\194-\244][\128-\191]*)') do callback({}, char, 'press') callback({}, char, 'release') end @@ -62,7 +73,20 @@ insulate('main', function () } -- luacheck: globals string function string.wlen(self) - return #self + local _, string_length = string.gsub(self, "[^\128-\193]", "") + local byte = string.byte(self) + if ( + #self > 0 and string_length == 0 + ) or ( + byte and + ( + (byte >= 194 and byte <= 244) + ) and + string_length == 1 and #self == 1 + ) then + return -1 + end + return string_length end local keygrabber = require("awful.keygrabber") package.loaded['awful.keygrabber'] = mock(keygrabber, true) @@ -171,6 +195,40 @@ insulate('main', function () prompt_callback({}, 'Left', 'press') assert_prompt_text('comman', 'd', ' ') end) + it('moving cursor readline', function() + prompt.run{ + textbox = atextbox, + } + enter_text(prompt_callback, 'command') + prompt_callback({'Control'}, 'a', 'press') + assert_prompt_text('', 'c', 'ommand ') + + prompt_callback({'Control'}, 'f', 'press') + assert_prompt_text('c', 'o', 'mmand ') + + prompt_callback({'Control'}, 'e', 'press') + assert_prompt_text('command', ' ', '') + + prompt_callback({'Control'}, 'b', 'press') + assert_prompt_text('comman', 'd', ' ') + end) + it('moving cursor readline multibyte', function() + prompt.run{ + textbox = atextbox, + } + enter_text(prompt_callback, 'кокаинум') + prompt_callback({'Control'}, 'a', 'press') + assert_prompt_text('', 'к', 'окаинум ') + + prompt_callback({'Control'}, 'f', 'press') + assert_prompt_text('к', 'о', 'каинум ') + + prompt_callback({'Control'}, 'e', 'press') + assert_prompt_text('кокаинум', ' ', '') + + prompt_callback({'Control'}, 'b', 'press') + assert_prompt_text('кокаину', 'м', ' ') + end) it('backspace', function() prompt.run{ textbox = atextbox, @@ -192,6 +250,70 @@ insulate('main', function () prompt_callback({}, 'BackSpace', 'press') assert_prompt_text('o', 'm', 'an ') end) + it('backspace multibyte', function() + prompt.run{ + textbox = atextbox, + } + enter_text(prompt_callback, 'кокаинум') + prompt_callback({}, 'BackSpace', 'press') + assert_prompt_text('кокаину', ' ', '') + + prompt_callback({}, 'Home', 'press') + prompt_callback({}, 'BackSpace', 'press') + assert_prompt_text('', 'к', 'окаину ') + + --@TODO: Left/Right not yet implemented for multibyte chars + --prompt_callback({}, 'Right', 'press') + --prompt_callback({}, 'BackSpace', 'press') + --assert_prompt_text('', 'о', 'каину ') + + --prompt_callback({}, 'Right', 'press') + --prompt_callback({}, 'Right', 'press') + --prompt_callback({}, 'BackSpace', 'press') + --assert_prompt_text('о', 'а', 'ину ') + end) + it('backspace readline', function() + prompt.run{ + textbox = atextbox, + } + enter_text(prompt_callback, 'command') + prompt_callback({'Control'}, 'h', 'press') + assert_prompt_text('comman', ' ', '') + + prompt_callback({'Control'}, 'a', 'press') + prompt_callback({'Control'}, 'h', 'press') + assert_prompt_text('', 'c', 'omman ') + + prompt_callback({'Control'}, 'f', 'press') + prompt_callback({'Control'}, 'h', 'press') + assert_prompt_text('', 'o', 'mman ') + + prompt_callback({'Control'}, 'f', 'press') + prompt_callback({'Control'}, 'f', 'press') + prompt_callback({'Control'}, 'h', 'press') + assert_prompt_text('o', 'm', 'an ') + end) + it('backspace readline multibyte', function() + prompt.run{ + textbox = atextbox, + } + enter_text(prompt_callback, 'кокаинум') + prompt_callback({'Control'}, 'h', 'press') + assert_prompt_text('кокаину', ' ', '') + + prompt_callback({'Control'}, 'a', 'press') + prompt_callback({'Control'}, 'h', 'press') + assert_prompt_text('', 'к', 'окаину ') + + prompt_callback({'Control'}, 'f', 'press') + prompt_callback({'Control'}, 'h', 'press') + assert_prompt_text('', 'о', 'каину ') + + prompt_callback({'Control'}, 'f', 'press') + prompt_callback({'Control'}, 'f', 'press') + prompt_callback({'Control'}, 'h', 'press') + assert_prompt_text('о', 'а', 'ину ') + end) it('delete', function() prompt.run{ textbox = atextbox,