diff --git a/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt b/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt index 5b513d39..8273df98 100644 --- a/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt +++ b/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt @@ -1,3 +1,5 @@ package world.phantasmal.core fun Char.isDigit(): Boolean = this in '0'..'9' + +expect fun Int.reinterpretAsFloat(): Float diff --git a/core/src/jsMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt b/core/src/jsMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt new file mode 100644 index 00000000..5b05bcb6 --- /dev/null +++ b/core/src/jsMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt @@ -0,0 +1,11 @@ +package world.phantasmal.core + +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.DataView + +private val dataView = DataView(ArrayBuffer(4)) + +actual fun Int.reinterpretAsFloat(): Float { + dataView.setInt32(0, this) + return dataView.getFloat32(0) +} diff --git a/core/src/jvmMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt b/core/src/jvmMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt new file mode 100644 index 00000000..9a0be331 --- /dev/null +++ b/core/src/jvmMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt @@ -0,0 +1,5 @@ +package world.phantasmal.core + +import java.lang.Float.intBitsToFloat + +actual fun Int.reinterpretAsFloat(): Float = intBitsToFloat(this) diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Assembly.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Assembly.kt index 14f58793..99b5a73c 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Assembly.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Assembly.kt @@ -20,11 +20,15 @@ class AssemblyProblem( fun assemble( assembly: List, - manualStack: Boolean = false, + inlineStackArgs: Boolean = true, ): PwResult> { - logger.trace { "Assembly start." } + logger.trace { + "Assembling ${assembly.size} lines with ${ + if (inlineStackArgs) "inline stack arguments" else "stack push instructions" + }." + } - val result = Assembler(assembly, manualStack).assemble() + val result = Assembler(assembly, inlineStackArgs).assemble() logger.trace { val warnings = result.problems.count { it.severity == Severity.Warning } @@ -36,7 +40,7 @@ fun assemble( return result } -private class Assembler(private val assembly: List, private val manualStack: Boolean) { +private class Assembler(private val assembly: List, private val inlineStackArgs: Boolean) { private var lineNo = 1 private lateinit var tokens: MutableList private var ir: MutableList = mutableListOf() @@ -355,7 +359,7 @@ private class Assembler(private val assembly: List, private val manualSt } val paramCount = - if (manualStack && opcode.stack == StackInteraction.Pop) 0 + if (!inlineStackArgs && opcode.stack == StackInteraction.Pop) 0 else opcode.params.size val argCount = tokens.count { it !is ArgSeparatorToken } diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Disassembly.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Disassembly.kt new file mode 100644 index 00000000..88fe1b98 --- /dev/null +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Disassembly.kt @@ -0,0 +1,310 @@ +package world.phantasmal.lib.assembly + +import mu.KotlinLogging +import world.phantasmal.core.reinterpretAsFloat +import kotlin.math.min + +private val logger = KotlinLogging.logger {} + +private const val INDENT_WIDTH = 4 +private val INDENT = " ".repeat(INDENT_WIDTH) + +/** + * @param inlineStackArgs If true, will output stack arguments inline instead of outputting stack + * management instructions (argpush variants). + */ +fun disassemble(byteCodeIr: List, inlineStackArgs: Boolean = true): List { + logger.trace { + "Disassembling ${byteCodeIr.size} segments with ${ + if (inlineStackArgs) "inline stack arguments" else "stack push instructions" + }." + } + + val lines = mutableListOf() + val stack = mutableListOf() + var sectionType: SegmentType? = null + + for (segment in byteCodeIr) { + // Section marker (.code, .data or .string). + if (sectionType != segment.type) { + sectionType = segment.type + + if (lines.isNotEmpty()) { + lines.add("") + } + + val sectionMarker = when (segment) { + is InstructionSegment -> ".code" + is DataSegment -> ".data" + is StringSegment -> ".string" + } + + lines.add(sectionMarker) + lines.add("") + } + + // Labels. + for (label in segment.labels) { + lines.add("$label:") + } + + // Code or data lines. + when (segment) { + is InstructionSegment -> { + var inVaList = false + + segment.instructions.forEachIndexed { i, instruction -> + val opcode = instruction.opcode + + if (opcode.code == OP_VA_START.code) { + inVaList = true + } else if (opcode.code == OP_VA_END.code) { + inVaList = false + } + + if (inlineStackArgs && + !inVaList && + opcode.stack == StackInteraction.Push && + canInlinePushedArg(segment, i) + ) { + stack.addAll(addTypeToArgs(opcode.params, instruction.args)) + } else { + val sb = StringBuilder(INDENT) + sb.append(opcode.mnemonic) + + if (opcode.stack == StackInteraction.Pop) { + if (inlineStackArgs) { + sb.appendArgs( + opcode.params, + stack.takeLast(opcode.params.size), + stack = true, + ) + } + } else { + sb.appendArgs( + opcode.params, + addTypeToArgs(opcode.params, instruction.args), + stack = false + ) + } + + if (opcode.stack != StackInteraction.Push) { + stack.clear() + } + + lines.add(sb.toString()) + } + } + } + + is DataSegment -> { + val sb = StringBuilder(INDENT) + + for (i in 0 until segment.data.size) { + sb.append("0x") + sb.append(segment.data.getUByte(i).toString(16).padStart(2, '0')) + + when { + // Last line. + i == segment.data.size - 1 -> { + lines.add(sb.toString()) + } + // Start a new line after every 16 bytes. + i % 16 == 15 -> { + lines.add(sb.toString()) + sb.setLength(0) + sb.append(INDENT) + } + // Add a space between each byte. + else -> { + sb.append(" ") + } + } + } + } + + is StringSegment -> { + lines.add(StringBuilder(INDENT).appendStringSegment(segment.value).toString()) + } + } + } + + // Ensure newline at the end. + lines.add("") + + logger.trace { "Disassembly finished, line count: ${lines.size}." } + + return lines +} + +private data class ArgWithType(val arg: Arg, val type: AnyType) + +private fun canInlinePushedArg(segment: InstructionSegment, index: Int): Boolean { + var pushedArgCount = 0 + + for (i in index until segment.instructions.size) { + val opcode = segment.instructions[i].opcode + + when (opcode.stack) { + StackInteraction.Push -> pushedArgCount++ + + StackInteraction.Pop -> { + var paramCount = 0 + var varArgs = false + + for (param in opcode.params) { + when (param.type) { + is ILabelVarType -> varArgs = true + is RegRefVarType -> varArgs = true + else -> paramCount++ + } + } + + return pushedArgCount <= paramCount || (pushedArgCount > paramCount && varArgs) + } + + null -> return false + } + } + + return false +} + +private fun addTypeToArgs(params: List, args: List): List { + val argsWithType = mutableListOf() + + for (i in 0 until min(params.size, args.size)) { + argsWithType.add(ArgWithType(args[i], params[i].type)) + } + + // Deal with varargs. + val lastParam = params.lastOrNull() + + if ( + lastParam != null && + (lastParam.type == ILabelVarType || lastParam.type == RegRefVarType) + ) { + for (i in argsWithType.size until args.size) { + argsWithType.add(ArgWithType(args[i], lastParam.type)) + } + } + + return argsWithType +} + +private fun StringBuilder.appendArgs(params: List, args: List, stack: Boolean) { + var i = 0 + + while (i < params.size) { + val paramType = params[i].type + + if (i == 0) { + append(" ") + } else { + append(", ") + } + + if (i < args.size) { + val (arg, argType) = args[i] + + if (argType is RegTupRefType) { + append("r") + append(arg.value) + } else { + when (paramType) { + FloatType -> { + // Floats are pushed onto the stack as integers with arg_pushl. + if (stack) { + append((arg.value as Int).reinterpretAsFloat()) + } else { + append(arg.value) + } + } + + ILabelVarType -> { + while (i < args.size) { + append(args[i].arg.value) + if (i < args.lastIndex) append(", ") + i++ + } + } + + RegRefVarType -> { + while (i < args.size) { + append("r") + append(args[i].arg.value) + if (i < args.lastIndex) append(", ") + i++ + } + } + + RegRefType, + is RegTupRefType, + -> { + append("r") + append(arg.value) + } + + StringType -> { + appendStringArg(arg.value as String) + } + + else -> { + append(arg.value) + } + } + } + } + + i++ + } +} + +private fun StringBuilder.appendStringArg(value: String) { + append("\"") + + for (char in value) { + when (char) { + '\r' -> append("\\r") + '\n' -> append("\\n") + '\t' -> append("\\t") + '"' -> append("\\\"") + else -> append(char) + } + } + + append("\"") +} + +private fun StringBuilder.appendStringSegment(value: String) { + append("\"") + + var i = 0 + + while (i < value.length) { + when (val char = value[i]) { + // Replace with \n. + '<' -> { + if (i + 3 < value.length && + value[i + 1] == 'c' && + value[i + 2] == 'r' && + value[i + 3] == '>' + ) { + append("\\n") + i += 3 + } else { + append(char) + } + } + '\r' -> append("\\r") + '\n' -> append("\\n") + '\t' -> append("\\t") + '"' -> append("\\\"") + else -> append(char) + } + + i++ + } + + append("\"") +} diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Instructions.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Instructions.kt index d2aee7bd..f97587dc 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Instructions.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Instructions.kt @@ -58,11 +58,12 @@ fun instructionSize(instruction: Instruction, dcGcFormat: Boolean): Int { is RegTupRefType, -> 1 + // Ensure this case is before the LabelType case because ILabelVarType extends + // LabelType. + is ILabelVarType -> 1 + 2 * args.size + is ShortType, is LabelType, - is ILabelType, - is DLabelType, - is SLabelType, -> 2 is IntType, @@ -77,8 +78,6 @@ fun instructionSize(instruction: Instruction, dcGcFormat: Boolean): Int { } } - is ILabelVarType -> 1 + 2 * args.size - is RegRefVarType -> 1 + args.size else -> error("Parameter type ${type::class} not implemented.") diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Opcode.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Opcode.kt index 9b5235f8..6ce6008f 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Opcode.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/Opcode.kt @@ -62,7 +62,7 @@ object DLabelType : LabelType() object SLabelType : LabelType() /** - * Arbitrary amount of instruction labels. + * Arbitrary amount of instruction labels (variadic arguments). */ object ILabelVarType : LabelType() @@ -88,7 +88,7 @@ object RegRefType : RefType() class RegTupRefType(val registerTuple: List) : RefType() /** - * Arbitrary amount of register references. + * Arbitrary amount of register references (variadic arguments). */ object RegRefVarType : RefType() diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/dataFlowAnalysis/ControlFlowGraph.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/dataFlowAnalysis/ControlFlowGraph.kt index e3951e76..c815fffc 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/dataFlowAnalysis/ControlFlowGraph.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/assembly/dataFlowAnalysis/ControlFlowGraph.kt @@ -5,10 +5,30 @@ import world.phantasmal.lib.assembly.* // See https://en.wikipedia.org/wiki/Control-flow_graph. enum class BranchType { + /** + * Only encountered when the last segment of a script has no jump or return. + */ None, + + /** + * ret + */ Return, + + /** + * jmp or switch_jmp. switch_jmp is a non-conditional jump because it always jumps even though + * the jump location is dynamic. + */ Jump, + + /** + * Every other jump instruction. + */ ConditionalJump, + + /** + * call, switch_call or va_call. + */ Call, } @@ -180,7 +200,7 @@ private fun createBasicBlocks(cfg: ControlFlowGraphBuilder, segment: Instruction branchLabels = listOf(inst.args[2].value as Int) } OP_SWITCH_JMP.code -> { - branchType = BranchType.ConditionalJump + branchType = BranchType.Jump branchLabels = inst.args.drop(1).map { it.value as Int } } @@ -248,7 +268,7 @@ private fun linkBlocks(cfg: ControlFlowGraphBuilder) { BranchType.ConditionalJump, -> nextBlock?.let(block::linkTo) - else -> { + BranchType.Jump -> { // Ignore. } } diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/ByteCode.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/ByteCode.kt index 9e475b55..c7b0f8f7 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/ByteCode.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/ByteCode.kt @@ -411,7 +411,7 @@ private fun parseInstructionsSegment( for (i in instructions.size - 1 downTo 0) { val opcode = instructions[i].opcode.code - if (opcode == OP_RET.code || opcode == OP_JMP.code) { + if (opcode == OP_RET.code || opcode == OP_JMP.code || opcode == OP_SWITCH_JMP.code) { dropThrough = false break } @@ -506,11 +506,7 @@ private fun parseInstructionArguments( args.addAll(cursor.uShortArray(argSize.toInt()).map { Arg(it.toInt()) }) } - is LabelType, - is ILabelType, - is DLabelType, - is SLabelType, - -> { + is LabelType -> { args.add(Arg(cursor.uShort().toInt())) } diff --git a/lib/src/commonTest/kotlin/world/phantasmal/lib/assembly/AssemblyTokenizationTests.kt b/lib/src/commonTest/kotlin/world/phantasmal/lib/assembly/AssemblyTokenizationTests.kt new file mode 100644 index 00000000..6daf1cd0 --- /dev/null +++ b/lib/src/commonTest/kotlin/world/phantasmal/lib/assembly/AssemblyTokenizationTests.kt @@ -0,0 +1,47 @@ +package world.phantasmal.lib.assembly + +import world.phantasmal.lib.test.LibTestSuite +import world.phantasmal.testUtils.assertCloseTo +import kotlin.test.Test +import kotlin.test.assertEquals + +class AssemblyTokenizationTests : LibTestSuite() { + @Test + fun valid_floats_are_parsed_as_FloatTokens() { + assertCloseTo(808.9f, (tokenizeLine("808.9")[0] as FloatToken).value) + assertCloseTo(-0.9f, (tokenizeLine("-0.9")[0] as FloatToken).value) + assertCloseTo(0.001f, (tokenizeLine("1e-3")[0] as FloatToken).value) + assertCloseTo(-600.0f, (tokenizeLine("-6e2")[0] as FloatToken).value) + } + + @Test + fun invalid_floats_area_parsed_as_InvalidNumberTokens_or_InvalidSectionTokens() { + val tokens1 = tokenizeLine(" 808.9a ") + + assertEquals(1, tokens1.size) + assertEquals(InvalidNumberToken::class, tokens1[0]::class) + assertEquals(2, tokens1[0].col) + assertEquals(6, tokens1[0].len) + + val tokens2 = tokenizeLine(" -55e ") + + assertEquals(1, tokens2.size) + assertEquals(InvalidNumberToken::class, tokens2[0]::class) + assertEquals(3, tokens2[0].col) + assertEquals(4, tokens2[0].len) + + val tokens3 = tokenizeLine(".7429") + + assertEquals(1, tokens3.size) + assertEquals(InvalidSectionToken::class, tokens3[0]::class) + assertEquals(1, tokens3[0].col) + assertEquals(5, tokens3[0].len) + + val tokens4 = tokenizeLine("\t\t\t4. test") + + assertEquals(2, tokens4.size) + assertEquals(InvalidNumberToken::class, tokens4[0]::class) + assertEquals(4, tokens4[0].col) + assertEquals(2, tokens4[0].len) + } +} diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValExtensions.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValExtensions.kt index e89ac9ff..21775b84 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValExtensions.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValExtensions.kt @@ -18,6 +18,9 @@ fun Val.isNull(): Val = fun Val.isNotNull(): Val = map { it != null } +fun Val.orElse(defaultValue: () -> T): Val = + map { it ?: defaultValue() } + infix fun > Val.gt(value: T): Val = map { it > value } diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/RegularValTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/RegularValTests.kt index 2e93430d..f9c67b82 100644 --- a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/RegularValTests.kt +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/RegularValTests.kt @@ -25,6 +25,9 @@ abstract class RegularValTests : ValTests() { // Test `isNotNull`. assertEquals(any != null, value.isNotNull().value) + + // Test `orElse`. + assertEquals(any ?: "default", value.orElse { "default" }.value) } listOf(10 to 10, 5 to 99, "a" to "a", "x" to "y").forEach { (a, b) -> val aVal = createWithValue(a) diff --git a/web/build.gradle.kts b/web/build.gradle.kts index f5493dc5..2eb00754 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -39,8 +39,11 @@ dependencies { implementation("io.ktor:ktor-client-core-js:$ktorVersion") implementation("io.ktor:ktor-client-serialization-js:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core-js:1.0.0") - implementation(npm("golden-layout", "1.5.9")) - implementation(npm("@babylonjs/core", "4.2.0-rc.5")) + implementation(npm("@babylonjs/core", "^4.2.0-rc.5")) + implementation(npm("golden-layout", "^1.5.9")) + implementation(npm("monaco-editor", "^0.21.2")) + + implementation(devNpm("file-loader", "^6.0.0")) testImplementation(kotlin("test-js")) testImplementation(project(":test-utils")) diff --git a/web/src/main/kotlin/world/phantasmal/web/core/widgets/DockWidget.kt b/web/src/main/kotlin/world/phantasmal/web/core/widgets/DockWidget.kt index dcf3b2cd..f3f16031 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/widgets/DockWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/widgets/DockWidget.kt @@ -51,8 +51,6 @@ class DockWidget( init { js("""require("golden-layout/src/css/goldenlayout-base.css");""") - - observeResize() } override fun Node.createElement() = @@ -94,11 +92,11 @@ class DockWidget( style.width = "" style.height = "" - } - override fun resized(width: Double, height: Double) { - goldenLayout.updateSize(width, height) - } + addDisposable(size.observe { (size) -> + goldenLayout.updateSize(size.width, size.height) + }) + } override fun internalDispose() { goldenLayout.destroy() @@ -155,6 +153,7 @@ class DockWidget( .pw-core-dock { width: 100%; height: 100%; + overflow: hidden; } #pw-root .lm_header { diff --git a/web/src/main/kotlin/world/phantasmal/web/core/widgets/RendererWidget.kt b/web/src/main/kotlin/world/phantasmal/web/core/widgets/RendererWidget.kt index fd5034f2..cb7d57dd 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/widgets/RendererWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/widgets/RendererWidget.kt @@ -19,8 +19,6 @@ class RendererWidget( className = "pw-core-renderer" tabIndex = -1 - observeResize() - observe(selfOrAncestorVisible) { visible -> if (visible) { renderer.startRendering() @@ -29,14 +27,14 @@ class RendererWidget( } } + addDisposable(size.observe { (size) -> + canvas.width = floor(size.width).toInt() + canvas.height = floor(size.height).toInt() + }) + append(canvas) } - override fun resized(width: Double, height: Double) { - canvas.width = floor(width).toInt() - canvas.height = floor(height).toInt() - } - companion object { init { @Suppress("CssUnusedSymbol") diff --git a/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt b/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt index 8b1a0036..71f82975 100644 --- a/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt +++ b/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt @@ -495,4 +495,11 @@ external class Color4( var g: Double var b: Double var a: Double + + companion object { + /** + * Creates a new Color4 from integer values (< 256) + */ + fun FromInts(r: Int, g: Int, b: Int, a: Int): Color4 + } } diff --git a/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/editor.kt b/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/editor.kt new file mode 100644 index 00000000..833cd8fc --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/editor.kt @@ -0,0 +1,769 @@ +@file:JsModule("monaco-editor") +@file:JsNonModule +@file:JsQualifier("editor") +@file:Suppress("unused", "PropertyName") + +package world.phantasmal.web.externals.monacoEditor + +import org.w3c.dom.HTMLElement +import org.w3c.dom.Range + +external fun create( + domElement: HTMLElement, + options: IStandaloneEditorConstructionOptions = definedExternally, +): IStandaloneCodeEditor + +external fun createModel( + value: String, + language: String = definedExternally, + uri: Uri = definedExternally, +): ITextModel + +external fun defineTheme(themeName: String, themeData: IStandaloneThemeData) + +external interface IStandaloneThemeData { + var base: String /* 'vs' | 'vs-dark' | 'hc-black' */ + var inherit: Boolean + var rules: Array + var encodedTokensColors: Array? + var colors: IColors +} + +external interface IColors + +external interface ITokenThemeRule { + var token: String + var foreground: String? + var background: String? + var fontStyle: String? +} + +external enum class ScrollType { + Smooth /* = 0 */, + Immediate /* = 1 */ +} + +external interface IDimension { + var width: Number + var height: Number +} + +external interface IEditor { + fun onDidDispose(listener: () -> Unit): IDisposable + fun dispose() + fun getId(): String + fun getEditorType(): String + fun updateOptions(newOptions: IEditorOptions) + fun layout(dimension: IDimension = definedExternally) + fun focus() + fun hasTextFocus(): Boolean + fun saveViewState(): dynamic /* ICodeEditorViewState? | IDiffEditorViewState? */ + fun getVisibleColumnFromPosition(position: IPosition): Number + fun getPosition(): Position? + fun setPosition(position: IPosition) + fun revealLine(lineNumber: Number, scrollType: ScrollType = definedExternally) + fun revealLineInCenter(lineNumber: Number, scrollType: ScrollType = definedExternally) + fun revealLineInCenterIfOutsideViewport( + lineNumber: Number, + scrollType: ScrollType = definedExternally, + ) + + fun revealLineNearTop(lineNumber: Number, scrollType: ScrollType = definedExternally) + fun revealPosition(position: IPosition, scrollType: ScrollType = definedExternally) + fun revealPositionInCenter(position: IPosition, scrollType: ScrollType = definedExternally) + fun revealPositionInCenterIfOutsideViewport( + position: IPosition, + scrollType: ScrollType = definedExternally, + ) + + fun revealPositionNearTop(position: IPosition, scrollType: ScrollType = definedExternally) + fun getSelection(): Selection? + fun getSelections(): Array? + fun setSelection(selection: IRange) + fun setSelection(selection: Range) + fun setSelection(selection: ISelection) + fun setSelection(selection: Selection) + fun setSelections(selections: Any) + fun revealLines( + startLineNumber: Number, + endLineNumber: Number, + scrollType: ScrollType = definedExternally, + ) + + fun revealLinesInCenter( + lineNumber: Number, + endLineNumber: Number, + scrollType: ScrollType = definedExternally, + ) + + fun revealLinesInCenterIfOutsideViewport( + lineNumber: Number, + endLineNumber: Number, + scrollType: ScrollType = definedExternally, + ) + + fun revealLinesNearTop( + lineNumber: Number, + endLineNumber: Number, + scrollType: ScrollType = definedExternally, + ) + + fun revealRange(range: IRange, scrollType: ScrollType = definedExternally) + fun revealRangeInCenter(range: IRange, scrollType: ScrollType = definedExternally) + fun revealRangeAtTop(range: IRange, scrollType: ScrollType = definedExternally) + fun revealRangeInCenterIfOutsideViewport( + range: IRange, + scrollType: ScrollType = definedExternally, + ) + + fun revealRangeNearTop(range: IRange, scrollType: ScrollType = definedExternally) + fun revealRangeNearTopIfOutsideViewport( + range: IRange, + scrollType: ScrollType = definedExternally, + ) + + fun trigger(source: String?, handlerId: String, payload: Any) + fun getModel(): dynamic /* ITextModel? | IDiffEditorModel? */ + fun setModel(model: ITextModel?) +} + +external interface ICodeEditor : IEditor { + fun onDidChangeModelContent(listener: (e: IModelContentChangedEvent) -> Unit): IDisposable + fun onDidChangeModelLanguage(listener: (e: IModelLanguageChangedEvent) -> Unit): IDisposable + fun onDidChangeModelLanguageConfiguration(listener: (e: IModelLanguageConfigurationChangedEvent) -> Unit): IDisposable + fun onDidChangeModelOptions(listener: (e: IModelOptionsChangedEvent) -> Unit): IDisposable + fun onDidChangeCursorPosition(listener: (e: ICursorPositionChangedEvent) -> Unit): IDisposable + fun onDidChangeCursorSelection(listener: (e: ICursorSelectionChangedEvent) -> Unit): IDisposable + fun onDidChangeModelDecorations(listener: (e: IModelDecorationsChangedEvent) -> Unit): IDisposable + fun onDidFocusEditorText(listener: () -> Unit): IDisposable + fun onDidBlurEditorText(listener: () -> Unit): IDisposable + fun onDidFocusEditorWidget(listener: () -> Unit): IDisposable + fun onDidBlurEditorWidget(listener: () -> Unit): IDisposable + fun onDidCompositionStart(listener: () -> Unit): IDisposable + fun onDidCompositionEnd(listener: () -> Unit): IDisposable + fun onDidAttemptReadOnlyEdit(listener: () -> Unit): IDisposable + fun hasWidgetFocus(): Boolean + override fun getModel(): ITextModel? + override fun setModel(model: ITextModel?) + fun getRawOptions(): IEditorOptions + fun setValue(newValue: String) + fun getContentWidth(): Number + fun getScrollWidth(): Number + fun getScrollLeft(): Number + fun getContentHeight(): Number + fun getScrollHeight(): Number + fun getScrollTop(): Number + fun pushUndoStop(): Boolean + fun executeEdits( + source: String?, + edits: Array, + endCursorState: ICursorStateComputer = definedExternally, + ): Boolean + + fun executeEdits( + source: String?, + edits: Array, + endCursorState: Array = definedExternally, + ): Boolean + + fun getLineDecorations(lineNumber: Number): Array? + fun deltaDecorations( + oldDecorations: Array, + newDecorations: Array, + ): Array + + fun getVisibleRanges(): Array + fun getTopForLineNumber(lineNumber: Number): Number + fun getTopForPosition(lineNumber: Number, column: Number): Number + fun getContainerDomNode(): HTMLElement + fun getDomNode(): HTMLElement? + fun getOffsetForColumn(lineNumber: Number, column: Number): Number + fun render(forceRedraw: Boolean = definedExternally) + fun applyFontInfo(target: HTMLElement) +} + +external interface IStandaloneCodeEditor : ICodeEditor { + override fun updateOptions(newOptions: IEditorOptions /* IEditorOptions & IGlobalEditorOptions */) +} + +external interface IGlobalEditorOptions { + var tabSize: Number? + var insertSpaces: Boolean? + var detectIndentation: Boolean? + var trimAutoWhitespace: Boolean? + var largeFileOptimizations: Boolean? + var wordBasedSuggestions: Boolean? + var stablePeek: Boolean? + var maxTokenizationLineLength: Number? + var theme: String? +} + +external interface IEditorScrollbarOptions { + var arrowSize: Number? + var vertical: String? /* 'auto' | 'visible' | 'hidden' */ + var horizontal: String? /* 'auto' | 'visible' | 'hidden' */ + var useShadows: Boolean? + var verticalHasArrows: Boolean? + var horizontalHasArrows: Boolean? + var handleMouseWheel: Boolean? + var alwaysConsumeMouseWheel: Boolean? + var horizontalScrollbarSize: Number? + var verticalScrollbarSize: Number? + var verticalSliderSize: Number? + var horizontalSliderSize: Number? +} + +external interface IEditorMinimapOptions { + var enabled: Boolean? + var side: String? /* 'right' | 'left' */ + var size: String? /* 'proportional' | 'fill' | 'fit' */ + var showSlider: String? /* 'always' | 'mouseover' */ + var renderCharacters: Boolean? + var maxColumn: Number? + var scale: Number? +} + +external interface IEditorFindOptions { + var cursorMoveOnType: Boolean? + var seedSearchStringFromSelection: Boolean? + var autoFindInSelection: String? /* 'never' | 'always' | 'multiline' */ + var addExtraSpaceOnTop: Boolean? + var loop: Boolean? +} + +external interface IEditorOptions { + var inDiffEditor: Boolean? + var ariaLabel: String? + var tabIndex: Number? + var rulers: Array? + var wordSeparators: String? + var selectionClipboard: Boolean? + var lineNumbers: dynamic /* String | String | String | String | ((lineNumber: Number) -> String)? */ + var cursorSurroundingLines: Number? + var cursorSurroundingLinesStyle: String? /* 'default' | 'all' */ + var renderFinalNewline: Boolean? + var unusualLineTerminators: String? /* 'off' | 'prompt' | 'auto' */ + var selectOnLineNumbers: Boolean? + var lineNumbersMinChars: Number? + var glyphMargin: Boolean? + var lineDecorationsWidth: dynamic /* Number? | String? */ + var revealHorizontalRightPadding: Number? + var roundedSelection: Boolean? + var extraEditorClassName: String? + var readOnly: Boolean? + var renameOnType: Boolean? + var renderValidationDecorations: String? /* 'editable' | 'on' | 'off' */ + var scrollbar: IEditorScrollbarOptions? + var minimap: IEditorMinimapOptions? + var find: IEditorFindOptions? + var fixedOverflowWidgets: Boolean? + var overviewRulerLanes: Number? + var overviewRulerBorder: Boolean? + var cursorBlinking: String? /* 'blink' | 'smooth' | 'phase' | 'expand' | 'solid' */ + var mouseWheelZoom: Boolean? + var mouseStyle: String? /* 'text' | 'default' | 'copy' */ + var cursorSmoothCaretAnimation: Boolean? + var cursorStyle: String? /* 'line' | 'block' | 'underline' | 'line-thin' | 'block-outline' | 'underline-thin' */ + var cursorWidth: Number? + var fontLigatures: dynamic /* Boolean? | String? */ + var disableLayerHinting: Boolean? + var disableMonospaceOptimizations: Boolean? + var hideCursorInOverviewRuler: Boolean? + var scrollBeyondLastLine: Boolean? + var scrollBeyondLastColumn: Number? + var smoothScrolling: Boolean? + var automaticLayout: Boolean? + var wordWrap: String? /* 'off' | 'on' | 'wordWrapColumn' | 'bounded' */ + var wordWrapColumn: Number? + var wordWrapMinified: Boolean? + var wrappingIndent: String? /* 'none' | 'same' | 'indent' | 'deepIndent' */ + var wrappingStrategy: String? /* 'simple' | 'advanced' */ + var wordWrapBreakBeforeCharacters: String? + var wordWrapBreakAfterCharacters: String? + var stopRenderingLineAfter: Number? + var links: Boolean? + var colorDecorators: Boolean? + var contextmenu: Boolean? + var mouseWheelScrollSensitivity: Number? + var fastScrollSensitivity: Number? + var scrollPredominantAxis: Boolean? + var columnSelection: Boolean? + var multiCursorModifier: String? /* 'ctrlCmd' | 'alt' */ + var multiCursorMergeOverlapping: Boolean? + var multiCursorPaste: String? /* 'spread' | 'full' */ + var accessibilitySupport: String? /* 'auto' | 'off' | 'on' */ + var accessibilityPageSize: Number? + var quickSuggestions: dynamic /* Boolean? | IQuickSuggestionsOptions? */ + var quickSuggestionsDelay: Number? + var autoClosingBrackets: String? /* 'always' | 'languageDefined' | 'beforeWhitespace' | 'never' */ + var autoClosingQuotes: String? /* 'always' | 'languageDefined' | 'beforeWhitespace' | 'never' */ + var autoClosingOvertype: String? /* 'always' | 'auto' | 'never' */ + var autoSurround: String? /* 'languageDefined' | 'quotes' | 'brackets' | 'never' */ + var autoIndent: String? /* 'none' | 'keep' | 'brackets' | 'advanced' | 'full' */ + var formatOnType: Boolean? + var formatOnPaste: Boolean? + var dragAndDrop: Boolean? + var suggestOnTriggerCharacters: Boolean? + var acceptSuggestionOnEnter: String? /* 'on' | 'smart' | 'off' */ + var acceptSuggestionOnCommitCharacter: Boolean? + var snippetSuggestions: String? /* 'top' | 'bottom' | 'inline' | 'none' */ + var emptySelectionClipboard: Boolean? + var copyWithSyntaxHighlighting: Boolean? + var suggestSelection: String? /* 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix' */ + var suggestFontSize: Number? + var suggestLineHeight: Number? + var tabCompletion: String? /* 'on' | 'off' | 'onlySnippets' */ + var selectionHighlight: Boolean? + var occurrencesHighlight: Boolean? + var codeLens: Boolean? + var codeActionsOnSaveTimeout: Number? + var folding: Boolean? + var foldingStrategy: String? /* 'auto' | 'indentation' */ + var foldingHighlight: Boolean? + var showFoldingControls: String? /* 'always' | 'mouseover' */ + var unfoldOnClickAfterEndOfLine: Boolean? + var matchBrackets: String? /* 'never' | 'near' | 'always' */ + var renderWhitespace: String? /* 'none' | 'boundary' | 'selection' | 'trailing' | 'all' */ + var renderControlCharacters: Boolean? + var renderIndentGuides: Boolean? + var highlightActiveIndentGuide: Boolean? + var renderLineHighlight: String? /* 'none' | 'gutter' | 'line' | 'all' */ + var renderLineHighlightOnlyWhenFocus: Boolean? + var useTabStops: Boolean? + var fontFamily: String? + var fontWeight: String? + var fontSize: Number? + var lineHeight: Number? + var letterSpacing: Number? + var showUnused: Boolean? + var peekWidgetDefaultFocus: String? /* 'tree' | 'editor' */ + var definitionLinkOpensInPeek: Boolean? + var showDeprecated: Boolean? +} + +external interface IEditorConstructionOptions : IEditorOptions { + var overflowWidgetsDomNode: HTMLElement? +} + +external interface IStandaloneEditorConstructionOptions : IEditorConstructionOptions, + IGlobalEditorOptions { + var model: ITextModel? + var value: String? + var language: String? + override var theme: String? + var accessibilityHelpUrl: String? +} + +external interface IMarker { + var owner: String + var resource: Uri + var severity: MarkerSeverity + var code: dynamic /* String? | `T$5`? */ + var message: String + var source: String? + var startLineNumber: Number + var startColumn: Number + var endLineNumber: Number + var endColumn: Number + var relatedInformation: Array? + var tags: Array? +} + +external interface IMarkerData { + var code: dynamic /* String? | `T$5`? */ + var severity: MarkerSeverity + var message: String + var source: String? + var startLineNumber: Number + var startColumn: Number + var endLineNumber: Number + var endColumn: Number + var relatedInformation: Array? + var tags: Array? +} + +external interface IRelatedInformation { + var resource: Uri + var message: String + var startLineNumber: Number + var startColumn: Number + var endLineNumber: Number + var endColumn: Number +} + +external interface IColorizerOptions { + var tabSize: Number? +} + +external interface IColorizerElementOptions : IColorizerOptions { + var theme: String? + var mimeType: String? +} + +external enum class ScrollbarVisibility { + Auto /* = 1 */, + Hidden /* = 2 */, + Visible /* = 3 */ +} + +external interface ThemeColor { + var id: String +} + +external enum class OverviewRulerLane { + Left /* = 1 */, + Center /* = 2 */, + Right /* = 4 */, + Full /* = 7 */ +} + +external enum class MinimapPosition { + Inline /* = 1 */, + Gutter /* = 2 */ +} + +external interface IDecorationOptions { + var color: dynamic /* String? | ThemeColor? */ + var darkColor: dynamic /* String? | ThemeColor? */ +} + +external interface IModelDecorationOverviewRulerOptions : IDecorationOptions { + var position: OverviewRulerLane +} + +external interface IModelDecorationMinimapOptions : IDecorationOptions { + var position: MinimapPosition +} + +external interface IModelDecorationOptions { + var stickiness: TrackedRangeStickiness? + var className: String? + var glyphMarginHoverMessage: dynamic /* IMarkdownString? | Array? */ + var hoverMessage: dynamic /* IMarkdownString? | Array? */ + var isWholeLine: Boolean? + var zIndex: Number? + var overviewRuler: IModelDecorationOverviewRulerOptions? + var minimap: IModelDecorationMinimapOptions? + var glyphMarginClassName: String? + var linesDecorationsClassName: String? + var firstLineDecorationClassName: String? + var marginClassName: String? + var inlineClassName: String? + var inlineClassNameAffectsLetterSpacing: Boolean? + var beforeContentClassName: String? + var afterContentClassName: String +} + +external interface IModelDeltaDecoration { + var range: IRange + var options: IModelDecorationOptions +} + +external interface IModelDecoration { + var id: String + var ownerId: Number + var range: Range + var options: IModelDecorationOptions +} + +external interface IWordAtPosition { + var word: String + var startColumn: Number + var endColumn: Number +} + +external enum class EndOfLinePreference { + TextDefined /* = 0 */, + LF /* = 1 */, + CRLF /* = 2 */ +} + +external enum class DefaultEndOfLine { + LF /* = 1 */, + CRLF /* = 2 */ +} + +external enum class EndOfLineSequence { + LF /* = 0 */, + CRLF /* = 1 */ +} + +external interface ISingleEditOperation { + var range: IRange + var text: String? + var forceMoveMarkers: Boolean? +} + +external interface IIdentifiedSingleEditOperation { + var range: IRange + var text: String? + var forceMoveMarkers: Boolean? +} + +external interface IValidEditOperation { + var range: Range + var text: String +} + +external interface ICursorStateComputer + +open external class TextModelResolvedOptions { + open var _textModelResolvedOptionsBrand: Unit + open var tabSize: Number + open var indentSize: Number + open var insertSpaces: Boolean + open var defaultEOL: DefaultEndOfLine + open var trimAutoWhitespace: Boolean +} + +external interface ITextModelUpdateOptions { + var tabSize: Number? + var indentSize: Number? + var insertSpaces: Boolean? + var trimAutoWhitespace: Boolean? +} + +open external class FindMatch { + open var _findMatchBrand: Unit + open var range: Range + open var matches: Array? +} + +external enum class TrackedRangeStickiness { + AlwaysGrowsWhenTypingAtEdges /* = 0 */, + NeverGrowsWhenTypingAtEdges /* = 1 */, + GrowsOnlyWhenTypingBefore /* = 2 */, + GrowsOnlyWhenTypingAfter /* = 3 */ +} + +external interface ITextModel { + var uri: Uri + var id: String + fun getOptions(): TextModelResolvedOptions + fun getVersionId(): Number + fun getAlternativeVersionId(): Number + fun setValue(newValue: String) + fun getValue( + eol: EndOfLinePreference = definedExternally, + preserveBOM: Boolean = definedExternally, + ): String + + fun getValueLength( + eol: EndOfLinePreference = definedExternally, + preserveBOM: Boolean = definedExternally, + ): Number + + fun getValueInRange(range: IRange, eol: EndOfLinePreference = definedExternally): String + fun getValueLengthInRange(range: IRange): Number + fun getCharacterCountInRange(range: IRange): Number + fun getLineCount(): Number + fun getLineContent(lineNumber: Number): String + fun getLineLength(lineNumber: Number): Number + fun getLinesContent(): Array + fun getEOL(): String + fun getLineMinColumn(lineNumber: Number): Number + fun getLineMaxColumn(lineNumber: Number): Number + fun getLineFirstNonWhitespaceColumn(lineNumber: Number): Number + fun getLineLastNonWhitespaceColumn(lineNumber: Number): Number + fun validatePosition(position: IPosition): Position + fun modifyPosition(position: IPosition, offset: Number): Position + fun validateRange(range: IRange): Range + fun getOffsetAt(position: IPosition): Number + fun getPositionAt(offset: Number): Position + fun getFullModelRange(): Range + fun isDisposed(): Boolean + fun findMatches( + searchString: String, + searchOnlyEditableRange: Boolean, + isRegex: Boolean, + matchCase: Boolean, + wordSeparators: String?, + captureMatches: Boolean, + limitResultCount: Number = definedExternally, + ): Array + + fun findMatches( + searchString: String, + searchScope: IRange, + isRegex: Boolean, + matchCase: Boolean, + wordSeparators: String?, + captureMatches: Boolean, + limitResultCount: Number = definedExternally, + ): Array + + fun findMatches( + searchString: String, + searchScope: Array, + isRegex: Boolean, + matchCase: Boolean, + wordSeparators: String?, + captureMatches: Boolean, + limitResultCount: Number = definedExternally, + ): Array + + fun findNextMatch( + searchString: String, + searchStart: IPosition, + isRegex: Boolean, + matchCase: Boolean, + wordSeparators: String?, + captureMatches: Boolean, + ): FindMatch? + + fun findPreviousMatch( + searchString: String, + searchStart: IPosition, + isRegex: Boolean, + matchCase: Boolean, + wordSeparators: String?, + captureMatches: Boolean, + ): FindMatch? + + fun getModeId(): String + fun getWordAtPosition(position: IPosition): IWordAtPosition? + fun getWordUntilPosition(position: IPosition): IWordAtPosition + fun deltaDecorations( + oldDecorations: Array, + newDecorations: Array, + ownerId: Number = definedExternally, + ): Array + + fun getDecorationOptions(id: String): IModelDecorationOptions? + fun getDecorationRange(id: String): Range? + fun getLineDecorations( + lineNumber: Number, + ownerId: Number = definedExternally, + filterOutValidation: Boolean = definedExternally, + ): Array + + fun getLinesDecorations( + startLineNumber: Number, + endLineNumber: Number, + ownerId: Number = definedExternally, + filterOutValidation: Boolean = definedExternally, + ): Array + + fun getDecorationsInRange( + range: IRange, + ownerId: Number = definedExternally, + filterOutValidation: Boolean = definedExternally, + ): Array + + fun getAllDecorations( + ownerId: Number = definedExternally, + filterOutValidation: Boolean = definedExternally, + ): Array + + fun getOverviewRulerDecorations( + ownerId: Number = definedExternally, + filterOutValidation: Boolean = definedExternally, + ): Array + + fun normalizeIndentation(str: String): String + fun updateOptions(newOpts: ITextModelUpdateOptions) + fun detectIndentation(defaultInsertSpaces: Boolean, defaultTabSize: Number) + fun pushStackElement() + fun pushEditOperations( + beforeCursorState: Array?, + editOperations: Array, + cursorStateComputer: ICursorStateComputer, + ): Array? + + fun pushEOL(eol: EndOfLineSequence) + fun applyEdits(operations: Array) + fun applyEdits( + operations: Array, + computeUndoEdits: Boolean, + ): dynamic /* Unit | Array */ + + fun setEOL(eol: EndOfLineSequence) + fun onDidChangeContent(listener: (e: IModelContentChangedEvent) -> Unit): IDisposable + fun onDidChangeDecorations(listener: (e: IModelDecorationsChangedEvent) -> Unit): IDisposable + fun onDidChangeOptions(listener: (e: IModelOptionsChangedEvent) -> Unit): IDisposable + fun onDidChangeLanguage(listener: (e: IModelLanguageChangedEvent) -> Unit): IDisposable + fun onDidChangeLanguageConfiguration(listener: (e: IModelLanguageConfigurationChangedEvent) -> Unit): IDisposable + fun onWillDispose(listener: () -> Unit): IDisposable + fun dispose() +} + +external object EditorType { + var ICodeEditor: String + var IDiffEditor: String +} + +external interface IModelLanguageChangedEvent { + var oldLanguage: String + var newLanguage: String +} + +external interface IModelLanguageConfigurationChangedEvent + +external interface IModelContentChange { + var range: IRange + var rangeOffset: Number + var rangeLength: Number + var text: String +} + +external interface IModelContentChangedEvent { + var changes: Array + var eol: String + var versionId: Number + var isUndoing: Boolean + var isRedoing: Boolean + var isFlush: Boolean +} + +external interface IModelDecorationsChangedEvent { + var affectsMinimap: Boolean + var affectsOverviewRuler: Boolean +} + +external interface IModelOptionsChangedEvent { + var tabSize: Boolean + var indentSize: Boolean + var insertSpaces: Boolean + var trimAutoWhitespace: Boolean +} + +external enum class CursorChangeReason { + NotSet /* = 0 */, + ContentFlush /* = 1 */, + RecoverFromMarkers /* = 2 */, + Explicit /* = 3 */, + Paste /* = 4 */, + Undo /* = 5 */, + Redo /* = 6 */ +} + +external interface ICursorPositionChangedEvent { + var position: Position + var secondaryPositions: Array + var reason: CursorChangeReason + var source: String +} + +external interface ICursorSelectionChangedEvent { + var selection: Selection + var secondarySelections: Array + var modelVersionId: Number + var oldSelections: Array? + var oldModelVersionId: Number + var source: String + var reason: CursorChangeReason +} + +external enum class AccessibilitySupport { + Unknown /* = 0 */, + Disabled /* = 1 */, + Enabled /* = 2 */ +} + +external enum class EditorAutoIndentStrategy { + None /* = 0 */, + Keep /* = 1 */, + Brackets /* = 2 */, + Advanced /* = 3 */, + Full /* = 4 */ +} diff --git a/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/editorExtensions.kt b/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/editorExtensions.kt new file mode 100644 index 00000000..766bc7fd --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/editorExtensions.kt @@ -0,0 +1,8 @@ +package world.phantasmal.web.externals.monacoEditor + +inline operator fun IColors.get(name: String): String = + asDynamic()[name].unsafeCast() + +inline operator fun IColors.set(name: String, value: String) { + asDynamic()[name] = value +} diff --git a/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/languages.kt b/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/languages.kt new file mode 100644 index 00000000..f681c20f --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/languages.kt @@ -0,0 +1,220 @@ +@file:JsModule("monaco-editor") +@file:JsNonModule +@file:JsQualifier("languages") + +package world.phantasmal.web.externals.monacoEditor + +import kotlin.js.RegExp + +external fun register(language: ILanguageExtensionPoint) + +external fun setLanguageConfiguration( + languageId: String, + configuration: LanguageConfiguration, +): IDisposable + +external fun setMonarchTokensProvider( + languageId: String, + languageDef: IMonarchLanguage, +): IDisposable + +external interface CommentRule { + var lineComment: String? + get() = definedExternally + set(value) = definedExternally + var blockComment: dynamic /* JsTuple */ + get() = definedExternally + set(value) = definedExternally +} + +external interface LanguageConfiguration { + var comments: CommentRule? + get() = definedExternally + set(value) = definedExternally + var brackets: Array */>? + get() = definedExternally + set(value) = definedExternally + var wordPattern: RegExp? + get() = definedExternally + set(value) = definedExternally + var indentationRules: IndentationRule? + get() = definedExternally + set(value) = definedExternally + var onEnterRules: Array? + get() = definedExternally + set(value) = definedExternally + var autoClosingPairs: Array? + get() = definedExternally + set(value) = definedExternally + var surroundingPairs: Array? + get() = definedExternally + set(value) = definedExternally + var autoCloseBefore: String? + get() = definedExternally + set(value) = definedExternally + var folding: FoldingRules? + get() = definedExternally + set(value) = definedExternally +} + +external interface IndentationRule { + var decreaseIndentPattern: RegExp + var increaseIndentPattern: RegExp + var indentNextLinePattern: RegExp? + get() = definedExternally + set(value) = definedExternally + var unIndentedLinePattern: RegExp? + get() = definedExternally + set(value) = definedExternally +} + +external interface FoldingMarkers { + var start: RegExp + var end: RegExp +} + +external interface FoldingRules { + var offSide: Boolean? + get() = definedExternally + set(value) = definedExternally + var markers: FoldingMarkers? + get() = definedExternally + set(value) = definedExternally +} + +external interface OnEnterRule { + var beforeText: RegExp + var afterText: RegExp? + get() = definedExternally + set(value) = definedExternally + var oneLineAboveText: RegExp? + get() = definedExternally + set(value) = definedExternally + var action: EnterAction +} + +external interface IDocComment { + var open: String + var close: String? + get() = definedExternally + set(value) = definedExternally +} + +external interface IAutoClosingPair { + var open: String + var close: String +} + +external interface IAutoClosingPairConditional : IAutoClosingPair { + var notIn: Array? + get() = definedExternally + set(value) = definedExternally +} + +external enum class IndentAction { + None /* = 0 */, + Indent /* = 1 */, + IndentOutdent /* = 2 */, + Outdent /* = 3 */ +} + +external interface EnterAction { + var indentAction: IndentAction + var appendText: String? + get() = definedExternally + set(value) = definedExternally + var removeText: Number? + get() = definedExternally + set(value) = definedExternally +} + +external interface ILanguageExtensionPoint { + var id: String + var extensions: Array? + get() = definedExternally + set(value) = definedExternally + var filenames: Array? + get() = definedExternally + set(value) = definedExternally + var filenamePatterns: Array? + get() = definedExternally + set(value) = definedExternally + var firstLine: String? + get() = definedExternally + set(value) = definedExternally + var aliases: Array? + get() = definedExternally + set(value) = definedExternally + var mimetypes: Array? + get() = definedExternally + set(value) = definedExternally + var configuration: Uri? + get() = definedExternally + set(value) = definedExternally +} + +external interface IMonarchLanguageTokenizer + +external interface IMonarchLanguage { + var tokenizer: IMonarchLanguageTokenizer + var ignoreCase: Boolean? + get() = definedExternally + set(value) = definedExternally + var unicode: Boolean? + get() = definedExternally + set(value) = definedExternally + var defaultToken: String? + get() = definedExternally + set(value) = definedExternally + var brackets: Array? + get() = definedExternally + set(value) = definedExternally + var start: String? + get() = definedExternally + set(value) = definedExternally + var tokenPostfix: String? + get() = definedExternally + set(value) = definedExternally +} + +external interface IExpandedMonarchLanguageRule { + var regex: RegExp + var action: IExpandedMonarchLanguageAction + var include: String +} + +external interface IExpandedMonarchLanguageAction { + var group: Array | Array */>? + get() = definedExternally + set(value) = definedExternally + var cases: Any? + get() = definedExternally + set(value) = definedExternally + var token: String? + get() = definedExternally + set(value) = definedExternally + var next: String? + get() = definedExternally + set(value) = definedExternally + var switchTo: String? + get() = definedExternally + set(value) = definedExternally + var goBack: Number? + get() = definedExternally + set(value) = definedExternally + var bracket: String? + get() = definedExternally + set(value) = definedExternally + var nextEmbedded: String? + get() = definedExternally + set(value) = definedExternally + var log: String? + get() = definedExternally + set(value) = definedExternally +} + +external interface IMonarchLanguageBracket { + var open: String + var close: String + var token: String +} diff --git a/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/languagesExtensions.kt b/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/languagesExtensions.kt new file mode 100644 index 00000000..4e12e6b1 --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/languagesExtensions.kt @@ -0,0 +1,10 @@ +package world.phantasmal.web.externals.monacoEditor + +typealias IMonarchLanguageRule = IExpandedMonarchLanguageRule + +inline operator fun IMonarchLanguageTokenizer.get(name: String): Array = + asDynamic()[name].unsafeCast>() + +inline operator fun IMonarchLanguageTokenizer.set(name: String, value: Array) { + asDynamic()[name] = value +} diff --git a/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/monacoEditor.kt b/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/monacoEditor.kt new file mode 100644 index 00000000..bd19012d --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/externals/monacoEditor/monacoEditor.kt @@ -0,0 +1,183 @@ +@file:JsModule("monaco-editor") +@file:JsNonModule +@file:Suppress("CovariantEquals", "unused") + +package world.phantasmal.web.externals.monacoEditor + +external interface IDisposable { + fun dispose() +} + +external enum class MarkerTag { + Unnecessary /* = 1 */, + Deprecated /* = 2 */ +} + +external enum class MarkerSeverity { + Hint /* = 1 */, + Info /* = 2 */, + Warning /* = 4 */, + Error /* = 8 */ +} + +external interface IRange { + var startLineNumber: Number + var startColumn: Number + var endLineNumber: Number + var endColumn: Number +} + +open external class Range( + startLineNumber: Number, + startColumn: Number, + endLineNumber: Number, + endColumn: Number, +) { + open var startLineNumber: Number + open var startColumn: Number + open var endLineNumber: Number + open var endColumn: Number + open fun isEmpty(): Boolean + open fun containsPosition(position: IPosition): Boolean + open fun containsRange(range: IRange): Boolean + open fun strictContainsRange(range: IRange): Boolean + open fun plusRange(range: IRange): Range + open fun intersectRanges(range: IRange): Range? + open fun equalsRange(other: IRange?): Boolean + open fun getEndPosition(): Position + open fun getStartPosition(): Position + override fun toString(): String + open fun setEndPosition(endLineNumber: Number, endColumn: Number): Range + open fun setStartPosition(startLineNumber: Number, startColumn: Number): Range + open fun collapseToStart(): Range + + companion object { + fun isEmpty(range: IRange): Boolean + fun containsPosition(range: IRange, position: IPosition): Boolean + fun containsRange(range: IRange, otherRange: IRange): Boolean + fun strictContainsRange(range: IRange, otherRange: IRange): Boolean + fun plusRange(a: IRange, b: IRange): Range + fun intersectRanges(a: IRange, b: IRange): Range? + fun equalsRange(a: IRange?, b: IRange?): Boolean + fun getEndPosition(range: IRange): Position + fun getStartPosition(range: IRange): Position + fun collapseToStart(range: IRange): Range + fun fromPositions(start: IPosition, end: IPosition = definedExternally): Range + fun lift(range: Nothing?): Nothing? + fun lift(range: IRange): Range + fun isIRange(obj: Any): Boolean + fun areIntersectingOrTouching(a: IRange, b: IRange): Boolean + fun areIntersecting(a: IRange, b: IRange): Boolean + fun compareRangesUsingStarts(a: IRange?, b: IRange?): Number + fun compareRangesUsingEnds(a: IRange, b: IRange): Number + fun spansMultipleLines(range: IRange): Boolean + } +} + +external interface ISelection { + var selectionStartLineNumber: Number + var selectionStartColumn: Number + var positionLineNumber: Number + var positionColumn: Number +} + +open external class Selection( + selectionStartLineNumber: Number, + selectionStartColumn: Number, + positionLineNumber: Number, + positionColumn: Number, +) : Range { + open var selectionStartLineNumber: Number + open var selectionStartColumn: Number + open var positionLineNumber: Number + open var positionColumn: Number + override fun toString(): String + open fun equalsSelection(other: ISelection): Boolean + open fun getDirection(): SelectionDirection + override fun setEndPosition(endLineNumber: Number, endColumn: Number): Selection + open fun getPosition(): Position + override fun setStartPosition(startLineNumber: Number, startColumn: Number): Selection + + companion object { + fun selectionsEqual(a: ISelection, b: ISelection): Boolean + fun fromPositions(start: IPosition, end: IPosition = definedExternally): Selection + fun liftSelection(sel: ISelection): Selection + fun selectionsArrEqual(a: Array, b: Array): Boolean + fun isISelection(obj: Any): Boolean + fun createWithDirection( + startLineNumber: Number, + startColumn: Number, + endLineNumber: Number, + endColumn: Number, + direction: SelectionDirection, + ): Selection + } +} + +external enum class SelectionDirection { + LTR /* = 0 */, + RTL /* = 1 */ +} + +external interface IPosition { + var lineNumber: Number + var column: Number +} + +open external class Position(lineNumber: Number, column: Number) { + open var lineNumber: Number + open var column: Number + open fun with( + newLineNumber: Number = definedExternally, + newColumn: Number = definedExternally, + ): Position + + open fun delta( + deltaLineNumber: Number = definedExternally, + deltaColumn: Number = definedExternally, + ): Position + + open fun equals(other: IPosition): Boolean + open fun isBefore(other: IPosition): Boolean + open fun isBeforeOrEqual(other: IPosition): Boolean + open fun clone(): Position + override fun toString(): String + + companion object { + fun equals(a: IPosition?, b: IPosition?): Boolean + fun isBefore(a: IPosition, b: IPosition): Boolean + fun isBeforeOrEqual(a: IPosition, b: IPosition): Boolean + fun compare(a: IPosition, b: IPosition): Number + fun lift(pos: IPosition): Position + fun isIPosition(obj: Any): Boolean + } +} + +external interface UriComponents { + var scheme: String + var authority: String + var path: String + var query: String + var fragment: String +} + +open external class Uri : UriComponents { + override var scheme: String + override var authority: String + override var path: String + override var query: String + override var fragment: String + open fun toString(skipEncoding: Boolean = definedExternally): String + open fun toJSON(): UriComponents + + companion object { + fun isUri(thing: Any): Boolean + fun parse(value: String, _strict: Boolean = definedExternally): Uri + fun file(path: String): Uri + fun joinPath(uri: Uri, vararg pathFragment: String): Uri + fun revive(data: UriComponents): Uri + fun revive(data: Uri): Uri + fun revive(data: UriComponents? = definedExternally): Uri? + fun revive(data: Uri? = definedExternally): Uri? + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/models/HuntMethodModel.kt b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/models/HuntMethodModel.kt index 7d83d4b4..ffe2364e 100644 --- a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/models/HuntMethodModel.kt +++ b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/models/HuntMethodModel.kt @@ -4,6 +4,7 @@ import world.phantasmal.lib.fileFormats.quest.Episode import world.phantasmal.lib.fileFormats.quest.NpcType import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.mutableVal +import world.phantasmal.observable.value.orElse import kotlin.time.Duration class HuntMethodModel( @@ -26,7 +27,7 @@ class HuntMethodModel( */ val userTime: Val = _userTime - val time: Val = userTime.map { it ?: defaultTime } + val time: Val = userTime.orElse { defaultTime } fun setUserTime(userTime: Duration?): HuntMethodModel { _userTime.value = userTime diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt index c669d98f..47b6b48b 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt @@ -8,6 +8,7 @@ import world.phantasmal.web.core.PwToolType import world.phantasmal.web.core.loading.AssetLoader import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.externals.babylon.Engine +import world.phantasmal.web.questEditor.controllers.AssemblyEditorController import world.phantasmal.web.questEditor.controllers.NpcCountsController import world.phantasmal.web.questEditor.controllers.QuestEditorToolbarController import world.phantasmal.web.questEditor.controllers.QuestInfoController @@ -18,6 +19,7 @@ import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager import world.phantasmal.web.questEditor.rendering.QuestRenderer import world.phantasmal.web.questEditor.rendering.UserInputManager import world.phantasmal.web.questEditor.stores.AreaStore +import world.phantasmal.web.questEditor.stores.AssemblyEditorStore import world.phantasmal.web.questEditor.stores.QuestEditorStore import world.phantasmal.web.questEditor.widgets.* import world.phantasmal.webui.DisposableContainer @@ -33,6 +35,7 @@ class QuestEditor( override fun initialize(scope: CoroutineScope): Widget { // Renderer val canvas = document.createElement("CANVAS") as HTMLCanvasElement + canvas.style.outline = "none" val renderer = addDisposable(QuestRenderer(canvas, createEngine(canvas))) // Asset Loaders @@ -43,6 +46,7 @@ class QuestEditor( // Stores val areaStore = addDisposable(AreaStore(scope, areaAssetLoader)) val questEditorStore = addDisposable(QuestEditorStore(scope, uiStore, areaStore)) + val assemblyEditorStore = addDisposable(AssemblyEditorStore(scope, questEditorStore)) // Controllers val toolbarController = addDisposable(QuestEditorToolbarController( @@ -52,6 +56,7 @@ class QuestEditor( )) val questInfoController = addDisposable(QuestInfoController(questEditorStore)) val npcCountsController = addDisposable(NpcCountsController(questEditorStore)) + val assemblyEditorController = addDisposable(AssemblyEditorController(assemblyEditorStore)) // Rendering addDisposables( @@ -71,7 +76,8 @@ class QuestEditor( { s -> QuestEditorToolbarWidget(s, toolbarController) }, { s -> QuestInfoWidget(s, questInfoController) }, { s -> NpcCountsWidget(s, npcCountsController) }, - { s -> QuestEditorRendererWidget(s, canvas, renderer) } + { s -> QuestEditorRendererWidget(s, canvas, renderer) }, + { s -> AssemblyEditorWidget(s, assemblyEditorController) }, ) } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/assembly/AssemblyAnalyser.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/assembly/AssemblyAnalyser.kt new file mode 100644 index 00000000..a5f8749c --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/assembly/AssemblyAnalyser.kt @@ -0,0 +1,8 @@ +package world.phantasmal.web.questEditor.assembly + +import world.phantasmal.core.disposable.TrackedDisposable + +class AssemblyAnalyser : TrackedDisposable() { + fun setAssembly(assembly: List) { + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/AssemblyEditorController.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/AssemblyEditorController.kt new file mode 100644 index 00000000..de73165f --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/AssemblyEditorController.kt @@ -0,0 +1,17 @@ +package world.phantasmal.web.questEditor.controllers + +import world.phantasmal.observable.value.* +import world.phantasmal.web.externals.monacoEditor.ITextModel +import world.phantasmal.web.externals.monacoEditor.createModel +import world.phantasmal.web.questEditor.stores.AssemblyEditorStore +import world.phantasmal.webui.controllers.Controller + +class AssemblyEditorController(assemblyEditorStore: AssemblyEditorStore) : Controller() { + val textModel: Val = assemblyEditorStore.textModel.orElse { EMPTY_MODEL } + val enabled: Val = assemblyEditorStore.editingEnabled + val readOnly: Val = enabled.not() or assemblyEditorStore.textModel.isNull() + + companion object { + private val EMPTY_MODEL = createModel("", AssemblyEditorStore.ASM_LANG_ID) + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestModel.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestModel.kt index daeb4366..eedb7eb4 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestModel.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestModel.kt @@ -1,5 +1,6 @@ package world.phantasmal.web.questEditor.models +import world.phantasmal.lib.assembly.Segment import world.phantasmal.lib.fileFormats.quest.Episode import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.list.ListVal @@ -16,6 +17,7 @@ class QuestModel( mapDesignations: Map, npcs: MutableList, objects: MutableList, + val byteCodeIr: List, getVariant: (Episode, areaId: Int, variantId: Int) -> AreaVariantModel?, ) { private val _id = mutableVal(0) diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/AssemblyEditorStore.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/AssemblyEditorStore.kt new file mode 100644 index 00000000..4a81e05b --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/AssemblyEditorStore.kt @@ -0,0 +1,178 @@ +package world.phantasmal.web.questEditor.stores + +import kotlinx.coroutines.CoroutineScope +import world.phantasmal.lib.assembly.disassemble +import world.phantasmal.observable.value.Val +import world.phantasmal.observable.value.trueVal +import world.phantasmal.web.externals.monacoEditor.* +import world.phantasmal.webui.obj +import world.phantasmal.webui.stores.Store +import kotlin.js.RegExp + +class AssemblyEditorStore( + scope: CoroutineScope, + questEditorStore: QuestEditorStore, +) : Store(scope) { + private var _textModel: ITextModel? = null + + val inlineStackArgs: Val = trueVal() + + val textModel: Val = + questEditorStore.currentQuest.map(inlineStackArgs) { quest, inlineArgs -> + _textModel?.dispose() + + _textModel = + if (quest == null) null + else { + val assembly = disassemble(quest.byteCodeIr, inlineArgs) + createModel(assembly.joinToString("\n"), ASM_LANG_ID) + } + + _textModel + } + + val editingEnabled: Val = questEditorStore.questEditingEnabled + + companion object { + const val ASM_LANG_ID = "psoasm" + + init { + register(obj { id = ASM_LANG_ID }) + + setMonarchTokensProvider(ASM_LANG_ID, obj { + defaultToken = "invalid" + + tokenizer = obj { + this["root"] = arrayOf( + // Strings. + obj { + // Unterminated string. + regex = RegExp('"' + """([^"\\]|\.)*$""") + action = obj { token = "string.invalid" } + }, + obj { + regex = RegExp("\"") + action = obj { + token = "string.quote" + bracket = "@open" + next = "@string" + } + }, + + // Registers. + obj { + regex = RegExp("""r\d+""") + action = obj { token = "predefined" } + }, + + // Labels. + obj { + regex = RegExp("""[^\s]+:""") + action = obj { token = "tag" } + }, + + // Numbers. + obj { + regex = RegExp("""0x[0-9a-fA-F]+""") + action = obj { token = "number.hex" } + }, + obj { + regex = RegExp("""-?\d+(\.\d+)?(e-?\d+)?""") + action = obj { token = "number.float" } + }, + obj { + regex = RegExp("""-?[0-9]+""") + action = obj { token = "number" } + }, + + // Section markers. + obj { + regex = RegExp("""\.[^\s]+""") + action = obj { token = "keyword" } + }, + + // Identifiers. + obj { + regex = RegExp("""[a-z][a-z0-9_=<>!]*""") + action = obj { token = "identifier" } + }, + + // Whitespace. + obj { + regex = RegExp("""[ \t\r\n]+""") + action = obj { token = "white" } + }, +// obj { +// regex = RegExp("""\/\*""") +// action = obj { token = "comment"; next = "@comment" } +// }, + obj { + regex = RegExp("\\/\\/.*$") + action = obj { token = "comment" } + }, + + // Delimiters. + obj { + regex = RegExp(",") + action = obj { token = "delimiter" } + }, + ) + +// this["comment"] = arrayOf( +// obj { +// regex = RegExp("""[^/*]+""") +// action = obj { token = "comment" } +// }, +// obj { +// // Nested comment. +// regex = RegExp("""\/\*""") +// action = obj { token = "comment"; next = "@push" } +// }, +// obj { +// // Nested comment end. +// regex = RegExp("""\*/""") +// action = obj { token = "comment"; next = "@pop" } +// }, +// obj { +// regex = RegExp("""[/*]""") +// action = obj { token = "comment" } +// }, +// ) + + this["string"] = arrayOf( + obj { + regex = RegExp("""[^\\"]+""") + action = obj { token = "string" } + }, + obj { + regex = RegExp("""\\(?:[n\\"])""") + action = obj { token = "string.escape" } + }, + obj { + regex = RegExp("""\\.""") + action = obj { token = "string.escape.invalid" } + }, + obj { + regex = RegExp("\"") + action = obj { + token = "string.quote" + bracket = "@close" + next = "@pop" + } + }, + ) + } + }) + + setLanguageConfiguration(ASM_LANG_ID, obj { + indentationRules = obj { + increaseIndentPattern = RegExp("^\\s*\\d+:") + decreaseIndentPattern = RegExp("^\\s*(\\d+|\\.)") + } + autoClosingPairs = arrayOf(obj { open = "\""; close = "\"" }) + surroundingPairs = arrayOf(obj { open = "\""; close = "\"" }) + comments = obj { lineComment = "//" } + }) + } + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/ModelConversion.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/ModelConversion.kt index f210dd14..8189675a 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/ModelConversion.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/ModelConversion.kt @@ -22,6 +22,7 @@ fun convertQuestToModel( // TODO: Add WaveModel to QuestNpcModel quest.npcs.mapTo(mutableListOf()) { QuestNpcModel(it, null) }, quest.objects.mapTo(mutableListOf()) { QuestObjectModel(it) }, + quest.byteCodeIr, getVariant ) } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/AssemblyEditorWidget.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/AssemblyEditorWidget.kt new file mode 100644 index 00000000..0cbbcc81 --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/AssemblyEditorWidget.kt @@ -0,0 +1,70 @@ +package world.phantasmal.web.questEditor.widgets + +import kotlinx.coroutines.CoroutineScope +import org.w3c.dom.Node +import world.phantasmal.core.disposable.disposable +import world.phantasmal.web.externals.monacoEditor.IStandaloneCodeEditor +import world.phantasmal.web.externals.monacoEditor.create +import world.phantasmal.web.externals.monacoEditor.defineTheme +import world.phantasmal.web.externals.monacoEditor.set +import world.phantasmal.web.questEditor.controllers.AssemblyEditorController +import world.phantasmal.webui.dom.div +import world.phantasmal.webui.obj +import world.phantasmal.webui.widgets.Widget + +class AssemblyEditorWidget( + scope: CoroutineScope, + private val ctrl: AssemblyEditorController, +) : Widget(scope) { + private lateinit var editor: IStandaloneCodeEditor + + override fun Node.createElement() = + div { + editor = create(this, obj { + theme = "phantasmal-world" + scrollBeyondLastLine = false + autoIndent = "full" + fontSize = 13 + wordWrap = "on" + wrappingIndent = "indent" + renderIndentGuides = false + folding = false + }) + + addDisposable(disposable { editor.dispose() }) + + observe(ctrl.textModel) { editor.setModel(it) } + + observe(ctrl.readOnly) { editor.updateOptions(obj { readOnly = it }) } + + addDisposable(size.observe { (size) -> + editor.layout(obj { + width = size.width + height = size.height + }) + }) + } + + companion object { + init { + defineTheme("phantasmal-world", obj { + base = "vs-dark" + inherit = true + rules = arrayOf( + obj { token = ""; foreground = "E0E0E0"; background = "#181818" }, + obj { token = "tag"; foreground = "99BBFF" }, + obj { token = "keyword"; foreground = "D0A0FF"; fontStyle = "bold" }, + obj { token = "predefined"; foreground = "BBFFBB" }, + obj { token = "number"; foreground = "FFFFAA" }, + obj { token = "number.hex"; foreground = "FFFFAA" }, + obj { token = "string"; foreground = "88FFFF" }, + obj { token = "string.escape"; foreground = "8888FF" }, + ) + colors = obj { + this["editor.background"] = "#181818" + this["editor.lineHighlightBackground"] = "#202020" + } + }) + } + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorWidget.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorWidget.kt index 1eb65395..23cf1286 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorWidget.kt @@ -26,6 +26,7 @@ class QuestEditorWidget( private val createQuestInfoWidget: (CoroutineScope) -> Widget, private val createNpcCountsWidget: (CoroutineScope) -> Widget, private val createQuestRendererWidget: (CoroutineScope) -> Widget, + private val createAssemblyEditorWidget: (CoroutineScope) -> Widget, ) : Widget(scope) { override fun Node.createElement() = div { @@ -60,7 +61,7 @@ class QuestEditorWidget( ), ) ), - DockedStack( + DockedRow( flex = 9, items = listOf( DockedWidget( @@ -71,7 +72,7 @@ class QuestEditorWidget( DockedWidget( title = "Script", id = "asm_editor", - createWidget = ::TestWidget + createWidget = createAssemblyEditorWidget ), ) ), diff --git a/web/src/test/kotlin/world/phantasmal/web/test/TestModels.kt b/web/src/test/kotlin/world/phantasmal/web/test/TestModels.kt index 1444c5d0..b3408e01 100644 --- a/web/src/test/kotlin/world/phantasmal/web/test/TestModels.kt +++ b/web/src/test/kotlin/world/phantasmal/web/test/TestModels.kt @@ -1,5 +1,6 @@ package world.phantasmal.web.test +import world.phantasmal.lib.assembly.Segment import world.phantasmal.lib.fileFormats.quest.Episode import world.phantasmal.lib.fileFormats.quest.NpcType import world.phantasmal.lib.fileFormats.quest.QuestNpc @@ -15,6 +16,7 @@ fun createQuestModel( episode: Episode = Episode.I, npcs: List = emptyList(), objects: List = emptyList(), + byteCodeIr: List = emptyList(), ): QuestModel = QuestModel( id, @@ -26,6 +28,7 @@ fun createQuestModel( emptyMap(), npcs.toMutableList(), objects.toMutableList(), + byteCodeIr, ) { _, _, _ -> null } fun createQuestNpcModel(type: NpcType, episode: Episode): QuestNpcModel = diff --git a/web/webpack.config.d/webpack.config.js b/web/webpack.config.d/webpack.config.js new file mode 100644 index 00000000..0bb64d61 --- /dev/null +++ b/web/webpack.config.d/webpack.config.js @@ -0,0 +1,4 @@ +config.module.rules.push({ + test: /\.(gif|jpg|png|svg|ttf)$/, + loader: "file-loader", +}); diff --git a/webui/src/main/kotlin/world/phantasmal/webui/Js.kt b/webui/src/main/kotlin/world/phantasmal/webui/Js.kt index 9188db64..cb1abbb2 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/Js.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/Js.kt @@ -1,4 +1,4 @@ package world.phantasmal.webui -fun obj(block: T.() -> Unit): T = +inline fun obj(block: T.() -> Unit): T = js("{}").unsafeCast().apply(block) diff --git a/webui/src/main/kotlin/world/phantasmal/webui/dom/HTMLElementSizeVal.kt b/webui/src/main/kotlin/world/phantasmal/webui/dom/HTMLElementSizeVal.kt new file mode 100644 index 00000000..fa6c5e56 --- /dev/null +++ b/webui/src/main/kotlin/world/phantasmal/webui/dom/HTMLElementSizeVal.kt @@ -0,0 +1,94 @@ +package world.phantasmal.webui.dom + +import org.w3c.dom.HTMLElement +import world.phantasmal.core.disposable.Disposable +import world.phantasmal.core.disposable.disposable +import world.phantasmal.core.unsafeToNonNull +import world.phantasmal.observable.Observer +import world.phantasmal.observable.value.AbstractVal + +data class Size(val width: Double, val height: Double) + +class HTMLElementSizeVal(element: HTMLElement? = null) : AbstractVal() { + private var resizeObserver: dynamic = null + + /** + * Set to true right before actual observers are added. + */ + private var hasObservers = false + + private var _value: Size? = null + + var element: HTMLElement? = null + set(element) { + if (resizeObserver != null) { + if (field != null) { + resizeObserver.unobserve(field) + } + + if (element != null) { + resizeObserver.observe(element) + } + } + + field = element + } + + init { + // Ensure we call the setter with element. + this.element = element + } + + override val value: Size + get() { + if (!hasObservers) { + _value = getSize() + } + + return _value.unsafeToNonNull() + } + + override fun observe(callNow: Boolean, observer: Observer): Disposable { + if (!hasObservers) { + hasObservers = true + + if (resizeObserver == null) { + @Suppress("UNUSED_VARIABLE") + val resize = ::resizeCallback + resizeObserver = js("new ResizeObserver(resize);") + } + + if (element != null) { + resizeObserver.observe(element) + } + + _value = getSize() + } + + val superDisposable = super.observe(callNow, observer) + + return disposable { + superDisposable.dispose() + + if (observers.isEmpty()) { + hasObservers = false + resizeObserver.disconnect() + } + } + } + + private fun getSize(): Size = + element + ?.let { Size(it.offsetWidth.toDouble(), it.offsetHeight.toDouble()) } + ?: Size(0.0, 0.0) + + private fun resizeCallback(entries: Array) { + entries.forEach { entry -> + _value = Size( + entry.contentRect.width.unsafeCast(), + entry.contentRect.height.unsafeCast() + ) + emit() + } + } +} diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Widget.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Widget.kt index 99cf8c6b..5fa7fec5 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Widget.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Widget.kt @@ -3,12 +3,13 @@ package world.phantasmal.webui.widgets import kotlinx.browser.document import kotlinx.coroutines.CoroutineScope import org.w3c.dom.* -import world.phantasmal.core.disposable.disposable import world.phantasmal.observable.Observable import world.phantasmal.observable.value.* import world.phantasmal.observable.value.list.ListVal import world.phantasmal.observable.value.list.ListValChangeEvent import world.phantasmal.webui.DisposableContainer +import world.phantasmal.webui.dom.HTMLElementSizeVal +import world.phantasmal.webui.dom.Size abstract class Widget( protected val scope: CoroutineScope, @@ -25,8 +26,7 @@ abstract class Widget( ) : DisposableContainer() { private val _ancestorVisible = mutableVal(true) private val _children = mutableListOf() - private var initResizeObserverRequested = false - private var resizeObserverInitialized = false + private val _size = HTMLElementSizeVal() private val elementDelegate = lazy { val el = document.createDocumentFragment().createElement() @@ -54,9 +54,7 @@ abstract class Widget( } } - if (initResizeObserverRequested) { - initResizeObserver(el) - } + _size.element = el interceptElement(el) el @@ -77,6 +75,8 @@ abstract class Widget( */ val selfOrAncestorVisible: Val = visible and ancestorVisible + val size: Val = _size + val children: List = _children open fun focus() { @@ -189,40 +189,6 @@ abstract class Widget( spliceChildren(0, 0, list.value) } - /** - * Called whenever [element] is resized. - * Must be initialized with [observeResize]. - */ - protected open fun resized(width: Double, height: Double) {} - - protected fun observeResize() { - if (elementDelegate.isInitialized()) { - initResizeObserver(element) - } else { - initResizeObserverRequested = true - } - } - - private fun initResizeObserver(element: Element) { - if (resizeObserverInitialized) return - - resizeObserverInitialized = true - @Suppress("UNUSED_VARIABLE") - val resize = ::resizeCallback - val observer = js("new ResizeObserver(resize);") - observer.observe(element) - addDisposable(disposable { observer.disconnect().unsafeCast() }) - } - - private fun resizeCallback(entries: Array) { - entries.forEach { entry -> - resized( - entry.contentRect.width.unsafeCast(), - entry.contentRect.height.unsafeCast() - ) - } - } - companion object { private val STYLE_EL by lazy { val el = document.createElement("style") as HTMLStyleElement