Ported several ASM editor features.

This commit is contained in:
Daan Vanden Bosch 2020-12-06 16:48:13 +01:00
parent dc0615e1d2
commit 0133e82d3f
36 changed files with 909 additions and 192 deletions

View File

@ -0,0 +1,8 @@
package world.phantasmal.core
@Suppress("NOTHING_TO_INLINE")
inline fun <T> jsArrayOf(vararg elements: T): JsArray<T> =
elements.unsafeCast<JsArray<T>>()
inline fun <T> JsArray<T>.asArray(): Array<T> =
unsafeCast<Array<T>>()

View File

@ -0,0 +1,5 @@
package world.phantasmal.core
external interface JsArray<T> {
fun push(vararg elements: T): Int
}

View File

@ -71,8 +71,8 @@ kotlin {
val generateOpcodes = tasks.register("generateOpcodes") {
group = "code generation"
val packageName = "world.phantasmal.lib.assembly"
val opcodesFile = file("assetsGeneration/assembly/opcodes.yml")
val packageName = "world.phantasmal.lib.asm"
val opcodesFile = file("assetsGeneration/asm/opcodes.yml")
val outputFile = file(
"build/generated-src/commonMain/kotlin/${packageName.replace('.', '/')}/Opcodes.kt"
)
@ -104,7 +104,9 @@ fun opcodeToCode(writer: PrintWriter, opcode: Map<String, Any>) {
val code = (opcode["code"] as String).drop(2).toInt(16)
val codeStr = code.toString(16).toUpperCase().padStart(2, '0')
val mnemonic = opcode["mnemonic"] as String? ?: "unknown_${codeStr.toLowerCase()}"
val description = opcode["description"] as String?
val doc = (opcode["doc"] as String?)?.let {
"\"${it.replace("\n", "\\n")}\""
}
val stack = opcode["stack"] as String?
val valName = "OP_" + mnemonic
@ -136,7 +138,7 @@ fun opcodeToCode(writer: PrintWriter, opcode: Map<String, Any>) {
|val $valName = Opcode(
| 0x$codeStr,
| "$mnemonic",
| ${description?.let { "\"$it\"" }},
| $doc,
| $params,
| $stackInteraction,
|).also { ${array}[0x$indexStr] = it }""".trimMargin()

View File

@ -1,4 +1,4 @@
package world.phantasmal.lib.assembly
package world.phantasmal.lib.asm
import world.phantasmal.core.isDigit

View File

@ -1,4 +1,4 @@
package world.phantasmal.lib.assembly
package world.phantasmal.lib.asm
import mu.KotlinLogging
import world.phantasmal.core.Problem

View File

@ -1,4 +1,4 @@
package world.phantasmal.lib.assembly
package world.phantasmal.lib.asm
import mu.KotlinLogging
import world.phantasmal.core.reinterpretAsFloat

View File

@ -1,4 +1,4 @@
package world.phantasmal.lib.assembly
package world.phantasmal.lib.asm
import world.phantasmal.lib.buffer.Buffer
import kotlin.math.min

View File

@ -1,4 +1,4 @@
package world.phantasmal.lib.assembly
package world.phantasmal.lib.asm
private val MNEMONIC_TO_OPCODES: MutableMap<String, Opcode> by lazy {
val map = mutableMapOf<String, Opcode>()

View File

@ -1,6 +1,6 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis
package world.phantasmal.lib.asm.dataFlowAnalysis
import world.phantasmal.lib.assembly.*
import world.phantasmal.lib.asm.*
// See https://en.wikipedia.org/wiki/Control-flow_graph.

View File

@ -1,10 +1,10 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis
package world.phantasmal.lib.asm.dataFlowAnalysis
import mu.KotlinLogging
import world.phantasmal.lib.assembly.InstructionSegment
import world.phantasmal.lib.assembly.OP_BB_MAP_DESIGNATE
import world.phantasmal.lib.assembly.OP_MAP_DESIGNATE
import world.phantasmal.lib.assembly.OP_MAP_DESIGNATE_EX
import world.phantasmal.lib.asm.InstructionSegment
import world.phantasmal.lib.asm.OP_BB_MAP_DESIGNATE
import world.phantasmal.lib.asm.OP_MAP_DESIGNATE
import world.phantasmal.lib.asm.OP_MAP_DESIGNATE_EX
private val logger = KotlinLogging.logger {}

View File

@ -1,7 +1,7 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis
package world.phantasmal.lib.asm.dataFlowAnalysis
import mu.KotlinLogging
import world.phantasmal.lib.assembly.*
import world.phantasmal.lib.asm.*
import kotlin.math.max
import kotlin.math.min

View File

@ -1,7 +1,7 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis
package world.phantasmal.lib.asm.dataFlowAnalysis
import mu.KotlinLogging
import world.phantasmal.lib.assembly.*
import world.phantasmal.lib.asm.*
private val logger = KotlinLogging.logger {}

View File

@ -1,4 +1,4 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis
package world.phantasmal.lib.asm.dataFlowAnalysis
import kotlin.math.max
import kotlin.math.min

View File

@ -3,10 +3,10 @@ package world.phantasmal.lib.fileFormats.quest
import mu.KotlinLogging
import world.phantasmal.core.PwResult
import world.phantasmal.core.Severity
import world.phantasmal.lib.assembly.*
import world.phantasmal.lib.assembly.dataFlowAnalysis.ControlFlowGraph
import world.phantasmal.lib.assembly.dataFlowAnalysis.getRegisterValue
import world.phantasmal.lib.assembly.dataFlowAnalysis.getStackValue
import world.phantasmal.lib.asm.*
import world.phantasmal.lib.asm.dataFlowAnalysis.ControlFlowGraph
import world.phantasmal.lib.asm.dataFlowAnalysis.getRegisterValue
import world.phantasmal.lib.asm.dataFlowAnalysis.getStackValue
import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.BufferCursor
import world.phantasmal.lib.cursor.Cursor

View File

@ -5,10 +5,10 @@ import world.phantasmal.core.PwResult
import world.phantasmal.core.PwResultBuilder
import world.phantasmal.core.Severity
import world.phantasmal.core.Success
import world.phantasmal.lib.assembly.InstructionSegment
import world.phantasmal.lib.assembly.OP_SET_EPISODE
import world.phantasmal.lib.assembly.Segment
import world.phantasmal.lib.assembly.dataFlowAnalysis.getMapDesignations
import world.phantasmal.lib.asm.InstructionSegment
import world.phantasmal.lib.asm.OP_SET_EPISODE
import world.phantasmal.lib.asm.Segment
import world.phantasmal.lib.asm.dataFlowAnalysis.getMapDesignations
import world.phantasmal.lib.compression.prs.prsDecompress
import world.phantasmal.lib.cursor.Cursor
import world.phantasmal.lib.cursor.cursor

View File

@ -1,11 +1,11 @@
package world.phantasmal.lib.assembly
package world.phantasmal.lib.asm
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.testUtils.assertCloseTo
import kotlin.test.Test
import kotlin.test.assertEquals
class AssemblyTokenizationTests : LibTestSuite() {
class AsmTokenizationTests : LibTestSuite() {
@Test
fun valid_floats_are_parsed_as_Float32_tokens() {
assertCloseTo(808.9f, (tokenizeLine("808.9")[0] as Token.Float32).value)

View File

@ -1,4 +1,4 @@
package world.phantasmal.lib.assembly
package world.phantasmal.lib.asm
import world.phantasmal.core.Success
import world.phantasmal.lib.test.LibTestSuite

View File

@ -1,4 +1,4 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis
package world.phantasmal.lib.asm.dataFlowAnalysis
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.toInstructions

View File

@ -1,6 +1,6 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis
package world.phantasmal.lib.asm.dataFlowAnalysis
import world.phantasmal.lib.assembly.*
import world.phantasmal.lib.asm.*
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.toInstructions
import kotlin.test.Test

View File

@ -1,4 +1,4 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis
package world.phantasmal.lib.asm.dataFlowAnalysis
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test

View File

@ -1,9 +1,9 @@
package world.phantasmal.lib.fileFormats.quest
import world.phantasmal.core.Success
import world.phantasmal.lib.assembly.InstructionSegment
import world.phantasmal.lib.assembly.OP_BB_MAP_DESIGNATE
import world.phantasmal.lib.assembly.OP_SET_EPISODE
import world.phantasmal.lib.asm.InstructionSegment
import world.phantasmal.lib.asm.OP_BB_MAP_DESIGNATE
import world.phantasmal.lib.asm.OP_SET_EPISODE
import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test

View File

@ -1,7 +1,7 @@
package world.phantasmal.lib.fileFormats.quest
import world.phantasmal.core.Success
import world.phantasmal.lib.assembly.*
import world.phantasmal.lib.asm.*
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile
import kotlin.test.Test

View File

@ -1,8 +1,8 @@
package world.phantasmal.lib.test
import world.phantasmal.core.Success
import world.phantasmal.lib.assembly.InstructionSegment
import world.phantasmal.lib.assembly.assemble
import world.phantasmal.lib.asm.InstructionSegment
import world.phantasmal.lib.asm.assemble
import world.phantasmal.lib.cursor.Cursor
import kotlin.test.assertTrue

View File

@ -1,6 +1,7 @@
@file:JsModule("monaco-editor")
@file:JsNonModule
@file:JsQualifier("languages")
@file:Suppress("unused")
package world.phantasmal.web.externals.monacoEditor
@ -18,6 +19,27 @@ external fun setMonarchTokensProvider(
languageDef: IMonarchLanguage,
): IDisposable
/**
* Register a completion item provider (use by e.g. suggestions).
*/
external fun registerCompletionItemProvider(
languageId: String,
provider: CompletionItemProvider,
): IDisposable
/**
* Register a signature help provider (used by e.g. parameter hints).
*/
external fun registerSignatureHelpProvider(
languageId: String,
provider: SignatureHelpProvider,
): IDisposable
/**
* Register a hover provider (used by e.g. editor hover).
*/
external fun registerHoverProvider(languageId: String, provider: HoverProvider): IDisposable
external interface CommentRule {
var lineComment: String?
get() = definedExternally
@ -218,3 +240,384 @@ external interface IMonarchLanguageBracket {
var close: String
var token: String
}
external interface CompletionItemLabel {
/**
* The function or variable. Rendered leftmost.
*/
var name: String
/**
* The signature without the return type. Render after `name`.
*/
var signature: String?
get() = definedExternally
set(value) = definedExternally
/**
* The fully qualified name, like package name or file path. Rendered after `signature`.
*/
var qualifier: String?
get() = definedExternally
set(value) = definedExternally
/**
* The return-type of a function or type of a property/variable. Rendered rightmost.
*/
var type: String?
get() = definedExternally
set(value) = definedExternally
}
external interface CompletionItemRanges {
var insert: IRange
var replace: IRange
}
external enum class CompletionItemKind {
Method /* = 0 */,
Function /* = 1 */,
Constructor /* = 2 */,
Field /* = 3 */,
Variable /* = 4 */,
Class /* = 5 */,
Struct /* = 6 */,
Interface /* = 7 */,
Module /* = 8 */,
Property /* = 9 */,
Event /* = 10 */,
Operator /* = 11 */,
Unit /* = 12 */,
Value /* = 13 */,
Constant /* = 14 */,
Enum /* = 15 */,
EnumMember /* = 16 */,
Keyword /* = 17 */,
Text /* = 18 */,
Color /* = 19 */,
File /* = 20 */,
Reference /* = 21 */,
Customcolor /* = 22 */,
Folder /* = 23 */,
TypeParameter /* = 24 */,
Snippet /* = 25 */,
}
external enum class CompletionItemTag {
Deprecated /* = 1 */,
}
external enum class CompletionItemInsertTextRule {
/**
* Adjust whitespace/indentation of multiline insert texts to
* match the current line indentation.
*/
KeepWhitespace /* = 1 */,
/**
* `insertText` is a snippet.
*/
InsertAsSnippet /* = 4 */,
}
external interface Command {
var id: String
var title: String
var tooltip: String
var arguments: Array<dynamic>
}
/**
* A completion item represents a text snippet that is
* proposed to complete text that is being typed.
*/
external interface CompletionItem {
/**
* The label of this completion item. By default
* this is also the text that is inserted when selecting
* this completion.
*/
var label: CompletionItemLabel /* string | CompletionItemLabel */
/**
* The kind of this completion item. Based on the kind
* an icon is chosen by the editor.
*/
var kind: CompletionItemKind
/**
* A modifier to the `kind` which affect how the item
* is rendered, e.g. Deprecated is rendered with a strikeout
*/
var tags: Array<CompletionItemTag>
/**
* A human-readable string with additional information
* about this item, like type or symbol information.
*/
var detail: String
/**
* A human-readable string that represents a doc-comment.
*/
var documentation: String /* string | IMarkdownString */
/**
* A string that should be used when comparing this item
* with other items. When `falsy` the [label](#CompletionItem.label)
* is used.
*/
var sortText: String
/**
* A string that should be used when filtering a set of
* completion items. When `falsy` the [label](#CompletionItem.label)
* is used.
*/
var filterText: String
/**
* Select this item when showing. *Note* that only one completion item can be selected and
* that the editor decides which item that is. The rule is that the *first* item of those
* that match best is selected.
*/
var preselect: Boolean
/**
* A string or snippet that should be inserted in a document when selecting
* this completion. When `falsy` the [label](#CompletionItem.label)
* is used.
*/
var insertText: String
/**
* Addition rules (as bitmask) that should be applied when inserting
* this completion.
*/
var insertTextRules: CompletionItemInsertTextRule
/**
* A range of text that should be replaced by this completion item.
*
* Defaults to a range from the start of the [current word](#TextDocument.getWordRangeAtPosition) to the
* current position.
*
* *Note:* The range must be a [single line](#Range.isSingleLine) and it must
* [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems).
*/
var range: CompletionItemRanges
/**
* An optional set of characters that when pressed while this completion is active will accept it first and
* then type that character. *Note* that all commit characters should have `length=1` and that superfluous
* characters will be ignored.
*/
var commitCharacters: Array<String>
/**
* An optional array of additional text edits that are applied when
* selecting this completion. Edits must not overlap with the main edit
* nor with themselves.
*/
var additionalTextEdits: Array<ISingleEditOperation>
/**
* A command that should be run upon acceptance of this item.
*/
var command: Command
}
external interface CompletionList {
var suggestions: Array<CompletionItem>
var incomplete: Boolean
fun dispose()
}
/**
* How a suggest provider was triggered.
*/
external enum class CompletionTriggerKind {
Invoke /* = 0 */,
TriggerCharacter /* = 1 */,
TriggerForIncompleteCompletions /* = 2 */,
}
/**
* Contains additional information about the context in which
* [completion provider](#CompletionItemProvider.provideCompletionItems) is triggered.
*/
external interface CompletionContext {
/**
* How the completion was triggered.
*/
var triggerKind: CompletionTriggerKind
/**
* Character that triggered the completion item provider.
*
* `undefined` if provider was not triggered by a character.
*/
var triggerCharacter: String
}
external interface CompletionItemProvider {
var triggerCharacters: Array<String>?
get() = definedExternally
set(value) = definedExternally
/**
* Provide completion items for the given position and document.
*/
fun provideCompletionItems(
model: ITextModel,
position: Position,
context: CompletionContext,
token: CancellationToken,
): CompletionList /* type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null> */
}
/**
* Represents a parameter of a callable-signature. A parameter can
* have a label and a doc-comment.
*/
external interface ParameterInformation {
/**
* The label of this signature. Will be shown in
* the UI.
*/
var label: Array<Int> /* string | [number, number] */
/**
* The human-readable doc-comment of this signature. Will be shown
* in the UI but can be omitted.
*
* This property is not nullable in TS. Do not assign null to it as null will be interpreted as
* IMarkdownString.
*/
var documentation: String? /* string | IMarkdownString */
get() = definedExternally
set(value) = definedExternally
}
/**
* Represents the signature of something callable. A signature
* can have a label, like a function-name, a doc-comment, and
* a set of parameters.
*/
external interface SignatureInformation {
/**
* The label of this signature. Will be shown in
* the UI.
*/
var label: String
/**
* The human-readable doc-comment of this signature. Will be shown
* in the UI but can be omitted.
*
* This property is not nullable in TS. Do not assign null to it as null will be interpreted as
* IMarkdownString.
*/
var documentation: String? /* string | IMarkdownString */
get() = definedExternally
set(value) = definedExternally
/**
* The parameters of this signature.
*/
var parameters: Array<ParameterInformation>
}
/**
* Signature help represents the signature of something
* callable. There can be multiple signatures but only one
* active and only one active parameter.
*/
external interface SignatureHelp {
/**
* One or more signatures.
*/
var signatures: Array<SignatureInformation>
/**
* The active signature.
*/
var activeSignature: Int
/**
* The active parameter of the active signature.
*/
var activeParameter: Int
}
external enum class SignatureHelpTriggerKind {
Invoke /* = 1 */,
TriggerCharacter /* = 2 */,
ContentChange /* = 3 */,
}
external interface SignatureHelpContext {
val triggerKind: SignatureHelpTriggerKind
val triggerCharacter: String?
get() = definedExternally
val isRetrigger: Boolean
val activeSignatureHelp: SignatureHelp?
get() = definedExternally
}
external interface SignatureHelpResult : IDisposable {
var value: SignatureHelp
}
/**
* The signature help provider interface defines the contract between extensions and
* the [parameter hints](https://code.visualstudio.com/docs/editor/intellisense)-feature.
*/
external interface SignatureHelpProvider {
val signatureHelpTriggerCharacters: Array<String>?
get() = definedExternally
val signatureHelpRetriggerCharacters: Array<String>?
get() = definedExternally
/**
* Provide help for the signature at the given position and document.
*/
fun provideSignatureHelp(
model: ITextModel,
position: Position,
token: CancellationToken,
context: SignatureHelpContext,
): SignatureHelpResult? /* type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null> */
}
/**
* A hover represents additional information for a symbol or word. Hovers are
* rendered in a tooltip-like widget.
*/
external interface Hover {
/**
* The contents of this hover.
*/
var contents: Array<IMarkdownString>
/**
* The range to which this hover applies. When missing, the
* editor will use the range at the current position or the
* current position itself.
*/
var range: IRange
}
external interface HoverProvider {
/**
* Provide a hover for the given position and document. Multiple hovers at the same
* position will be merged by the editor. A hover can have a range which defaults
* to the word range at the position when omitted.
*/
fun provideHover(
model: ITextModel,
position: Position,
token: CancellationToken,
): Hover? /* type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null> */
}

View File

@ -5,6 +5,16 @@ 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>) {
inline operator fun IMonarchLanguageTokenizer.set(
name: String,
value: Array<IMonarchLanguageRule>,
) {
asDynamic()[name] = value
}
inline operator fun IMarkdownStringUris.get(name: String): UriComponents =
asDynamic()[name].unsafeCast<UriComponents>()
inline operator fun IMarkdownStringUris.set(name: String, value: UriComponents) {
asDynamic()[name] = value
}

View File

@ -8,6 +8,19 @@ external interface IDisposable {
fun dispose()
}
external interface CancellationToken {
val isCancellationRequested: Boolean
/**
* An event emitted when cancellation is requested
* @event
*/
fun onCancellationRequested(
listener: (e: Any) -> Any,
thisArg: Any = definedExternally,
): IDisposable
}
external enum class MarkerTag {
Unnecessary /* = 1 */,
Deprecated /* = 2 */
@ -120,21 +133,21 @@ external enum class SelectionDirection {
}
external interface IPosition {
var lineNumber: Number
var column: Number
var lineNumber: Int
var column: Int
}
open external class Position(lineNumber: Number, column: Number) {
open var lineNumber: Number
open var column: Number
open external class Position(lineNumber: Int, column: Int) {
open var lineNumber: Int
open var column: Int
open fun with(
newLineNumber: Number = definedExternally,
newColumn: Number = definedExternally,
newLineNumber: Int = definedExternally,
newColumn: Int = definedExternally,
): Position
open fun delta(
deltaLineNumber: Number = definedExternally,
deltaColumn: Number = definedExternally,
deltaLineNumber: Int = definedExternally,
deltaColumn: Int = definedExternally,
): Position
open fun equals(other: IPosition): Boolean
@ -147,7 +160,7 @@ open external class Position(lineNumber: Number, column: Number) {
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 compare(a: IPosition, b: IPosition): Int
fun lift(pos: IPosition): Position
fun isIPosition(obj: Any): Boolean
}
@ -182,6 +195,15 @@ open external class Uri : UriComponents {
}
}
external interface IMarkdownStringUris
external interface IMarkdownString {
var value: String
var isTrusted: Boolean
var supportThemeIcons: Boolean
var uris: IMarkdownStringUris
}
external object KeyCode {
/**
* Placed first to cover the 0 value of the enum.

View File

@ -0,0 +1,70 @@
package world.phantasmal.web.questEditor.asm
import world.phantasmal.lib.asm.OPCODES
import world.phantasmal.lib.asm.OPCODES_F8
import world.phantasmal.lib.asm.OPCODES_F9
import world.phantasmal.web.externals.monacoEditor.*
import world.phantasmal.webui.obj
object AsmCompletionItemProvider : CompletionItemProvider {
override fun provideCompletionItems(
model: ITextModel,
position: Position,
context: CompletionContext,
token: CancellationToken,
): CompletionList {
val text = model.getValueInRange(obj {
startLineNumber = position.lineNumber
endLineNumber = position.lineNumber
startColumn = 1
endColumn = position.column
})
val suggestions = when {
KEYWORD_REGEX.matches(text) -> KEYWORD_SUGGESTIONS
INSTRUCTION_REGEX.matches(text) -> INSTRUCTION_SUGGESTIONS
else -> emptyArray()
}
return obj {
this.suggestions = suggestions
incomplete = false
}
}
private val KEYWORD_REGEX = Regex("""^\s*\.[a-z]+${'$'}""")
private val KEYWORD_SUGGESTIONS: Array<CompletionItem> =
arrayOf(
obj {
label = obj { name = ".code" }
kind = CompletionItemKind.Keyword
insertText = "code"
},
obj {
label = obj { name = ".data" }
kind = CompletionItemKind.Keyword
insertText = "data"
},
obj {
label = obj { name = ".string" }
kind = CompletionItemKind.Keyword
insertText = "string"
},
)
private val INSTRUCTION_REGEX = Regex("""^\s*([a-z][a-z0-9_=<>!]*)?${'$'}""")
private val INSTRUCTION_SUGGESTIONS: Array<CompletionItem> =
(OPCODES + OPCODES_F8 + OPCODES_F9)
.filterNotNull()
.map { opcode ->
obj<CompletionItem> {
label = obj {
name = opcode.mnemonic
// TODO: Add signature?
}
kind = CompletionItemKind.Function
insertText = opcode.mnemonic
}
}
.toTypedArray()
}

View File

@ -0,0 +1,60 @@
package world.phantasmal.web.questEditor.asm
import world.phantasmal.core.asArray
import world.phantasmal.core.jsArrayOf
import world.phantasmal.web.externals.monacoEditor.*
import world.phantasmal.webui.obj
object AsmHoverProvider : HoverProvider {
override fun provideHover(
model: ITextModel,
position: Position,
token: CancellationToken,
): Hover? {
val help = AsmSignatureHelpProvider.getSignatureHelp(model, position)
?: return null
val sig = help.signatures[help.activeSignature]
val param = sig.parameters.getOrNull(help.activeParameter)
val contents = jsArrayOf<IMarkdownString>()
// Instruction signature. Parameter highlighted if possible.
contents.push(
obj {
value =
if (param == null) {
sig.label
} else {
// TODO: Figure out how to underline the active parameter in addition to
// bolding it to make it match the look of the signature help.
sig.label.substring(0, param.label[0]) +
"__" +
sig.label.substring(param.label[0], param.label[1]) +
"__" +
sig.label.substring(param.label[1])
}
}
)
// Put the parameter doc and the instruction doc in the same string to match the look of the
// signature help.
var doc = ""
// Parameter doc.
if (param?.documentation != null) {
doc += param.documentation
// TODO: Figure out how add an empty line here to make it match the look of the
// signature help.
doc += "\n\n"
}
// Instruction doc.
sig.documentation?.let { doc += it }
contents.push(obj { value = doc })
return obj<Hover> { this.contents = contents.asArray() }
}
}

View File

@ -0,0 +1,22 @@
package world.phantasmal.web.questEditor.asm
import world.phantasmal.web.externals.monacoEditor.*
import world.phantasmal.webui.obj
import kotlin.js.RegExp
object AsmLanguageConfiguration : LanguageConfiguration {
override var indentationRules: IndentationRule? =
obj<IndentationRule> {
increaseIndentPattern = RegExp("""^\s*\d+:""")
decreaseIndentPattern = RegExp("""^\s*(\d+|\.)""")
}
override var autoClosingPairs: Array<IAutoClosingPairConditional>? =
arrayOf(obj { open = "\""; close = "\"" })
override var surroundingPairs: Array<IAutoClosingPair>? =
arrayOf(obj { open = "\""; close = "\"" })
override var comments: CommentRule? =
obj<CommentRule> { lineComment = "//" }
}

View File

@ -0,0 +1,132 @@
package world.phantasmal.web.questEditor.asm
import world.phantasmal.web.externals.monacoEditor.IMonarchLanguage
import world.phantasmal.web.externals.monacoEditor.IMonarchLanguageTokenizer
import world.phantasmal.web.externals.monacoEditor.set
import world.phantasmal.webui.obj
import kotlin.js.RegExp
object AsmMonarchLanguage : IMonarchLanguage {
override var defaultToken: String? = "invalid"
override var tokenizer: IMonarchLanguageTokenizer = 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"
}
},
)
}
}

View File

@ -0,0 +1,113 @@
package world.phantasmal.web.questEditor.asm
import world.phantasmal.core.asArray
import world.phantasmal.core.jsArrayOf
import world.phantasmal.lib.asm.*
import world.phantasmal.web.externals.monacoEditor.*
import world.phantasmal.webui.obj
object AsmSignatureHelpProvider : SignatureHelpProvider {
override val signatureHelpTriggerCharacters: Array<String> =
arrayOf(" ", ",")
override val signatureHelpRetriggerCharacters: Array<String> =
arrayOf(", ")
override fun provideSignatureHelp(
model: ITextModel,
position: Position,
token: CancellationToken,
context: SignatureHelpContext,
): SignatureHelpResult? =
getSignatureHelp(model, position)?.let { signatureHelp ->
object : SignatureHelpResult {
override var value: SignatureHelp = signatureHelp
override fun dispose() {
// Nothing to dispose.
}
}
}
fun getSignatureHelp(model: ITextModel, position: Position): SignatureHelp? {
// Hacky way of providing parameter hints.
// We just tokenize the current line and look for the first identifier and check whether
// it's a valid opcode.
var signatureInfo: SignatureInformation? = null
var activeParam = -1
val line = model.getLineContent(position.lineNumber)
val tokens = tokenizeLine(line)
tokens.find { it is Token.Ident }?.let { ident ->
ident as Token.Ident
mnemonicToOpcode(ident.value)?.let { opcode ->
signatureInfo = getSignatureInformation(opcode)
for (tkn in tokens) {
if (tkn.col + tkn.len > position.column) {
break
} else if (tkn is Token.Ident && activeParam == -1) {
activeParam = 0
} else if (tkn is Token.ArgSeparator) {
activeParam++
}
}
}
}
return signatureInfo?.let { sigInfo ->
obj<SignatureHelp> {
signatures = arrayOf(sigInfo)
activeSignature = 0
activeParameter = activeParam
}
}
}
private fun getSignatureInformation(opcode: Opcode): SignatureInformation {
var signature = opcode.mnemonic + " "
val params = jsArrayOf<ParameterInformation>()
var first = true
for (param in opcode.params) {
if (first) {
first = false
} else {
signature += ", "
}
val paramTypeStr = when (param.type) {
ByteType -> "Byte"
ShortType -> "Short"
IntType -> "Int"
FloatType -> "Float"
ILabelType -> "&Function"
DLabelType -> "&Data"
SLabelType -> "&String"
ILabelVarType -> "...&Function"
StringType -> "String"
RegRefType, is RegTupRefType -> "Register"
RegRefVarType -> "...Register"
PointerType -> "Pointer"
else -> "Any"
}
params.push(
obj {
label = arrayOf(signature.length, signature.length + paramTypeStr.length)
param.doc?.let { documentation = it }
}
)
signature += paramTypeStr
}
return obj {
label = signature
opcode.doc?.let { documentation = it }
parameters = params.asArray()
}
}
}

View File

@ -1,6 +1,6 @@
package world.phantasmal.web.questEditor.models
import world.phantasmal.lib.assembly.Segment
import world.phantasmal.lib.asm.Segment
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.list.ListVal

View File

@ -1,6 +1,6 @@
package world.phantasmal.web.questEditor.stores
import world.phantasmal.lib.assembly.disassemble
import world.phantasmal.lib.asm.disassemble
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observable
import world.phantasmal.observable.emitter
@ -10,10 +10,10 @@ import world.phantasmal.observable.value.trueVal
import world.phantasmal.web.core.undo.SimpleUndo
import world.phantasmal.web.core.undo.UndoManager
import world.phantasmal.web.externals.monacoEditor.*
import world.phantasmal.web.questEditor.asm.*
import world.phantasmal.web.questEditor.models.QuestModel
import world.phantasmal.webui.obj
import world.phantasmal.webui.stores.Store
import kotlin.js.RegExp
class AsmStore(
questEditorStore: QuestEditorStore,
@ -103,141 +103,11 @@ class AsmStore(
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 = "//" }
})
setMonarchTokensProvider(ASM_LANG_ID, AsmMonarchLanguage)
setLanguageConfiguration(ASM_LANG_ID, AsmLanguageConfiguration)
registerCompletionItemProvider(ASM_LANG_ID, AsmCompletionItemProvider)
registerSignatureHelpProvider(ASM_LANG_ID, AsmSignatureHelpProvider)
registerHoverProvider(ASM_LANG_ID, AsmHoverProvider)
}
}
}

View File

@ -1,6 +1,6 @@
package world.phantasmal.web.test
import world.phantasmal.lib.assembly.Segment
import world.phantasmal.lib.asm.Segment
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.lib.fileFormats.quest.NpcType
import world.phantasmal.lib.fileFormats.quest.QuestNpc