Added script editor widget.

This commit is contained in:
Daan Vanden Bosch 2020-11-14 22:18:10 +01:00
parent 0983be905d
commit cd70d22da2
35 changed files with 2028 additions and 80 deletions

View File

@ -1,3 +1,5 @@
package world.phantasmal.core
fun Char.isDigit(): Boolean = this in '0'..'9'
expect fun Int.reinterpretAsFloat(): Float

View File

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

View File

@ -0,0 +1,5 @@
package world.phantasmal.core
import java.lang.Float.intBitsToFloat
actual fun Int.reinterpretAsFloat(): Float = intBitsToFloat(this)

View File

@ -20,11 +20,15 @@ class AssemblyProblem(
fun assemble(
assembly: List<String>,
manualStack: Boolean = false,
inlineStackArgs: Boolean = true,
): PwResult<List<Segment>> {
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<String>, private val manualStack: Boolean) {
private class Assembler(private val assembly: List<String>, private val inlineStackArgs: Boolean) {
private var lineNo = 1
private lateinit var tokens: MutableList<Token>
private var ir: MutableList<Segment> = mutableListOf()
@ -355,7 +359,7 @@ private class Assembler(private val assembly: List<String>, 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 }

View File

@ -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<Segment>, inlineStackArgs: Boolean = true): List<String> {
logger.trace {
"Disassembling ${byteCodeIr.size} segments with ${
if (inlineStackArgs) "inline stack arguments" else "stack push instructions"
}."
}
val lines = mutableListOf<String>()
val stack = mutableListOf<ArgWithType>()
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<Param>, args: List<Arg>): List<ArgWithType> {
val argsWithType = mutableListOf<ArgWithType>()
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<Param>, args: List<ArgWithType>, 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 <cr> 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("\"")
}

View File

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

View File

@ -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<Param>) : RefType()
/**
* Arbitrary amount of register references.
* Arbitrary amount of register references (variadic arguments).
*/
object RegRefVarType : RefType()

View File

@ -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.
}
}

View File

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

View File

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

View File

@ -18,6 +18,9 @@ fun Val<Any?>.isNull(): Val<Boolean> =
fun Val<Any?>.isNotNull(): Val<Boolean> =
map { it != null }
fun <T> Val<T?>.orElse(defaultValue: () -> T): Val<T> =
map { it ?: defaultValue() }
infix fun <T : Comparable<T>> Val<T>.gt(value: T): Val<Boolean> =
map { it > value }

View File

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

View File

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

View File

@ -51,8 +51,6 @@ class DockWidget(
init {
js("""require("golden-layout/src/css/goldenlayout-base.css");""")
observeResize()
}
override fun Node.createElement() =
@ -94,10 +92,10 @@ 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() {
@ -155,6 +153,7 @@ class DockWidget(
.pw-core-dock {
width: 100%;
height: 100%;
overflow: hidden;
}
#pw-root .lm_header {

View File

@ -19,8 +19,6 @@ class RendererWidget(
className = "pw-core-renderer"
tabIndex = -1
observeResize()
observe(selfOrAncestorVisible) { visible ->
if (visible) {
renderer.startRendering()
@ -29,12 +27,12 @@ class RendererWidget(
}
}
append(canvas)
}
addDisposable(size.observe { (size) ->
canvas.width = floor(size.width).toInt()
canvas.height = floor(size.height).toInt()
})
override fun resized(width: Double, height: Double) {
canvas.width = floor(width).toInt()
canvas.height = floor(height).toInt()
append(canvas)
}
companion object {

View File

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

View File

@ -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<ITokenThemeRule>
var encodedTokensColors: Array<String>?
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<Selection>?
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<IIdentifiedSingleEditOperation>,
endCursorState: ICursorStateComputer = definedExternally,
): Boolean
fun executeEdits(
source: String?,
edits: Array<IIdentifiedSingleEditOperation>,
endCursorState: Array<Selection> = definedExternally,
): Boolean
fun getLineDecorations(lineNumber: Number): Array<IModelDecoration>?
fun deltaDecorations(
oldDecorations: Array<String>,
newDecorations: Array<IModelDeltaDecoration>,
): Array<String>
fun getVisibleRanges(): Array<Range>
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<dynamic /* Number | IRulerOption */>?
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<IRelatedInformation>?
var tags: Array<MarkerTag>?
}
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<IRelatedInformation>?
var tags: Array<MarkerTag>?
}
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<IMarkdownString>? */
var hoverMessage: dynamic /* IMarkdownString? | Array<IMarkdownString>? */
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<String>?
}
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<String>
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<FindMatch>
fun findMatches(
searchString: String,
searchScope: IRange,
isRegex: Boolean,
matchCase: Boolean,
wordSeparators: String?,
captureMatches: Boolean,
limitResultCount: Number = definedExternally,
): Array<FindMatch>
fun findMatches(
searchString: String,
searchScope: Array<IRange>,
isRegex: Boolean,
matchCase: Boolean,
wordSeparators: String?,
captureMatches: Boolean,
limitResultCount: Number = definedExternally,
): Array<FindMatch>
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<String>,
newDecorations: Array<IModelDeltaDecoration>,
ownerId: Number = definedExternally,
): Array<String>
fun getDecorationOptions(id: String): IModelDecorationOptions?
fun getDecorationRange(id: String): Range?
fun getLineDecorations(
lineNumber: Number,
ownerId: Number = definedExternally,
filterOutValidation: Boolean = definedExternally,
): Array<IModelDecoration>
fun getLinesDecorations(
startLineNumber: Number,
endLineNumber: Number,
ownerId: Number = definedExternally,
filterOutValidation: Boolean = definedExternally,
): Array<IModelDecoration>
fun getDecorationsInRange(
range: IRange,
ownerId: Number = definedExternally,
filterOutValidation: Boolean = definedExternally,
): Array<IModelDecoration>
fun getAllDecorations(
ownerId: Number = definedExternally,
filterOutValidation: Boolean = definedExternally,
): Array<IModelDecoration>
fun getOverviewRulerDecorations(
ownerId: Number = definedExternally,
filterOutValidation: Boolean = definedExternally,
): Array<IModelDecoration>
fun normalizeIndentation(str: String): String
fun updateOptions(newOpts: ITextModelUpdateOptions)
fun detectIndentation(defaultInsertSpaces: Boolean, defaultTabSize: Number)
fun pushStackElement()
fun pushEditOperations(
beforeCursorState: Array<Selection>?,
editOperations: Array<IIdentifiedSingleEditOperation>,
cursorStateComputer: ICursorStateComputer,
): Array<Selection>?
fun pushEOL(eol: EndOfLineSequence)
fun applyEdits(operations: Array<IIdentifiedSingleEditOperation>)
fun applyEdits(
operations: Array<IIdentifiedSingleEditOperation>,
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<IModelContentChange>
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<Position>
var reason: CursorChangeReason
var source: String
}
external interface ICursorSelectionChangedEvent {
var selection: Selection
var secondarySelections: Array<Selection>
var modelVersionId: Number
var oldSelections: Array<Selection>?
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 */
}

View File

@ -0,0 +1,8 @@
package world.phantasmal.web.externals.monacoEditor
inline operator fun IColors.get(name: String): String =
asDynamic()[name].unsafeCast<String>()
inline operator fun IColors.set(name: String, value: String) {
asDynamic()[name] = value
}

View File

@ -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<String, String> */
get() = definedExternally
set(value) = definedExternally
}
external interface LanguageConfiguration {
var comments: CommentRule?
get() = definedExternally
set(value) = definedExternally
var brackets: Array<dynamic /* JsTuple<String, String> */>?
get() = definedExternally
set(value) = definedExternally
var wordPattern: RegExp?
get() = definedExternally
set(value) = definedExternally
var indentationRules: IndentationRule?
get() = definedExternally
set(value) = definedExternally
var onEnterRules: Array<OnEnterRule>?
get() = definedExternally
set(value) = definedExternally
var autoClosingPairs: Array<IAutoClosingPairConditional>?
get() = definedExternally
set(value) = definedExternally
var surroundingPairs: Array<IAutoClosingPair>?
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<String>?
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<String>?
get() = definedExternally
set(value) = definedExternally
var filenames: Array<String>?
get() = definedExternally
set(value) = definedExternally
var filenamePatterns: Array<String>?
get() = definedExternally
set(value) = definedExternally
var firstLine: String?
get() = definedExternally
set(value) = definedExternally
var aliases: Array<String>?
get() = definedExternally
set(value) = definedExternally
var mimetypes: Array<String>?
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<IMonarchLanguageBracket>?
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<dynamic /* IShortMonarchLanguageAction | IExpandedMonarchLanguageAction | Array<IShortMonarchLanguageAction> | Array<IExpandedMonarchLanguageAction> */>?
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
}

View File

@ -0,0 +1,10 @@
package world.phantasmal.web.externals.monacoEditor
typealias IMonarchLanguageRule = IExpandedMonarchLanguageRule
inline operator fun IMonarchLanguageTokenizer.get(name: String): Array<IMonarchLanguageRule> =
asDynamic()[name].unsafeCast<Array<IMonarchLanguageRule>>()
inline operator fun IMonarchLanguageTokenizer.set(name: String, value: Array<IMonarchLanguageRule>) {
asDynamic()[name] = value
}

View File

@ -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<ISelection>, b: Array<ISelection>): 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?
}
}

View File

@ -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<Duration?> = _userTime
val time: Val<Duration> = userTime.map { it ?: defaultTime }
val time: Val<Duration> = userTime.orElse { defaultTime }
fun setUserTime(userTime: Duration?): HuntMethodModel {
_userTime.value = userTime

View File

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

View File

@ -0,0 +1,8 @@
package world.phantasmal.web.questEditor.assembly
import world.phantasmal.core.disposable.TrackedDisposable
class AssemblyAnalyser : TrackedDisposable() {
fun setAssembly(assembly: List<String>) {
}
}

View File

@ -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<ITextModel> = assemblyEditorStore.textModel.orElse { EMPTY_MODEL }
val enabled: Val<Boolean> = assemblyEditorStore.editingEnabled
val readOnly: Val<Boolean> = enabled.not() or assemblyEditorStore.textModel.isNull()
companion object {
private val EMPTY_MODEL = createModel("", AssemblyEditorStore.ASM_LANG_ID)
}
}

View File

@ -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<Int, Int>,
npcs: MutableList<QuestNpcModel>,
objects: MutableList<QuestObjectModel>,
val byteCodeIr: List<Segment>,
getVariant: (Episode, areaId: Int, variantId: Int) -> AreaVariantModel?,
) {
private val _id = mutableVal(0)

View File

@ -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<Boolean> = trueVal()
val textModel: Val<ITextModel?> =
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<Boolean> = 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<IndentationRule> {
increaseIndentPattern = RegExp("^\\s*\\d+:")
decreaseIndentPattern = RegExp("^\\s*(\\d+|\\.)")
}
autoClosingPairs = arrayOf(obj { open = "\""; close = "\"" })
surroundingPairs = arrayOf(obj { open = "\""; close = "\"" })
comments = obj<CommentRule> { lineComment = "//" }
})
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<QuestNpcModel> = emptyList(),
objects: List<QuestObjectModel> = emptyList(),
byteCodeIr: List<Segment> = emptyList(),
): QuestModel =
QuestModel(
id,
@ -26,6 +28,7 @@ fun createQuestModel(
emptyMap(),
npcs.toMutableList(),
objects.toMutableList(),
byteCodeIr,
) { _, _, _ -> null }
fun createQuestNpcModel(type: NpcType, episode: Episode): QuestNpcModel =

View File

@ -0,0 +1,4 @@
config.module.rules.push({
test: /\.(gif|jpg|png|svg|ttf)$/,
loader: "file-loader",
});

View File

@ -1,4 +1,4 @@
package world.phantasmal.webui
fun <T> obj(block: T.() -> Unit): T =
inline fun <T> obj(block: T.() -> Unit): T =
js("{}").unsafeCast<T>().apply(block)

View File

@ -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<Size>() {
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<Size>): 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<dynamic>) {
entries.forEach { entry ->
_value = Size(
entry.contentRect.width.unsafeCast<Double>(),
entry.contentRect.height.unsafeCast<Double>()
)
emit()
}
}
}

View File

@ -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<Widget>()
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<Boolean> = visible and ancestorVisible
val size: Val<Size> = _size
val children: List<Widget> = _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<Unit>() })
}
private fun resizeCallback(entries: Array<dynamic>) {
entries.forEach { entry ->
resized(
entry.contentRect.width.unsafeCast<Double>(),
entry.contentRect.height.unsafeCast<Double>()
)
}
}
companion object {
private val STYLE_EL by lazy {
val el = document.createElement("style") as HTMLStyleElement