mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 06:28:28 +08:00
AsmAnalyser is now updated whenever text changes.
This commit is contained in:
parent
fb7aaf2906
commit
540e35ffc9
@ -0,0 +1,29 @@
|
||||
package world.phantasmal.core
|
||||
|
||||
fun <E> MutableList<E>.replaceAll(elements: Collection<E>): Boolean {
|
||||
clear()
|
||||
return addAll(elements)
|
||||
}
|
||||
|
||||
fun <E> MutableList<E>.replaceAll(elements: Iterable<E>): Boolean {
|
||||
clear()
|
||||
return addAll(elements)
|
||||
}
|
||||
|
||||
fun <E> MutableList<E>.replaceAll(elements: Sequence<E>): Boolean {
|
||||
clear()
|
||||
return addAll(elements)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace [amount] elements at [startIndex] with [elements].
|
||||
*/
|
||||
fun <E> MutableList<E>.splice(startIndex: Int, amount: Int, elements: Iterable<E>) {
|
||||
repeat(amount) { removeAt(startIndex) }
|
||||
|
||||
var i = startIndex
|
||||
|
||||
for (element in elements) {
|
||||
add(i++, element)
|
||||
}
|
||||
}
|
@ -1,8 +1,33 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package world.phantasmal.core
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
external interface JsArray<T> {
|
||||
val length: Int
|
||||
|
||||
fun push(vararg elements: T): Int
|
||||
|
||||
fun slice(start: Int = definedExternally): JsArray<T>
|
||||
fun slice(start: Int, end: Int = definedExternally): JsArray<T>
|
||||
|
||||
fun splice(start: Int, deleteCount: Int = definedExternally): JsArray<T>
|
||||
fun splice(start: Int, deleteCount: Int, vararg items: T): JsArray<T>
|
||||
}
|
||||
|
||||
inline operator fun <T> JsArray<T>.get(index: Int): T = asDynamic()[index].unsafeCast<T>()
|
||||
|
||||
inline operator fun <T> JsArray<T>.set(index: Int, value: T) {
|
||||
asDynamic()[index] = value
|
||||
}
|
||||
|
||||
inline fun <T> jsArrayOf(vararg elements: T): JsArray<T> =
|
||||
elements.unsafeCast<JsArray<T>>()
|
||||
|
||||
inline fun <T> JsArray<T>.asArray(): Array<T> =
|
||||
unsafeCast<Array<T>>()
|
||||
|
||||
inline fun <T> Array<T>.asJsArray(): JsArray<T> =
|
||||
unsafeCast<JsArray<T>>()
|
||||
|
||||
inline fun <T> List<T>.toJsArray(): JsArray<T> =
|
||||
toTypedArray().asJsArray()
|
||||
|
@ -1,5 +0,0 @@
|
||||
package world.phantasmal.core
|
||||
|
||||
external interface JsArray<T> {
|
||||
fun push(vararg elements: T): Int
|
||||
}
|
@ -2,6 +2,7 @@ package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.core.replaceAll
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.value.AbstractVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
@ -61,8 +62,7 @@ class DependentListVal<E>(
|
||||
}
|
||||
|
||||
private fun recompute() {
|
||||
elements.clear()
|
||||
elements.addAll(computeElements())
|
||||
elements.replaceAll(computeElements())
|
||||
}
|
||||
|
||||
private fun initDependencyObservers() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.core.replaceAll
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.value.MutableVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
@ -64,15 +65,13 @@ class SimpleListVal<E>(
|
||||
|
||||
override fun replaceAll(elements: Iterable<E>) {
|
||||
val removed = ArrayList(this.elements)
|
||||
this.elements.clear()
|
||||
this.elements.addAll(elements)
|
||||
this.elements.replaceAll(elements)
|
||||
finalizeUpdate(ListValChangeEvent.Change(0, removed, this.elements))
|
||||
}
|
||||
|
||||
override fun replaceAll(elements: Sequence<E>) {
|
||||
val removed = ArrayList(this.elements)
|
||||
this.elements.clear()
|
||||
this.elements.addAll(elements)
|
||||
this.elements.replaceAll(elements)
|
||||
finalizeUpdate(ListValChangeEvent.Change(0, removed, this.elements))
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.web.questEditor.asm
|
||||
|
||||
import world.phantasmal.core.Success
|
||||
import world.phantasmal.core.*
|
||||
import world.phantasmal.lib.asm.*
|
||||
import world.phantasmal.lib.asm.dataFlowAnalysis.getMapDesignations
|
||||
import world.phantasmal.observable.value.Val
|
||||
@ -46,7 +46,7 @@ object AsmAnalyser {
|
||||
}
|
||||
|
||||
private var inlineStackArgs: Boolean = true
|
||||
private var asm: List<String> = emptyList()
|
||||
private val asm: JsArray<String> = jsArrayOf()
|
||||
private var _bytecodeIr = mutableVal(BytecodeIr(emptyList()))
|
||||
private var _mapDesignations = mutableVal<Map<Int, Int>>(emptyMap())
|
||||
private val _problems = mutableListVal<AssemblyProblem>()
|
||||
@ -57,13 +57,111 @@ object AsmAnalyser {
|
||||
|
||||
suspend fun setAsm(asm: List<String>, inlineStackArgs: Boolean) {
|
||||
this.inlineStackArgs = inlineStackArgs
|
||||
this.asm = asm
|
||||
this.asm.splice(0, this.asm.length, *asm.toTypedArray())
|
||||
|
||||
processAsm()
|
||||
}
|
||||
|
||||
suspend fun updateAssembly(changes: List<AsmChange>) {
|
||||
for (change in changes) {
|
||||
val (startLineNo, startCol, endLineNo, endCol) = change.range
|
||||
val linesChanged = endLineNo - startLineNo + 1
|
||||
val newLines = change.newAsm.split("\n").toJsArray()
|
||||
|
||||
when {
|
||||
linesChanged == 1 -> {
|
||||
replaceLinePart(startLineNo, startCol, endCol, newLines)
|
||||
}
|
||||
|
||||
newLines.length == 1 -> {
|
||||
replaceLinesAndMergeLineParts(
|
||||
startLineNo,
|
||||
endLineNo,
|
||||
startCol,
|
||||
endCol,
|
||||
newLines[0],
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Keep the left part of the first changed line.
|
||||
replaceLinePartRight(startLineNo, startCol, newLines[0])
|
||||
|
||||
// Keep the right part of the last changed line.
|
||||
replaceLinePartLeft(endLineNo, endCol, newLines[newLines.length - 1])
|
||||
|
||||
// Replace all the lines in between.
|
||||
// It's important that we do this last.
|
||||
replaceLines(
|
||||
startLineNo + 1,
|
||||
endLineNo - 1,
|
||||
newLines.slice(1, newLines.length - 1),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processAsm()
|
||||
}
|
||||
|
||||
private fun replaceLinePart(
|
||||
lineNo: Int,
|
||||
startCol: Int,
|
||||
endCol: Int,
|
||||
newLineParts: JsArray<String>,
|
||||
) {
|
||||
val line = asm[lineNo - 1]
|
||||
// We keep the parts of the line that weren't affected by the edit.
|
||||
val lineStart = line.substring(0, startCol - 1)
|
||||
val lineEnd = line.substring(endCol - 1)
|
||||
|
||||
if (newLineParts.length == 1) {
|
||||
asm[lineNo - 1] = lineStart + newLineParts[0] + lineEnd
|
||||
} else {
|
||||
asm.splice(
|
||||
lineNo - 1,
|
||||
1,
|
||||
lineStart + newLineParts[0],
|
||||
*newLineParts.slice(1, newLineParts.length - 1).asArray(),
|
||||
newLineParts[newLineParts.length - 1] + lineEnd,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceLinePartLeft(lineNo: Int, endCol: Int, newLinePart: String) {
|
||||
asm[lineNo - 1] = newLinePart + asm[lineNo - 1].substring(endCol - 1)
|
||||
}
|
||||
|
||||
private fun replaceLinePartRight(lineNo: Int, startCol: Int, newLinePart: String) {
|
||||
asm[lineNo - 1] = asm[lineNo - 1].substring(0, startCol - 1) + newLinePart
|
||||
}
|
||||
|
||||
private fun replaceLines(startLineNo: Int, endLineNo: Int, newLines: JsArray<String>) {
|
||||
asm.splice(startLineNo - 1, endLineNo - startLineNo + 1, *newLines.asArray())
|
||||
}
|
||||
|
||||
private fun replaceLinesAndMergeLineParts(
|
||||
startLineNo: Int,
|
||||
endLineNo: Int,
|
||||
startCol: Int,
|
||||
endCol: Int,
|
||||
newLinePart: String,
|
||||
) {
|
||||
val startLine = asm[startLineNo - 1]
|
||||
val endLine = asm[endLineNo - 1]
|
||||
// We keep the parts of the lines that weren't affected by the edit.
|
||||
val startLineStart = startLine.substring(0, startCol - 1)
|
||||
val endLineEnd = endLine.substring(endCol - 1)
|
||||
|
||||
asm.splice(
|
||||
startLineNo - 1,
|
||||
endLineNo - startLineNo + 1,
|
||||
startLineStart + newLinePart + endLineEnd,
|
||||
)
|
||||
}
|
||||
|
||||
private fun processAsm() {
|
||||
val assemblyResult = assemble(asm, inlineStackArgs)
|
||||
val assemblyResult = assemble(asm.asArray().toList(), inlineStackArgs)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
_problems.value = assemblyResult.problems as List<AssemblyProblem>
|
||||
@ -220,7 +318,7 @@ object AsmAnalyser {
|
||||
return Hover(contents)
|
||||
}
|
||||
|
||||
suspend fun getDefinition(lineNo: Int, col: Int): List<TextRange> {
|
||||
suspend fun getDefinition(lineNo: Int, col: Int): List<AsmRange> {
|
||||
getInstruction(lineNo, col)?.let { inst ->
|
||||
for ((paramIdx, param) in inst.opcode.params.withIndex()) {
|
||||
if (param.type is LabelType) {
|
||||
@ -290,14 +388,14 @@ object AsmAnalyser {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getLabelDefinitions(label: Int): List<TextRange> =
|
||||
private fun getLabelDefinitions(label: Int): List<AsmRange> =
|
||||
bytecodeIr.value.segments.asSequence()
|
||||
.filter { label in it.labels }
|
||||
.mapNotNull { segment ->
|
||||
val labelIdx = segment.labels.indexOf(label)
|
||||
|
||||
segment.srcLoc.labels.getOrNull(labelIdx)?.let { labelSrcLoc ->
|
||||
TextRange(
|
||||
AsmRange(
|
||||
startLineNo = labelSrcLoc.lineNo,
|
||||
startCol = labelSrcLoc.col,
|
||||
endLineNo = labelSrcLoc.lineNo,
|
||||
@ -314,5 +412,6 @@ object AsmAnalyser {
|
||||
lineNo == srcLoc.lineNo && col >= srcLoc.col && col <= srcLoc.col + srcLoc.len
|
||||
}
|
||||
|
||||
private fun getLine(lineNo: Int): String? = asm.getOrNull(lineNo - 1)
|
||||
@Suppress("RedundantNullableReturnType") // Can return undefined.
|
||||
private fun getLine(lineNo: Int): String? = asm[lineNo - 1]
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package world.phantasmal.web.questEditor.asm
|
||||
|
||||
class TextRange(
|
||||
var startLineNo: Int,
|
||||
var startCol: Int,
|
||||
var endLineNo: Int,
|
||||
var endCol: Int,
|
||||
data class AsmRange(
|
||||
val startLineNo: Int,
|
||||
val startCol: Int,
|
||||
val endLineNo: Int,
|
||||
val endCol: Int,
|
||||
)
|
||||
|
||||
enum class CompletionItemType {
|
||||
@ -35,3 +35,8 @@ class Hover(
|
||||
*/
|
||||
val contents: List<String>,
|
||||
)
|
||||
|
||||
class AsmChange(
|
||||
val range: AsmRange,
|
||||
val newAsm: String,
|
||||
)
|
||||
|
@ -1,7 +1,10 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.value.*
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.not
|
||||
import world.phantasmal.observable.value.or
|
||||
import world.phantasmal.observable.value.orElse
|
||||
import world.phantasmal.web.externals.monacoEditor.ITextModel
|
||||
import world.phantasmal.web.externals.monacoEditor.createModel
|
||||
import world.phantasmal.web.questEditor.stores.AsmStore
|
||||
@ -17,19 +20,24 @@ class AsmController(private val store: AsmStore) : Controller() {
|
||||
val didRedo: Observable<Unit> = store.didRedo
|
||||
|
||||
val inlineStackArgs: Val<Boolean> = store.inlineStackArgs
|
||||
val inlineStackArgsEnabled: Val<Boolean> = falseVal() // TODO
|
||||
val inlineStackArgsEnabled: Val<Boolean> = store.problems.map { it.isEmpty() }
|
||||
val inlineStackArgsTooltip: Val<String> =
|
||||
inlineStackArgsEnabled.map { enabled ->
|
||||
buildString {
|
||||
append("Transform arg_push* opcodes to be inline with the opcode the arguments are given to.")
|
||||
|
||||
// TODO: Notify user when disabled because of issues with the ASM.
|
||||
val inlineStackArgsTooltip: Val<String> = value(
|
||||
"Transform arg_push* opcodes to be inline with the opcode the arguments are given to."
|
||||
)
|
||||
if (!enabled) {
|
||||
append("\nThis mode cannot be toggled because there are issues in the script.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun makeUndoCurrent() {
|
||||
store.makeUndoCurrent()
|
||||
}
|
||||
|
||||
fun setInlineStackArgs(value: Boolean) {
|
||||
TODO()
|
||||
fun setInlineStackArgs(inline: Boolean) {
|
||||
store.setInlineStackArgs(inline)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1,13 +1,16 @@
|
||||
package world.phantasmal.web.questEditor.stores
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.lib.asm.AssemblyProblem
|
||||
import world.phantasmal.lib.asm.disassemble
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.emitter
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.value.trueVal
|
||||
import world.phantasmal.web.core.undo.SimpleUndo
|
||||
import world.phantasmal.web.core.undo.UndoManager
|
||||
import world.phantasmal.web.externals.monacoEditor.*
|
||||
@ -19,8 +22,15 @@ class AsmStore(
|
||||
questEditorStore: QuestEditorStore,
|
||||
private val undoManager: UndoManager,
|
||||
) : Store() {
|
||||
private val _inlineStackArgs = mutableVal(true)
|
||||
private var _textModel = mutableVal<ITextModel?>(null)
|
||||
|
||||
/**
|
||||
* Contains all model-related disposables. All contained disposables are disposed whenever a new
|
||||
* model is created.
|
||||
*/
|
||||
private val modelDisposer = addDisposable(Disposer())
|
||||
|
||||
private val _didUndo = emitter<Unit>()
|
||||
private val _didRedo = emitter<Unit>()
|
||||
private val undo = SimpleUndo(
|
||||
@ -30,7 +40,7 @@ class AsmStore(
|
||||
{ _didRedo.emit(ChangeEvent(Unit)) },
|
||||
)
|
||||
|
||||
val inlineStackArgs: Val<Boolean> = trueVal()
|
||||
val inlineStackArgs: Val<Boolean> = _inlineStackArgs
|
||||
|
||||
val textModel: Val<ITextModel?> = _textModel
|
||||
|
||||
@ -39,17 +49,38 @@ class AsmStore(
|
||||
val didUndo: Observable<Unit> = _didUndo
|
||||
val didRedo: Observable<Unit> = _didRedo
|
||||
|
||||
val problems: ListVal<AssemblyProblem> = AsmAnalyser.problems
|
||||
|
||||
init {
|
||||
observe(questEditorStore.currentQuest, inlineStackArgs) { quest, inlineStackArgs ->
|
||||
_textModel.value?.dispose()
|
||||
modelDisposer.disposeAll()
|
||||
|
||||
quest?.let {
|
||||
val asm = disassemble(quest.bytecodeIr, inlineStackArgs)
|
||||
scope.launch { AsmAnalyser.setAsm(asm, inlineStackArgs) }
|
||||
|
||||
_textModel.value =
|
||||
createModel(asm.joinToString("\n"), ASM_LANG_ID)
|
||||
.also(::addModelChangeListener)
|
||||
_textModel.value = createModel(asm.joinToString("\n"), ASM_LANG_ID).also { model ->
|
||||
modelDisposer.add(disposable { model.dispose() })
|
||||
|
||||
setupUndoRedo(model)
|
||||
|
||||
model.onDidChangeContent { e ->
|
||||
scope.launch {
|
||||
AsmAnalyser.updateAssembly(e.changes.map {
|
||||
AsmChange(
|
||||
AsmRange(
|
||||
it.range.startLineNumber,
|
||||
it.range.startColumn,
|
||||
it.range.endLineNumber,
|
||||
it.range.endColumn,
|
||||
),
|
||||
it.text,
|
||||
)
|
||||
})
|
||||
}
|
||||
// TODO: Update breakpoints.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,10 +97,11 @@ class AsmStore(
|
||||
undoManager.setCurrent(undo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up undo/redo, code analysis and breakpoint updates on model change.
|
||||
*/
|
||||
private fun addModelChangeListener(model: ITextModel) {
|
||||
fun setInlineStackArgs(inline: Boolean) {
|
||||
_inlineStackArgs.value = inline
|
||||
}
|
||||
|
||||
private fun setupUndoRedo(model: ITextModel) {
|
||||
val initialVersion = model.getAlternativeVersionId()
|
||||
var currentVersion = initialVersion
|
||||
var lastVersion = initialVersion
|
||||
@ -102,8 +134,6 @@ class AsmStore(
|
||||
}
|
||||
|
||||
currentVersion = version
|
||||
|
||||
// TODO: Code analysis and breakpoint update.
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user