mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 07:18:29 +08:00
Reworked script editor decorations.
Breakpoints now move correctly when script is edited.
This commit is contained in:
parent
8c4e0c2ed2
commit
6e41b6fb79
@ -29,90 +29,12 @@ editor.defineTheme("phantasmal-world", {
|
||||
|
||||
const DUMMY_MODEL = editor.createModel("", "psoasm");
|
||||
|
||||
/**
|
||||
* Merge Monaco decorations into one.
|
||||
*/
|
||||
function merge_monaco_decorations(
|
||||
...decos: readonly editor.IModelDeltaDecoration[]
|
||||
): editor.IModelDeltaDecoration {
|
||||
if (decos.length === 0) {
|
||||
throw new Error("At least 1 argument is required.");
|
||||
}
|
||||
|
||||
const merged: any = Object.assign({}, decos[0]);
|
||||
merged.options = Object.assign({}, decos[0].options);
|
||||
|
||||
if (decos.length === 1) {
|
||||
return merged;
|
||||
}
|
||||
|
||||
for (let i = 1; i < decos.length; i++) {
|
||||
const deco = decos[i];
|
||||
for (const key of Object.keys(deco.options)) {
|
||||
if (deco.options.hasOwnProperty(key)) {
|
||||
const val = (deco.options as any)[key];
|
||||
|
||||
switch (typeof val) {
|
||||
case "object":
|
||||
case "boolean":
|
||||
case "number":
|
||||
case "string":
|
||||
merged.options[key] = val;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
/**
|
||||
* Monaco doesn't normally support having more than one decoration per line.
|
||||
* This function enables multiple decorations per line by merging them.
|
||||
*/
|
||||
function update_monaco_decorations(
|
||||
editor: IStandaloneCodeEditor,
|
||||
deco_opts: editor.IModelDecorationOptions,
|
||||
...line_nums: readonly number[]
|
||||
): void {
|
||||
const old_decos: string[] = [];
|
||||
const delta_decos: editor.IModelDeltaDecoration[] = [];
|
||||
|
||||
if (line_nums.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const line_num of line_nums) {
|
||||
const update_deco = {
|
||||
range: new Range(line_num, 0, line_num, 0),
|
||||
options: deco_opts,
|
||||
};
|
||||
|
||||
const cur_decos = editor.getLineDecorations(line_num);
|
||||
if (cur_decos) {
|
||||
// save current decos for replacement
|
||||
for (const deco of cur_decos) {
|
||||
old_decos.push(deco.id);
|
||||
}
|
||||
|
||||
// merge current and new decos
|
||||
delta_decos.push(merge_monaco_decorations(...cur_decos, update_deco));
|
||||
} else {
|
||||
// nothing to update on this line
|
||||
delta_decos.push(update_deco);
|
||||
}
|
||||
}
|
||||
|
||||
// commit changes
|
||||
editor.deltaDecorations(old_decos, delta_decos);
|
||||
}
|
||||
|
||||
export class AsmEditorView extends ResizableWidget {
|
||||
private readonly tool_bar_view = this.disposable(new AsmEditorToolBar());
|
||||
private readonly editor: IStandaloneCodeEditor;
|
||||
private readonly history: EditorHistory;
|
||||
private breakpoint_decoration_ids: string[] = [];
|
||||
private execloc_decoration_id: string | undefined;
|
||||
|
||||
readonly element = el.div();
|
||||
|
||||
@ -169,33 +91,53 @@ export class AsmEditorView extends ResizableWidget {
|
||||
this.editor.updateOptions({ readOnly: model == undefined });
|
||||
this.editor.setModel(model || DUMMY_MODEL);
|
||||
this.history.reset();
|
||||
|
||||
this.breakpoint_decoration_ids = [];
|
||||
this.execloc_decoration_id = "";
|
||||
|
||||
asm_editor_store.clear_breakpoints();
|
||||
},
|
||||
{ call_now: true },
|
||||
),
|
||||
|
||||
asm_editor_store.breakpoints.observe_list(change => {
|
||||
if (change.type === ListChangeType.ListChange) {
|
||||
|
||||
// remove
|
||||
update_monaco_decorations(
|
||||
this.editor,
|
||||
{
|
||||
glyphMarginClassName: null,
|
||||
glyphMarginHoverMessage: null,
|
||||
},
|
||||
...change.removed,
|
||||
);
|
||||
for (const line_num of change.removed) {
|
||||
const cur_decos = this.editor.getLineDecorations(line_num);
|
||||
// find decoration on line
|
||||
if (cur_decos) {
|
||||
for (const deco of cur_decos) {
|
||||
const idx = this.breakpoint_decoration_ids.indexOf(deco.id);
|
||||
|
||||
if (idx > -1) {
|
||||
// remove decoration
|
||||
this.editor.deltaDecorations([deco.id], []);
|
||||
this.breakpoint_decoration_ids.splice(idx, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add
|
||||
update_monaco_decorations(
|
||||
this.editor,
|
||||
{
|
||||
glyphMarginClassName: "quest_editor_AsmEditorView_breakpoint-enabled",
|
||||
glyphMarginHoverMessage: {
|
||||
value: "Breakpoint",
|
||||
},
|
||||
},
|
||||
...change.inserted,
|
||||
);
|
||||
for (const line_num of change.inserted) {
|
||||
const cur_decos = this.editor.getLineDecorations(line_num);
|
||||
// don't allow duplicates
|
||||
if (!cur_decos?.some(deco => this.breakpoint_decoration_ids.includes(deco.id))) {
|
||||
// add new decoration, don't overwrite anything, save decoration id
|
||||
this.breakpoint_decoration_ids.push(this.editor.deltaDecorations([], [{
|
||||
range: new Range(line_num, 0, line_num, 0),
|
||||
options: {
|
||||
glyphMarginClassName: "quest_editor_AsmEditorView_breakpoint-enabled",
|
||||
glyphMarginHoverMessage: {
|
||||
value: "Breakpoint"
|
||||
}
|
||||
}
|
||||
}])[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
@ -203,28 +145,24 @@ export class AsmEditorView extends ResizableWidget {
|
||||
const old_line_num = e.old_value;
|
||||
const new_line_num = e.value;
|
||||
|
||||
// unset old
|
||||
if (old_line_num !== undefined) {
|
||||
update_monaco_decorations(
|
||||
this.editor,
|
||||
{
|
||||
className: null,
|
||||
isWholeLine: false,
|
||||
},
|
||||
old_line_num,
|
||||
);
|
||||
// remove old
|
||||
if (old_line_num !== undefined && this.execloc_decoration_id !== undefined) {
|
||||
const old_line_decos = this.editor.getLineDecorations(old_line_num);
|
||||
|
||||
if (old_line_decos) {
|
||||
this.editor.deltaDecorations([this.execloc_decoration_id], []);
|
||||
}
|
||||
}
|
||||
|
||||
// set new
|
||||
// add new
|
||||
if (new_line_num !== undefined) {
|
||||
update_monaco_decorations(
|
||||
this.editor,
|
||||
{
|
||||
this.execloc_decoration_id = this.editor.deltaDecorations([], [{
|
||||
range: new Range(new_line_num, 0, new_line_num, 0),
|
||||
options: {
|
||||
className: "quest_editor_AsmEditorView_execution-location",
|
||||
isWholeLine: true,
|
||||
},
|
||||
new_line_num,
|
||||
);
|
||||
}
|
||||
}])[0];
|
||||
}
|
||||
}),
|
||||
|
||||
|
@ -91,7 +91,9 @@ export class AsmEditorStore implements Disposable {
|
||||
private readonly _did_redo = emitter<string>();
|
||||
private readonly _inline_args_mode: WritableProperty<boolean> = property(true);
|
||||
private readonly _breakpoints: WritableListProperty<number> = list_property();
|
||||
private readonly _execution_location: WritableProperty<number | undefined> = property(undefined);
|
||||
private readonly _execution_location: WritableProperty<number | undefined> = property(
|
||||
undefined,
|
||||
);
|
||||
|
||||
readonly model: Property<ITextModel | undefined> = this._model;
|
||||
readonly did_undo: Observable<string> = this._did_undo;
|
||||
@ -181,6 +183,69 @@ export class AsmEditorStore implements Disposable {
|
||||
current_version = version;
|
||||
|
||||
assembly_analyser.update_assembly(e.changes);
|
||||
|
||||
// update breakpoints
|
||||
for (const change of e.changes) {
|
||||
// empty text means something was deleted
|
||||
if (change.text === "") {
|
||||
const num_removed_lines =
|
||||
change.range.endLineNumber - change.range.startLineNumber;
|
||||
|
||||
if (num_removed_lines > 0) {
|
||||
// if a line that has a decoration is removed
|
||||
// monaco will automatically move the decoration
|
||||
// to the line before the change.
|
||||
// we need to reflect this in the state as well.
|
||||
// move breakpoints that were in removed lines
|
||||
// backwards by the number of removed lines.
|
||||
for (
|
||||
let line_num = change.range.startLineNumber + 1;
|
||||
line_num <= change.range.endLineNumber;
|
||||
line_num++
|
||||
) {
|
||||
// line numbers can't go less than 1
|
||||
const new_line_num = Math.max(line_num - num_removed_lines, 1);
|
||||
const breakpoint_idx = this._breakpoints.val.indexOf(line_num);
|
||||
|
||||
// don't add breakpoint if one already exists
|
||||
if (
|
||||
breakpoint_idx > -1 &&
|
||||
!this._breakpoints.val.includes(new_line_num)
|
||||
) {
|
||||
this._breakpoints.splice(breakpoint_idx, 1, new_line_num);
|
||||
}
|
||||
}
|
||||
|
||||
// move breakpoints that are after the affected
|
||||
// lines backwards by the number of removed lines
|
||||
for (let i = 0; i < this._breakpoints.val.length; i++) {
|
||||
if (this._breakpoints.val[i] > change.range.endLineNumber) {
|
||||
this._breakpoints.splice(
|
||||
i,
|
||||
1,
|
||||
this._breakpoints.val[i] - num_removed_lines,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const num_added_lines = change.text.split("\n").length - 1;
|
||||
|
||||
if (num_added_lines > 0) {
|
||||
// move breakpoints that are after the affected lines
|
||||
// forwards by the number of added lines
|
||||
for (let i = 0; i < this.breakpoints.val.length; i++) {
|
||||
if (this._breakpoints.val[i] > change.range.endLineNumber) {
|
||||
this._breakpoints.splice(
|
||||
i,
|
||||
1,
|
||||
this._breakpoints.val[i] + num_added_lines,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
@ -257,6 +322,10 @@ export class AsmEditorStore implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
public clear_breakpoints(): void {
|
||||
this._breakpoints.splice(0);
|
||||
}
|
||||
|
||||
public set_execution_location(line_num: number): void {
|
||||
this._execution_location.val = line_num;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user