mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 15:28:29 +08:00

- When the assembly worker updates map designations, it now takes map_designate and map_designate_ex into account
298 lines
10 KiB
TypeScript
298 lines
10 KiB
TypeScript
import { editor, languages, Uri } from "monaco-editor";
|
|
import AssemblyWorker from "worker-loader!./assembly_worker";
|
|
import {
|
|
AssemblyChangeInput,
|
|
AssemblySettingsChangeInput,
|
|
AssemblyWorkerOutput,
|
|
DefinitionInput,
|
|
InputMessageType,
|
|
NewAssemblyInput,
|
|
OutputMessageType,
|
|
SignatureHelpInput,
|
|
} from "./assembly_worker_messages";
|
|
import { AssemblyError, AssemblySettings, AssemblyWarning } from "./assembly";
|
|
import { disassemble } from "./disassembly";
|
|
import { QuestModel } from "../model/QuestModel";
|
|
import { Kind, OPCODES } from "../../core/data_formats/asm/opcodes";
|
|
import { Property } from "../../core/observable/property/Property";
|
|
import { property } from "../../core/observable";
|
|
import { WritableProperty } from "../../core/observable/property/WritableProperty";
|
|
import { Disposable } from "../../core/observable/Disposable";
|
|
import CompletionList = languages.CompletionList;
|
|
import CompletionItemKind = languages.CompletionItemKind;
|
|
import CompletionItem = languages.CompletionItem;
|
|
import IModelContentChange = editor.IModelContentChange;
|
|
import SignatureHelp = languages.SignatureHelp;
|
|
import ParameterInformation = languages.ParameterInformation;
|
|
import LocationLink = languages.LocationLink;
|
|
|
|
const INSTRUCTION_SUGGESTIONS = OPCODES.filter(opcode => opcode != null).map(opcode => {
|
|
return ({
|
|
label: opcode.mnemonic,
|
|
kind: CompletionItemKind.Function,
|
|
insertText: opcode.mnemonic,
|
|
} as any) as languages.CompletionItem;
|
|
});
|
|
|
|
const KEYWORD_SUGGESTIONS = [
|
|
{
|
|
label: ".code",
|
|
kind: CompletionItemKind.Keyword,
|
|
insertText: "code",
|
|
},
|
|
{
|
|
label: ".data",
|
|
kind: CompletionItemKind.Keyword,
|
|
insertText: "data",
|
|
},
|
|
{
|
|
label: ".string",
|
|
kind: CompletionItemKind.Keyword,
|
|
insertText: "string",
|
|
},
|
|
] as CompletionItem[];
|
|
|
|
export class AssemblyAnalyser implements Disposable {
|
|
readonly _issues: WritableProperty<{
|
|
warnings: AssemblyWarning[];
|
|
errors: AssemblyError[];
|
|
}> = property({ warnings: [], errors: [] });
|
|
|
|
readonly issues: Property<{
|
|
warnings: AssemblyWarning[];
|
|
errors: AssemblyError[];
|
|
}> = this._issues;
|
|
|
|
private worker = new AssemblyWorker();
|
|
private quest?: QuestModel;
|
|
|
|
private promises = new Map<
|
|
number,
|
|
{
|
|
uri: Uri;
|
|
resolve: (result: any) => void;
|
|
reject: (error: Error) => void;
|
|
}
|
|
>();
|
|
|
|
private message_id = 0;
|
|
|
|
constructor() {
|
|
this.worker.onmessage = this.process_worker_message;
|
|
}
|
|
|
|
disassemble(quest: QuestModel, manual_stack: boolean): string[] {
|
|
this.quest = quest;
|
|
const assembly = disassemble(quest.object_code, manual_stack);
|
|
const message: NewAssemblyInput = { type: InputMessageType.NewAssembly, assembly };
|
|
this.worker.postMessage(message);
|
|
return assembly;
|
|
}
|
|
|
|
update_assembly(changes: IModelContentChange[]): void {
|
|
const message: AssemblyChangeInput = {
|
|
type: InputMessageType.AssemblyChange,
|
|
changes: changes.map(change => ({
|
|
start_line_no: change.range.startLineNumber,
|
|
start_col: change.range.startColumn,
|
|
end_line_no: change.range.endLineNumber,
|
|
end_col: change.range.endColumn,
|
|
new_text: change.text,
|
|
})),
|
|
};
|
|
this.worker.postMessage(message);
|
|
}
|
|
|
|
provide_completion_items(text: string): CompletionList {
|
|
const suggestions = /^\s*([a-z][a-z0-9_=<>!]*)?$/.test(text)
|
|
? INSTRUCTION_SUGGESTIONS
|
|
: /^\s*\.[a-z]+$/.test(text)
|
|
? KEYWORD_SUGGESTIONS
|
|
: [];
|
|
|
|
return {
|
|
suggestions,
|
|
incomplete: false,
|
|
};
|
|
}
|
|
|
|
async provide_signature_help(
|
|
uri: Uri,
|
|
line_no: number,
|
|
col: number,
|
|
): Promise<SignatureHelp | undefined> {
|
|
return await this.send_and_await_response<SignatureHelpInput, SignatureHelp>(
|
|
"Signature help provision",
|
|
uri,
|
|
id => ({
|
|
type: InputMessageType.SignatureHelp,
|
|
id,
|
|
line_no,
|
|
col,
|
|
}),
|
|
);
|
|
}
|
|
|
|
async provide_definition(uri: Uri, line_no: number, col: number): Promise<LocationLink[]> {
|
|
return await this.send_and_await_response<DefinitionInput, LocationLink[]>(
|
|
"Definition provision",
|
|
uri,
|
|
id => ({
|
|
type: InputMessageType.Definition,
|
|
id,
|
|
line_no,
|
|
col,
|
|
}),
|
|
);
|
|
}
|
|
|
|
update_settings(changed_settings: Partial<AssemblySettings>): void {
|
|
const message: AssemblySettingsChangeInput = {
|
|
type: InputMessageType.SettingsChange,
|
|
settings: changed_settings,
|
|
};
|
|
this.worker.postMessage(message);
|
|
}
|
|
|
|
dispose(): void {
|
|
this.worker.terminate();
|
|
}
|
|
|
|
private async send_and_await_response<M, R>(
|
|
name: string,
|
|
uri: Uri,
|
|
create_request: (id: number) => M,
|
|
): Promise<R> {
|
|
const id = this.message_id++;
|
|
|
|
return new Promise<R>((resolve, reject) => {
|
|
this.promises.set(id, { uri, resolve, reject });
|
|
const message: M = create_request(id);
|
|
this.worker.postMessage(message);
|
|
|
|
setTimeout(() => {
|
|
if (this.promises.delete(id)) {
|
|
reject(new Error(`${name} timed out.`));
|
|
}
|
|
}, 5_000);
|
|
});
|
|
}
|
|
|
|
private process_worker_message = (e: MessageEvent): void => {
|
|
const message: AssemblyWorkerOutput = e.data;
|
|
|
|
switch (message.type) {
|
|
case OutputMessageType.NewObjectCode:
|
|
if (this.quest) {
|
|
this.quest.object_code.splice(
|
|
0,
|
|
this.quest.object_code.length,
|
|
...message.object_code,
|
|
);
|
|
this.quest.set_map_designations(message.map_designations);
|
|
this._issues.val = { warnings: message.warnings, errors: message.errors };
|
|
}
|
|
break;
|
|
|
|
case OutputMessageType.SignatureHelp:
|
|
{
|
|
const promise = this.promises.get(message.id);
|
|
|
|
if (promise) {
|
|
this.promises.delete(message.id);
|
|
|
|
if (message.opcode) {
|
|
let signature = message.opcode.mnemonic + " ";
|
|
const parameters: ParameterInformation[] = [];
|
|
let first = true;
|
|
|
|
for (const param of message.opcode.params) {
|
|
if (first) {
|
|
first = false;
|
|
} else {
|
|
signature += ", ";
|
|
}
|
|
|
|
let param_name: string;
|
|
|
|
switch (param.type.kind) {
|
|
case Kind.ILabel:
|
|
param_name = "FuncLabel";
|
|
break;
|
|
case Kind.DLabel:
|
|
param_name = "DataLabel";
|
|
break;
|
|
case Kind.SLabel:
|
|
param_name = "StringLabel";
|
|
break;
|
|
case Kind.ILabelVar:
|
|
param_name = "...FuncLabel";
|
|
break;
|
|
case Kind.RegRef:
|
|
case Kind.RegTupRef:
|
|
param_name = "Register";
|
|
break;
|
|
case Kind.RegRefVar:
|
|
param_name = "...Register";
|
|
break;
|
|
default:
|
|
param_name = Kind[param.type.kind];
|
|
break;
|
|
}
|
|
|
|
parameters.push({
|
|
label: [signature.length, signature.length + param_name.length],
|
|
documentation: param.doc,
|
|
});
|
|
|
|
signature += param_name;
|
|
}
|
|
|
|
const help: SignatureHelp = {
|
|
signatures: [
|
|
{
|
|
label: signature,
|
|
documentation: message.opcode.doc,
|
|
parameters,
|
|
},
|
|
],
|
|
activeSignature: 0,
|
|
activeParameter: message.active_param,
|
|
};
|
|
promise.resolve(help);
|
|
} else {
|
|
promise.resolve(undefined);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case OutputMessageType.Definition:
|
|
{
|
|
const promise = this.promises.get(message.id);
|
|
|
|
if (promise) {
|
|
this.promises.delete(message.id);
|
|
|
|
const location_links: LocationLink[] = [];
|
|
|
|
if (message.line_no != undefined) {
|
|
location_links.push({
|
|
uri: promise.uri,
|
|
range: {
|
|
startLineNumber: message.line_no,
|
|
startColumn: message.col!,
|
|
endLineNumber: message.line_no,
|
|
endColumn: message.col! + message.len!,
|
|
},
|
|
});
|
|
}
|
|
|
|
promise.resolve(location_links);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
}
|