xP: implement tab completion
Currently it only goes for the longest common prefix. Refactor WebSocket handling into an abstraction for our protocol. The Go code generater finally needed fixing.
This commit is contained in:
parent
2d30b6d115
commit
8cd94b30f6
|
@ -382,7 +382,8 @@ function codegen_union(name, cg, gotype, tagfield, tagvar) {
|
|||
print "}"
|
||||
print ""
|
||||
|
||||
print "func (u *" gotype ") MarshalJSON() ([]byte, error) {"
|
||||
# This cannot be a pointer method, it wouldn't work recursively.
|
||||
print "func (u " gotype ") MarshalJSON() ([]byte, error) {"
|
||||
print "\treturn json.Marshal(u.Interface)"
|
||||
print "}"
|
||||
print ""
|
||||
|
|
2
xC-proto
2
xC-proto
|
@ -104,6 +104,8 @@ struct EventMessage {
|
|||
} items<>;
|
||||
case BUFFER_CLEAR:
|
||||
string buffer_name;
|
||||
|
||||
// Restriction: command_seq is strictly increasing, across both of these.
|
||||
case ERROR:
|
||||
u32 command_seq;
|
||||
string error;
|
||||
|
|
293
xP/public/xP.js
293
xP/public/xP.js
|
@ -1,5 +1,175 @@
|
|||
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
'use strict'
|
||||
|
||||
// ---- RPC --------------------------------------------------------------------
|
||||
|
||||
class RelayRpc extends EventTarget {
|
||||
constructor(url) {
|
||||
super()
|
||||
this.url = url
|
||||
this.commandSeq = 0
|
||||
}
|
||||
|
||||
connect() {
|
||||
// We can't close the connection immediately, as that queues a task.
|
||||
if (this.ws !== undefined)
|
||||
throw "Already connecting or connected"
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let ws = this.ws = new WebSocket(this.url)
|
||||
ws.onopen = event => {
|
||||
this._initialize()
|
||||
resolve()
|
||||
}
|
||||
// It's going to be code 1006 with no further info.
|
||||
ws.onclose = event => {
|
||||
reject()
|
||||
this.ws = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_initialize() {
|
||||
this.ws.onopen = undefined
|
||||
this.ws.onmessage = event => {
|
||||
this._process(event.data)
|
||||
}
|
||||
this.ws.onerror = event => {
|
||||
this.dispatchEvent(new CustomEvent('error'))
|
||||
}
|
||||
this.ws.onclose = event => {
|
||||
let message = "Connection closed: " +
|
||||
event.code + " (" + event.reason + ")"
|
||||
for (const seq in this.promised)
|
||||
this.promised[seq].reject(message)
|
||||
|
||||
this.ws = undefined
|
||||
this.dispatchEvent(new CustomEvent('close', {
|
||||
detail: {message, code: event.code, reason: event.reason},
|
||||
}))
|
||||
|
||||
// Now connect() can be called again.
|
||||
}
|
||||
|
||||
this.promised = {}
|
||||
}
|
||||
|
||||
_process(data) {
|
||||
console.log(data)
|
||||
|
||||
if (typeof data !== 'string')
|
||||
throw "Binary messages not supported"
|
||||
|
||||
let message = JSON.parse(data)
|
||||
if (typeof message !== 'object')
|
||||
throw "Invalid message"
|
||||
let e = message.data
|
||||
if (typeof e !== 'object')
|
||||
throw "Invalid message"
|
||||
|
||||
switch (e.event) {
|
||||
case 'Error':
|
||||
if (this.promised[e.commandSeq] !== undefined)
|
||||
this.promised[e.commandSeq].reject(e.error)
|
||||
else
|
||||
console.error("Unawaited error")
|
||||
break
|
||||
case 'Response':
|
||||
if (this.promised[e.commandSeq] !== undefined)
|
||||
this.promised[e.commandSeq].resolve(e.data)
|
||||
else
|
||||
console.error("Unawaited response")
|
||||
break
|
||||
default:
|
||||
if (typeof e.event !== 'string')
|
||||
throw "Invalid event tag"
|
||||
|
||||
this.dispatchEvent(new CustomEvent(e.event, {detail: e}))
|
||||
|
||||
// Minor abstraction layering violation.
|
||||
m.redraw()
|
||||
return
|
||||
}
|
||||
|
||||
delete this.promised[e.commandSeq]
|
||||
for (const seq in this.promised) {
|
||||
// We don't particularly care about wraparound issues.
|
||||
if (seq >= e.commandSeq)
|
||||
continue
|
||||
|
||||
this.promised[seq].reject("No response")
|
||||
delete this.promised[seq]
|
||||
}
|
||||
}
|
||||
|
||||
send(params) {
|
||||
if (this.ws === undefined)
|
||||
throw "Not connected"
|
||||
if (typeof params !== 'object')
|
||||
throw "Method parameters must be an object"
|
||||
|
||||
let seq = ++this.commandSeq
|
||||
if (seq >= 1 << 32)
|
||||
seq = this.commandSeq = 0
|
||||
|
||||
this.ws.send(JSON.stringify({commandSeq: seq, data: params}))
|
||||
return new Promise((resolve, reject) => {
|
||||
this.promised[seq] = {resolve, reject}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Event processing -------------------------------------------------------
|
||||
|
||||
// TODO: Probably reset state on disconnect, and indicate to user.
|
||||
let rpc = new RelayRpc(proxy)
|
||||
rpc.connect()
|
||||
.then(result => {
|
||||
rpc.send({command: 'Hello', version: 1})
|
||||
})
|
||||
|
||||
let buffers = new Map()
|
||||
let bufferCurrent = undefined
|
||||
|
||||
rpc.addEventListener('BufferUpdate', event => {
|
||||
let e = event.detail, b = buffers.get(e.bufferName)
|
||||
if (b === undefined) {
|
||||
b = {lines: []}
|
||||
buffers.set(e.bufferName, b)
|
||||
}
|
||||
// TODO: Update any buffer properties.
|
||||
})
|
||||
|
||||
rpc.addEventListener('BufferRename', event => {
|
||||
let e = event.detail
|
||||
buffers.set(e.new, buffers.get(e.bufferName))
|
||||
buffers.delete(e.bufferName)
|
||||
})
|
||||
|
||||
rpc.addEventListener('BufferRemove', event => {
|
||||
let e = event.detail
|
||||
buffers.delete(e.bufferName)
|
||||
})
|
||||
|
||||
rpc.addEventListener('BufferActivate', event => {
|
||||
let e = event.detail
|
||||
bufferCurrent = e.bufferName
|
||||
// TODO: Somehow scroll to the end of it immediately.
|
||||
// TODO: Focus the textarea.
|
||||
})
|
||||
|
||||
rpc.addEventListener('BufferLine', event => {
|
||||
let e = event.detail, b = buffers.get(e.bufferName)
|
||||
if (b !== undefined)
|
||||
b.lines.push({when: e.when, rendition: e.rendition, items: e.items})
|
||||
})
|
||||
|
||||
rpc.addEventListener('BufferClear', event => {
|
||||
let e = event.detail, b = buffers.get(e.bufferName)
|
||||
if (b !== undefined)
|
||||
b.lines.length = 0
|
||||
})
|
||||
|
||||
// --- Colours -----------------------------------------------------------------
|
||||
|
||||
|
@ -33,69 +203,6 @@ function applyColor(fg, bg, inverse) {
|
|||
return style
|
||||
}
|
||||
|
||||
// ---- Event processing -------------------------------------------------------
|
||||
|
||||
// TODO: Probably reset state on disconnect, and indicate to user.
|
||||
let socket = new WebSocket(proxy)
|
||||
|
||||
let commandSeq = 0
|
||||
function send(command) {
|
||||
socket.send(JSON.stringify({commandSeq: ++commandSeq, data: command}))
|
||||
}
|
||||
|
||||
socket.onopen = function(event) {
|
||||
send({command: 'Hello', version: 1})
|
||||
}
|
||||
|
||||
let buffers = new Map()
|
||||
let bufferCurrent = undefined
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
console.log(event.data)
|
||||
|
||||
let e = JSON.parse(event.data).data
|
||||
switch (e.event) {
|
||||
case 'BufferUpdate':
|
||||
{
|
||||
let b = buffers.get(e.bufferName)
|
||||
if (b === undefined) {
|
||||
b = {lines: []}
|
||||
buffers.set(e.bufferName, b)
|
||||
}
|
||||
// TODO: Update any buffer properties.
|
||||
break
|
||||
}
|
||||
case 'BufferRename':
|
||||
buffers.set(e.new, buffers.get(e.bufferName))
|
||||
buffers.delete(e.bufferName)
|
||||
break
|
||||
case 'BufferRemove':
|
||||
buffers.delete(e.bufferName)
|
||||
break
|
||||
case 'BufferActivate':
|
||||
bufferCurrent = e.bufferName
|
||||
// TODO: Somehow scroll to the end of it immediately.
|
||||
// TODO: Focus the textarea.
|
||||
break
|
||||
case 'BufferLine':
|
||||
{
|
||||
let b = buffers.get(e.bufferName)
|
||||
if (b !== undefined)
|
||||
b.lines.push({when: e.when, rendition: e.rendition, items: e.items})
|
||||
break
|
||||
}
|
||||
case 'BufferClear':
|
||||
{
|
||||
let b = buffers.get(e.bufferName)
|
||||
if (b !== undefined)
|
||||
b.lines.length = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
m.redraw()
|
||||
}
|
||||
|
||||
// ---- UI ---------------------------------------------------------------------
|
||||
|
||||
let BufferList = {
|
||||
|
@ -103,8 +210,8 @@ let BufferList = {
|
|||
let items = []
|
||||
buffers.forEach((b, name) => {
|
||||
let attrs = {
|
||||
onclick: e => {
|
||||
send({command: 'BufferActivate', bufferName: name})
|
||||
onclick: event => {
|
||||
rpc.send({command: 'BufferActivate', bufferName: name})
|
||||
},
|
||||
}
|
||||
if (name == bufferCurrent)
|
||||
|
@ -213,26 +320,54 @@ let Buffer = {
|
|||
},
|
||||
}
|
||||
|
||||
function onKeyDown(event) {
|
||||
// TODO: And perhaps on other actions, too.
|
||||
rpc.send({command: 'Active'})
|
||||
|
||||
// TODO: Cancel any current autocomplete.
|
||||
|
||||
let textarea = event.currentTarget
|
||||
switch (event.keyCode) {
|
||||
case 9:
|
||||
if (textarea.selectionStart !== textarea.selectionEnd)
|
||||
return
|
||||
rpc.send({
|
||||
command: 'BufferComplete',
|
||||
bufferName: bufferCurrent,
|
||||
text: textarea.value,
|
||||
position: textarea.selectionEnd,
|
||||
}).then(response => {
|
||||
// TODO: Somehow display remaining options, or cycle through.
|
||||
if (response.completions.length)
|
||||
textarea.setRangeText(response.completions[0],
|
||||
response.start, textarea.selectionEnd, 'end')
|
||||
if (response.completions.length === 1)
|
||||
textarea.setRangeText(' ',
|
||||
textarea.selectionStart, textarea.selectionEnd, 'end')
|
||||
})
|
||||
break;
|
||||
case 13:
|
||||
rpc.send({
|
||||
command: 'BufferInput',
|
||||
bufferName: bufferCurrent,
|
||||
text: textarea.value,
|
||||
})
|
||||
textarea.value = ''
|
||||
break;
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// TODO: This should be remembered across buffer switches,
|
||||
// and we'll probably have to intercept /all/ key presses.
|
||||
let Input = {
|
||||
view: vnode => {
|
||||
return m('textarea', {
|
||||
rows: 1,
|
||||
onkeydown: e => {
|
||||
// TODO: And perhaps on other actions, too.
|
||||
send({command: 'Active'})
|
||||
if (e.keyCode !== 13)
|
||||
return
|
||||
|
||||
send({
|
||||
command: 'BufferInput',
|
||||
bufferName: bufferCurrent,
|
||||
text: e.currentTarget.value,
|
||||
})
|
||||
e.preventDefault()
|
||||
e.currentTarget.value = ''
|
||||
},
|
||||
onkeydown: onKeyDown,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue