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:
Přemysl Eric Janouch 2022-09-06 17:17:32 +02:00
parent 2d30b6d115
commit 8cd94b30f6
Signed by: p
GPG Key ID: A0420B94F92B9493
3 changed files with 218 additions and 80 deletions

View File

@ -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 ""

View File

@ -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;

View File

@ -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,
})
},
}