1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
|
local destination_engine = require("clever_f.destination_engine")
local domain = require("clever_f.domain")
local sequence_state = require("clever_f.sequence_state")
local text_topology = require("clever_f.text_topology")
local M = {}
local MotionExecutor = {}
M.MotionExecutor = MotionExecutor
M.ExecutionPath = {
VISUAL = "visual",
COMMAND = "command",
}
local executor_records = setmetatable({}, { __mode = "k" })
local function fail(message, level)
error(message, (level or 1) + 1)
end
function M.execution_path(context)
context = domain.ModeContext.from_full_mode(context)
if context.visual_kind ~= nil then
return M.ExecutionPath.VISUAL
end
return M.ExecutionPath.COMMAND
end
function M.moved_forward(origin, destination)
origin = domain.Position.coerce(origin)
destination = domain.Position.coerce(destination)
return domain.Position.compare(destination, origin) > 0
end
function M.command_moved_forward(descriptor, origin, destination)
descriptor = domain.Descriptor.from_string(descriptor)
origin = domain.Position.coerce(origin)
destination = domain.Position.coerce(destination)
if descriptor.family == domain.Family.TILL
and domain.Position.stationary(origin, destination)
then
return false
end
return M.moved_forward(origin, destination)
end
local function copy_options(options)
local result = {}
for key, value in pairs(options or {}) do
result[key] = value
end
return result
end
local function normalize_options(options, dependencies)
if MotionExecutor.is(options) and dependencies == nil then
return options
end
if type(options) ~= "table" then
fail("MotionExecutor options must be a table", 3)
end
if options.host ~= nil then
if dependencies ~= nil then
fail("MotionExecutor dependencies must be part of its options", 3)
end
return options
end
local result = copy_options(dependencies)
result.host = options
return result
end
local function require_host(host)
if type(host) ~= "table"
or type(host.read_cursor) ~= "function"
or type(host.apply_cursor) ~= "function"
then
fail("MotionExecutor host must provide cursor movement", 3)
end
return host
end
local function require_destination_engine(engine)
engine = engine or destination_engine.new()
if type(engine) ~= "table" or type(engine.calculate) ~= "function" then
fail("MotionExecutor destination engine must provide calculate", 3)
end
return engine
end
local function require_feedback_service(service)
if service ~= nil and (type(service) ~= "table"
or type(service.migrate_command) ~= "function")
then
fail("MotionExecutor feedback service must provide migrate_command", 3)
end
return service
end
local function require_state(state)
state = state or sequence_state.get()
if not sequence_state.is(state) then
fail("MotionExecutor state must be the plugin-global SequenceState", 3)
end
return state
end
local executor_metatable = {
__index = MotionExecutor,
__newindex = function()
fail("MotionExecutor values are immutable", 2)
end,
__tostring = function()
return "motion-executor"
end,
__metatable = "clever_f.motion_executor.MotionExecutor",
}
function MotionExecutor.new(options, dependencies)
options = normalize_options(options, dependencies)
if MotionExecutor.is(options) then
return options
end
local executor = setmetatable({}, executor_metatable)
executor_records[executor] = {
host = require_host(options.host),
destination_engine = require_destination_engine(
options.destination_engine or options.engine
),
feedback_service = require_feedback_service(
options.feedback_service or options.feedback
),
state = require_state(options.state),
}
return executor
end
function MotionExecutor.is(value)
return type(value) == "table" and executor_records[value] ~= nil
end
local function execution_request(view, context, plan, count, first_move)
if not text_topology.TextView.is(view) then
fail("motion execution view must be a TextView", 3)
end
context = domain.ModeContext.from_full_mode(context)
if not domain.ResolvedMotionPlan.is(plan) then
fail("motion execution plan must be a ResolvedMotionPlan", 3)
end
count = domain.Count.new(count)
if type(first_move) ~= "boolean" then
fail("motion execution first_move must be a Boolean", 3)
end
return {
view = view,
context = context,
plan = plan,
count = count,
first_move = first_move,
}
end
local function calculate(executor, request, origin)
return executor_records[executor].destination_engine:calculate(
request.view,
origin,
request.plan,
request.count,
request.first_move
)
end
local function migrate_command_feedback(executor, request, origin, outcome)
local record = executor_records[executor]
local feedback = record.feedback_service
if feedback == nil then
return
end
feedback:migrate_command({
context = request.context,
origin = origin,
destination = outcome.endpoint,
plan = request.plan,
resolved_motion_plan = request.plan,
outcome = outcome,
count = request.count,
first_move = request.first_move,
moved_forward = request.moved_forward,
previous_moved_forward = record.state.moved_forward,
previous_moved_forward_initialized = record.state.moved_forward_initialized,
})
end
function MotionExecutor:_execute_command(request)
local host = executor_records[self].host
local origin = host:read_cursor()
local outcome = calculate(self, request, origin)
if outcome.successful_steps > 0 then
host:apply_cursor(outcome.endpoint)
end
if not outcome.complete then
return domain.ActionOutcome.from_search(outcome, request.plan.descriptor)
end
request.moved_forward = M.command_moved_forward(
request.plan.descriptor,
origin,
outcome.endpoint
)
migrate_command_feedback(self, request, origin, outcome)
return domain.ActionOutcome.from_search(outcome, request.plan.descriptor)
end
function MotionExecutor:_execute_visual(request)
local host = executor_records[self].host
local origin = host:read_cursor()
local outcome = calculate(self, request, origin)
return domain.ActionOutcome.from_search(outcome, request.plan.descriptor)
end
function MotionExecutor:execute(view, context, plan, count, first_move)
local request = execution_request(view, context, plan, count, first_move)
if M.execution_path(request.context) == M.ExecutionPath.VISUAL then
return self:_execute_visual(request)
end
return self:_execute_command(request)
end
function M.new(options, dependencies)
return MotionExecutor.new(options, dependencies)
end
function M.execute(host, view, context, plan, count, first_move, dependencies)
return MotionExecutor.new(host, dependencies):execute(
view,
context,
plan,
count,
first_move
)
end
M.run = M.execute
setmetatable(M, {
__call = function(_, options, dependencies)
return MotionExecutor.new(options, dependencies)
end,
})
return M
|