Merge pull request #3362 from actionless/prompt-multibyte-hack

Fix the hack for multibyte characters in prompt (fixes #3308)
This commit is contained in:
mergify[bot] 2021-07-12 06:07:03 +00:00 committed by GitHub
commit 832483dd60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 159 additions and 29 deletions

View File

@ -114,7 +114,6 @@ local io = io
local table = table local table = table
local math = math local math = math
local ipairs = ipairs local ipairs = ipairs
local pcall = pcall
local capi = local capi =
{ {
selection = selection selection = selection
@ -258,6 +257,11 @@ local function history_add(id, command)
end end
local function have_multibyte_char_at(text, position)
return text:sub(position, position):wlen() == -1
end
--- Draw the prompt text with a cursor. --- Draw the prompt text with a cursor.
-- @tparam table args The table of arguments. -- @tparam table args The table of arguments.
-- @field text The text. -- @field text The text.
@ -285,10 +289,14 @@ local function prompt_text_with_cursor(args)
text_start = gstring.xml_escape(text) text_start = gstring.xml_escape(text)
text_end = "" text_end = ""
else 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 = " " spacer = " "
text_start = gstring.xml_escape(text:sub(1, args.cursor_pos - 1)) 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 end
local cursor_color = gcolor.ensure_pango_color(args.cursor_color) 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() local function update()
textbox:set_font(font) textbox:set_font(font)
textbox:set_markup(prompt_text_with_cursor{ textbox:set_markup(prompt_text_with_cursor{
text = command, text_color = inv_col, cursor_color = cur_col, text = command, text_color = inv_col, cursor_color = cur_col,
cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall, cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall,
prompt = prettyprompt, highlighter = highlighter }) prompt = prettyprompt, highlighter = highlighter })
end end
grabber = keygrabber.run( grabber = keygrabber.run(
@ -663,6 +671,9 @@ function prompt.run(args, textbox, exe_callback, completion_callback,
elseif key == "b" then elseif key == "b" then
if cur_pos > 1 then if cur_pos > 1 then
cur_pos = cur_pos - 1 cur_pos = cur_pos - 1
if have_multibyte_char_at(command, cur_pos) then
cur_pos = cur_pos - 1
end
end end
elseif key == "d" then elseif key == "d" then
if cur_pos <= #command then if cur_pos <= #command then
@ -711,12 +722,20 @@ function prompt.run(args, textbox, exe_callback, completion_callback,
end end
elseif key == "f" then elseif key == "f" then
if cur_pos <= #command 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 end
elseif key == "h" then elseif key == "h" then
if cur_pos > 1 then if cur_pos > 1 then
command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos) local offset = 0
cur_pos = cur_pos - 1 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 end
elseif key == "k" then elseif key == "k" then
command = command:sub(1, cur_pos - 1) command = command:sub(1, cur_pos - 1)
@ -844,8 +863,12 @@ function prompt.run(args, textbox, exe_callback, completion_callback,
cur_pos = #command + 1 cur_pos = #command + 1
elseif key == "BackSpace" then elseif key == "BackSpace" then
if cur_pos > 1 then if cur_pos > 1 then
command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos) local offset = 0
cur_pos = cur_pos - 1 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 end
elseif key == "Delete" then elseif key == "Delete" then
command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1) 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 selectall = nil
end end
local success = pcall(update) 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
if changed_callback then if changed_callback then
changed_callback(command) changed_callback(command)
end end

View File

@ -41,9 +41,20 @@ describe('helper functions', function()
assert.are_equal('<span property1="foo">', get_tag(sample_markup)) assert.are_equal('<span property1="foo">', get_tag(sample_markup))
end) end)
end) end)
describe('helper functions multibyte', function()
local sample_markup = 'Сперва<span property1="foo">Высокоосвещенный</span>Конечный'
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('<span property1="foo">', get_tag(sample_markup))
end)
end)
local function enter_text(callback, text) 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, 'press')
callback({}, char, 'release') callback({}, char, 'release')
end end
@ -62,7 +73,20 @@ insulate('main', function ()
} }
-- luacheck: globals string -- luacheck: globals string
function string.wlen(self) 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 end
local keygrabber = require("awful.keygrabber") local keygrabber = require("awful.keygrabber")
package.loaded['awful.keygrabber'] = mock(keygrabber, true) package.loaded['awful.keygrabber'] = mock(keygrabber, true)
@ -171,6 +195,40 @@ insulate('main', function ()
prompt_callback({}, 'Left', 'press') prompt_callback({}, 'Left', 'press')
assert_prompt_text('comman', 'd', ' ') assert_prompt_text('comman', 'd', ' ')
end) 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() it('backspace', function()
prompt.run{ prompt.run{
textbox = atextbox, textbox = atextbox,
@ -192,6 +250,70 @@ insulate('main', function ()
prompt_callback({}, 'BackSpace', 'press') prompt_callback({}, 'BackSpace', 'press')
assert_prompt_text('o', 'm', 'an ') assert_prompt_text('o', 'm', 'an ')
end) 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() it('delete', function()
prompt.run{ prompt.run{
textbox = atextbox, textbox = atextbox,