Compare commits

...

22 Commits

Author SHA1 Message Date
Francesco Abbate 80fb59c73f Implement vim . command 2021-04-05 17:01:32 +02:00
Francesco Abbate 6113c7b38e Implement vim r and ^ command 2021-04-05 16:43:08 +02:00
Francesco Abbate 3986233b5c Implement vim edit inside delimiters 2021-04-02 18:01:33 +02:00
Francesco Abbate 87613a7619 More complete vim command y 2021-04-02 15:42:33 +02:00
Francesco Abbate 4584f98a23 Implement vim visual mode 2021-04-02 12:30:55 +02:00
Francesco Abbate ee040b5785 Add vim command 'b' 2021-04-02 11:12:52 +02:00
Francesco Abbate db8938413c Fix cursor display in command mode 2021-04-02 11:05:26 +02:00
Francesco Abbate e44a408088 Let vim-mode accept textinput events as well as keypress 2021-04-02 11:05:02 +02:00
Francesco Abbate ff3362f9a9 More vim stuff 2021-03-28 00:12:02 +01:00
Francesco Abbate 686ff9b2ad Add begin-of-line vim command 2021-03-28 00:04:04 +01:00
Francesco Abbate 7f37451398 Implement mode dependent caret 2021-03-28 00:03:03 +01:00
Francesco Abbate fbefcc2b72 Add more vim crazyness 2021-03-27 20:15:25 +01:00
Francesco Abbate 879018502f Move vim function in a specific file 2021-03-27 16:27:25 +01:00
Francesco Abbate 4b68ce431c Use global vim mode flag 2021-03-27 15:54:55 +01:00
Francesco Abbate 296ea8b03d vim-mode: accept multi-digit command multiplier 2021-03-25 14:22:36 +01:00
Robert Štojs 40d69470fb
Replicate Vim backspace and CTRL-C behaviour (#129) 2021-03-25 08:55:48 +01:00
Francesco Abbate 9ead6f6427 More accurate '$' and add 'd' vim action's objects 2021-03-24 17:15:20 +01:00
Francesco Abbate 948a4e046d Fix behavior of e and w vim objects 2021-03-24 16:59:13 +01:00
Francesco Abbate 3f4856bccd Add undo command
Do not insert text in command mode
2021-03-24 11:23:04 +01:00
Francesco Abbate b0438b60dc Set the editing mode per view 2021-03-24 11:17:42 +01:00
Francesco Abbate 50bd5e8b2b Fix a few things for vim mode 2021-03-23 16:20:07 +01:00
Francesco Abbate 97a00f946d First preliminary implementation of vim-mode
The basic is there and sort-of-work but largely incomplete and not
yet usable.
2021-03-23 15:41:11 +01:00
10 changed files with 385 additions and 4 deletions

View File

@ -58,6 +58,18 @@ function command.perform(...)
end end
function command.perform_many(n, name)
local cmd = command.map[name]
if not cmd or not cmd.predicate() then return false end
pcall(function()
for i = 1, n do
cmd.perform()
end
end)
return true
end
function command.add_defaults() function command.add_defaults()
local reg = { "core", "root", "command", "doc", "findreplace", "files" } local reg = { "core", "root", "command", "doc", "findreplace", "files" }
for _, name in ipairs(reg) do for _, name in ipairs(reg) do

View File

@ -171,4 +171,20 @@ command.add(nil, {
return common.home_encode_list(common.dir_list_suggest(text, dir_list)) return common.home_encode_list(common.dir_list_suggest(text, dir_list))
end) end)
end, end,
["core:toggle-vim-mode"] = function()
core.vim_mode = not core.vim_mode
end,
["core:set-command-mode"] = function()
core.set_editing_mode(core.active_view, 'command')
end,
["core:set-insert-mode"] = function()
core.set_editing_mode(core.active_view, 'insert')
end,
["core:set-visual-mode"] = function()
core.set_editing_mode(core.active_view, 'visual')
end,
}) })

View File

@ -337,11 +337,13 @@ local translations = {
["next-char"] = translate.next_char, ["next-char"] = translate.next_char,
["previous-word-start"] = translate.previous_word_start, ["previous-word-start"] = translate.previous_word_start,
["next-word-end"] = translate.next_word_end, ["next-word-end"] = translate.next_word_end,
["next-word-begin"] = translate.next_word_begin,
["previous-block-start"] = translate.previous_block_start, ["previous-block-start"] = translate.previous_block_start,
["next-block-end"] = translate.next_block_end, ["next-block-end"] = translate.next_block_end,
["start-of-doc"] = translate.start_of_doc, ["start-of-doc"] = translate.start_of_doc,
["end-of-doc"] = translate.end_of_doc, ["end-of-doc"] = translate.end_of_doc,
["start-of-line"] = translate.start_of_line, ["start-of-line"] = translate.start_of_line,
["start-of-line-content"] = translate.start_of_line_content,
["end-of-line"] = translate.end_of_line, ["end-of-line"] = translate.end_of_line,
["start-of-word"] = translate.start_of_word, ["start-of-word"] = translate.start_of_word,
["end-of-word"] = translate.end_of_word, ["end-of-word"] = translate.end_of_word,
@ -375,4 +377,14 @@ commands["doc:move-to-next-char"] = function()
end end
end end
commands["doc:move-to-end-of-selection"] = function()
local _, _, line, col = doc():get_selection(true)
doc():set_selection(line, col)
end
commands["doc:move-to-start-of-selection"] = function()
local line, col = doc():get_selection(true)
doc():set_selection(line, col)
end
command.add("core.docview", commands) command.add("core.docview", commands)

View File

@ -3,6 +3,7 @@ local Highlighter = require "core.doc.highlighter"
local syntax = require "core.syntax" local syntax = require "core.syntax"
local config = require "core.config" local config = require "core.config"
local common = require "core.common" local common = require "core.common"
local translate = require "core.doc.translate"
local Doc = Object:extend() local Doc = Object:extend()
@ -399,4 +400,10 @@ function Doc:select_to(...)
end end
function Doc:select_with_delimiters(delims, outer)
local line, col = self:get_selection()
self:set_selection(self:position_offset(line, col, translate.inside_delimiters, delims, outer))
end
return Doc return Doc

View File

@ -42,7 +42,7 @@ function translate.previous_word_start(doc, line, col)
end end
function translate.next_word_end(doc, line, col) function translate.up_to_word(doc, line, col)
local prev local prev
local end_line, end_col = translate.end_of_doc(doc, line, col) local end_line, end_col = translate.end_of_doc(doc, line, col)
while line < end_line or col < end_col do while line < end_line or col < end_col do
@ -53,10 +53,22 @@ function translate.next_word_end(doc, line, col)
line, col = doc:position_offset(line, col, 1) line, col = doc:position_offset(line, col, 1)
prev = char prev = char
end end
return line, col
end
function translate.next_word_end(doc, line, col)
line, col = translate.up_to_word(doc, line, col)
return translate.end_of_word(doc, line, col) return translate.end_of_word(doc, line, col)
end end
function translate.next_word_begin(doc, line, col)
line, col = translate.end_of_word(doc, line, col)
return translate.up_to_word(doc, line, col)
end
function translate.start_of_word(doc, line, col) function translate.start_of_word(doc, line, col)
while true do while true do
local line2, col2 = doc:position_offset(line, col, -1) local line2, col2 = doc:position_offset(line, col, -1)
@ -118,6 +130,11 @@ function translate.start_of_line(doc, line, col)
end end
function translate.start_of_line_content(doc, line, col)
return translate.up_to_word(doc, line, 1)
end
function translate.end_of_line(doc, line, col) function translate.end_of_line(doc, line, col)
return line, math.huge return line, math.huge
end end
@ -133,4 +150,45 @@ function translate.end_of_doc(doc, line, col)
end end
function translate.inside_delimiters(doc, line, col, delims, outer)
print('translate.inside_delimiters', delims[2], outer)
local line1, col1 = line, col
while true do
local lineb, colb = doc:position_offset(line1, col1, -1)
local char = doc:get_char(lineb, colb)
if char == delims[1]
or line1 == lineb and col1 == colb then
break
end
line1, col1 = lineb, colb
end
if outer then
line1, col1 = doc:position_offset(line1, col1, -1)
end
local line2, col2 = line, col
while true do
local linef, colf = doc:position_offset(line2, col2, 1)
local char = doc:get_char(line2, col2)
if char == delims[2]
or linef == line2 and colf == col2 then
break
end
line2, col2 = linef, colf
end
if outer then
line2, col2 = doc:position_offset(line2, col2, 1)
while doc:get_char(line2, col2) == ' ' do
local nline2, ncol2 = doc:position_offset(line2, col2, 1)
if nline2 == line2 and ncol2 == col2 then break end
line2, col2 = nline2, ncol2
end
end
return line1, col1, line2, col2
end
return translate return translate

View File

@ -56,6 +56,7 @@ function DocView:new(doc)
self.font = "code_font" self.font = "code_font"
self.last_x_offset = {} self.last_x_offset = {}
self.blink_timer = 0 self.blink_timer = 0
self.editing_mode = 'command'
end end
@ -168,6 +169,16 @@ function DocView:get_x_offset_col(line, x)
end end
function DocView:get_editing_mode()
return self.editing_mode
end
function DocView:set_editing_mode(mode)
self.editing_mode = mode
end
function DocView:resolve_screen_position(x, y) function DocView:resolve_screen_position(x, y)
local ox, oy = self:get_line_screen_position(1) local ox, oy = self:get_line_screen_position(1)
local line = math.floor((y - oy) / self:get_line_height()) + 1 local line = math.floor((y - oy) / self:get_line_height()) + 1
@ -273,8 +284,10 @@ end
function DocView:on_text_input(text) function DocView:on_text_input(text)
if not core.using_vim_mode(self) or self.editing_mode ~= 'command' then
self.doc:text_input(text) self.doc:text_input(text)
end end
end
function DocView:update() function DocView:update()
@ -349,7 +362,13 @@ function DocView:draw_line_body(idx, x, y)
and system.window_has_focus() then and system.window_has_focus() then
local lh = self:get_line_height() local lh = self:get_line_height()
local x1 = x + self:get_col_x_offset(line, col) local x1 = x + self:get_col_x_offset(line, col)
renderer.draw_rect(x1, y, style.caret_width, lh, style.caret) local caret_width
if core.get_editing_mode(self) == 'command' then
caret_width = self:get_font():get_width(self.doc.lines[line1]:sub(col1, col1))
else
caret_width = style.caret_width
end
renderer.draw_rect(x1, y, caret_width, lh, style.caret)
end end
end end

View File

@ -395,6 +395,7 @@ function core.init()
core.log_items = {} core.log_items = {}
core.docs = {} core.docs = {}
core.threads = setmetatable({}, { __mode = "k" }) core.threads = setmetatable({}, { __mode = "k" })
core.vim_mode = false
local project_dir_abs = system.absolute_path(project_dir) local project_dir_abs = system.absolute_path(project_dir)
local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs) local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs)
@ -713,10 +714,18 @@ function core.try(fn, ...)
return false, err return false, err
end end
-- vim module but loaded only when required.
local vim
function core.on_event(type, ...) function core.on_event(type, ...)
local did_keymap = false local did_keymap = false
if type == "textinput" then if type == "textinput" then
local vim_mode = core.get_editing_mode(core.active_view)
if vim_mode then
local vim = require "core.vim"
local accepted = vim.on_text_input(vim_mode, ...)
if accepted then return false end
end
core.root_view:on_text_input(...) core.root_view:on_text_input(...)
elseif type == "keypressed" then elseif type == "keypressed" then
did_keymap = keymap.on_key_pressed(...) did_keymap = keymap.on_key_pressed(...)
@ -907,5 +916,23 @@ core.add_save_hook(function(filename)
end) end)
function core.using_vim_mode(view)
return core.vim_mode and getmetatable(view) == DocView
end
function core.get_editing_mode(view)
if core.using_vim_mode(view) then
return view:get_editing_mode()
end
end
function core.set_editing_mode(view, mode)
if core.using_vim_mode(view) then
view:set_editing_mode(mode)
end
end
return core return core

View File

@ -1,3 +1,4 @@
local core = require "core"
local command = require "core.command" local command = require "core.command"
local keymap = {} local keymap = {}
@ -62,6 +63,14 @@ function keymap.on_key_pressed(k)
end end
else else
local stroke = key_to_stroke(k) local stroke = key_to_stroke(k)
local vim_mode = core.get_editing_mode(core.active_view)
if vim_mode then
local vim = require "core.vim"
local accepted = vim.on_text_input(vim_mode, nil, stroke)
if accepted then return true end
-- if the command is not recognized by vim fall throught to process
-- it as an ordinary command
end
local commands = keymap.map[stroke] local commands = keymap.map[stroke]
if commands then if commands then
for _, cmd in ipairs(commands) do for _, cmd in ipairs(commands) do

View File

@ -110,6 +110,7 @@ function StatusView:get_items()
local indent = dv.doc.indent_info local indent = dv.doc.indent_info
local indent_label = (indent and indent.type == "hard") and "tabs: " or "spaces: " local indent_label = (indent and indent.type == "hard") and "tabs: " or "spaces: "
local indent_size = indent and tostring(indent.size) .. (indent.confirmed and "" or "*") or "unknown" local indent_size = indent and tostring(indent.size) .. (indent.confirmed and "" or "*") or "unknown"
local editing_mode = core.get_editing_mode(dv)
return { return {
dirty and style.accent or style.text, style.icon_font, "f", dirty and style.accent or style.text, style.icon_font, "f",
@ -124,7 +125,8 @@ function StatusView:get_items()
self.separator, self.separator,
string.format("%d%%", line / #dv.doc.lines * 100), string.format("%d%%", line / #dv.doc.lines * 100),
}, { }, {
style.text, indent_label, indent_size, style.caret, core.vim_mode and string.upper(editing_mode) or '', style.text, self.separator2,
indent_label, indent_size,
style.dim, self.separator2, style.text, style.dim, self.separator2, style.text,
style.icon_font, "g", style.icon_font, "g",
style.font, style.dim, self.separator2, style.text, style.font, style.dim, self.separator2, style.text,

219
data/core/vim.lua Normal file
View File

@ -0,0 +1,219 @@
local core = require "core"
local command = require "core.command"
local vim = {}
local command_buffer = {verb = '', mult_accu = '', inside = ''}
function command_buffer:reset()
self.verb = ''
self.mult_accu = ''
self.inside = ''
end
function command_buffer:mult()
return self.mult_accu == '' and 1 or tostring(self.mult_accu)
end
function command_buffer:add_mult_char(k)
self.mult_accu = self.mult_accu .. k
end
local function table_find(t, e)
for i = 1, #t do
if t[i] == e then return i end
end
end
local verbs_obj = {'c', 'd', 'r', 'y'}
local verbs_imm = {'a', 'h', 'i', 'j', 'k', 'l', 'o', 'p', 'u', 'v', 'x', 'O',
'left', 'right', 'up', 'down', 'escape'}
local vim_objects = {'a', 'b', 'd', 'e', 'i', 'w', 'y', '^', '0', '$'}
local vim_object_map = {
['b'] = 'start-of-word',
['e'] = 'next-word-end',
['w'] = 'next-word-begin',
['$'] = 'end-of-line',
['^'] = 'start-of-line-content',
['0'] = 'start-of-line',
}
local inside_delims = {
[')'] = {'(', ')'},
[']'] = {'[', ']'},
['}'] = {'{', '}'},
['>'] = {'<', '>'},
['"'] = {'"', '"'},
["'"] = {"'", "'"},
}
local vim_previous_command
local function doc_command(action, command)
return 'doc:' .. action .. '-' .. command
end
local function vim_execute(mode, verb, mult, object)
vim_previous_command = {verb, mult, object}
local action = (mode == 'command' and 'move-to' or 'select-to')
if verb == '' then
if object == '$' then
command.perform_many(mult - 1, doc_command(action, 'next-line'))
command.perform(doc_command(action, 'end-of-line'))
else
if object == 'e' then
command.perform(doc_command(action, 'next-char'))
end
command.perform_many(mult, doc_command(action, vim_object_map[object]))
if object == 'e' then
command.perform(doc.command(action, 'previous-char'))
end
end
elseif verb == 'd' or verb == 'y' then
if mode == 'command' then -- d and y act as immediate mode commands in visual mode
if object == '$' then
command.perform_many(mult - 1, 'doc:select-to-next-line')
command.perform('doc:select-to-end-of-line')
elseif object == verb then
command.perform('doc:move-to-start-of-line')
command.perform_many(mult, 'doc:select-to-next-line')
else
command.perform_many(mult, 'doc:select-to-' .. vim_object_map[object])
end
end
command.perform('doc:copy')
if verb == 'd' then
command.perform('doc:cut')
else
command.perform('doc:move-to-start-of-selection')
end
command.perform('core:set-command-mode')
elseif verb == 'c' then
command.perform_many(mult, 'doc:select-to-' .. vim_object_map[object])
command.perform('doc:copy')
command.perform('doc:cut')
command.perform('core:set-insert-mode')
elseif verb == 'h' or verb == 'left' then
command.perform_many(mult, doc_command(action, 'previous-char'))
elseif verb == 'j' or verb == 'down' then
command.perform_many(mult, doc_command(action, 'next-line'))
elseif verb == 'k' or verb == 'up' then
command.perform_many(mult, doc_command(action, 'previous-line'))
elseif verb == 'l' or verb == 'right' then
command.perform_many(mult, doc_command(action, 'next-char'))
elseif verb == 'x' then
command.perform_many(mult, 'doc:delete')
elseif verb == 'a' then
command.perform('core:set-insert-mode')
command.perform('doc:move-to-next-char')
elseif verb == 'i' then
command.perform('core:set-insert-mode')
elseif verb == 'o' then
command.perform('doc:move-to-end-of-line')
command.perform('doc:newline')
command.perform('core:set-insert-mode')
elseif verb == 'O' then
command.perform('doc:move-to-start-of-line')
command.perform('doc:newline')
command.perform('doc:move-to-previous-line')
command.perform('core:set-insert-mode')
elseif verb == 'p' then
command.perform('doc:paste')
elseif verb == 'u' then
command.perform('doc:undo')
elseif verb == 'v' then
command.perform('core:set-visual-mode')
elseif verb == 'y' then
command.perform('doc:copy')
command.perform('doc:move-to-start-of-selection')
command.perform('core:set-command-mode')
elseif verb == 'escape' then
command.perform('doc:move-to-end-of-selection')
command.perform('core:set-command-mode')
else
return false
end
return true
end
function vim.on_text_input(mode, text_raw, stroke)
local text = text_raw or stroke
local byte = text_raw and string.byte(text_raw)
local byte0, byte9 = string.byte('0'), string.byte('9')
local view = core.active_view
local doc = view.doc
if mode == 'command' or mode == 'visual' then
if command_buffer.inside ~= '' and inside_delims[text] then
-- got character for inside delimiter edits
local outer = command_buffer.inside == 'a'
doc:select_with_delimiters(inside_delims[text], outer)
command.perform('doc:delete')
if command_buffer.verb == 'c' then
view:set_editing_mode('insert')
end
command_buffer:reset()
return true
elseif command_buffer.verb == 'r' then
if text_raw then
command.perform('doc:delete')
local line, col = doc:get_selection()
doc:insert(line, col, text_raw)
command_buffer:reset()
return true
end
elseif text == '.' then
if vim_previous_command then
vim_execute(mode, unpack(vim_previous_command))
return true
end
elseif command_buffer.verb == '' and table_find(verbs_imm, text) then
-- execute immediate vim command
vim_execute(mode, text, command_buffer:mult())
command_buffer:reset()
return true
elseif command_buffer.verb == '' and table_find(verbs_obj, text) then
-- vim command that takes an object
if mode == 'command' then
-- store the command without executing
command_buffer.verb = text
else
-- visual mode: execute the command
vim_execute(mode, text, command_buffer:mult())
command_buffer:reset()
end
return true
elseif text_raw and byte
and (byte > byte0 or command_buffer.mult_accu ~= '' and byte == byte0)
and byte <= byte9 then
-- numeric command multiplier
command_buffer:add_mult_char(text_raw)
return true
elseif table_find(vim_objects, text) then
-- object of a verb
if text == 'i' or text == 'a' then
command_buffer.inside = text
else
vim_execute(mode, command_buffer.verb, command_buffer:mult(), text)
command_buffer:reset()
end
return true
elseif stroke == 'escape' then
core.active_view:set_editing_mode('command')
command_buffer:reset()
return true
end
elseif mode == 'insert' then
if stroke == 'escape' or stroke == 'ctrl+c' then
core.active_view:set_editing_mode('command')
return true
end
if stroke == 'backspace' then
command.perform('doc:backspace')
end
end
return false
end
return vim