online-editor: Improve type safety

Use `monaco.Uri` more in the internal parts of the `EditorWidget`, add
`TextRange` and `TextPosition` types and use them to data around.

Also fix a bug introduced when switching to the tree of items in the
outline editor and another small bug with the file tab name changing for
temporary files. Sorry for mixing this into this patch, but I discover
the issue while working on improving type safety.
This commit is contained in:
Tobias Hunger 2022-10-20 13:46:43 +02:00 committed by Tobias Hunger
parent 0ba8f58076
commit 41bc847151
4 changed files with 94 additions and 130 deletions

View file

@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
// cSpell: ignore lumino mimetypes printerdemo
// cSpell: ignore lumino inmemory mimetypes printerdemo
import { slint_language } from "./highlighting";
import {
@ -10,6 +10,7 @@ import {
DefinitionPosition,
} from "./lsp_integration";
import { FilterProxyReader } from "./proxy";
import { TextPosition, TextRange } from "./text";
import { BoxLayout, TabBar, Title, Widget } from "@lumino/widgets";
import { Message as LuminoMessage } from "@lumino/messaging";
@ -85,16 +86,15 @@ function createModel(
},
};
function tabTitleFromURL(url: string): string {
if (url === "") {
function tabTitleFromURL(url: monaco.Uri): string {
if (url.scheme == "inmemory") {
return "unnamed.slint";
}
try {
const parsed_url = new URL(url);
const path = parsed_url.pathname;
const path = url.path;
return path.substring(path.lastIndexOf("/") + 1);
} catch (e) {
return url;
return url.toString();
}
}
@ -129,9 +129,9 @@ class EditorPaneWidget extends Widget {
_fetch: (_url: string) => Promise<string>,
) => Promise<monaco.editor.IMarkerData[]>;
#onModelRemoved?: (_url: string) => void;
#onModelAdded?: (_url: string) => void;
#onModelSelected?: (_url: string) => void;
#onModelRemoved?: (_url: monaco.Uri) => void;
#onModelAdded?: (_url: monaco.Uri) => void;
#onModelSelected?: (_url: monaco.Uri) => void;
#onModelsCleared?: () => void;
static createNode(): HTMLElement {
@ -195,40 +195,26 @@ class EditorPaneWidget extends Widget {
return this.#editor?.getModel()?.uri.toString();
}
goto_position(
uri: string,
start_line: number,
start_character: number,
end_line?: number,
end_character?: number,
) {
console.log(
"EW: goto_position called",
uri,
start_line,
start_character,
end_line,
end_character,
);
if (end_line == null) {
end_line = start_line;
}
if (end_character == null) {
end_character = start_character;
goto_position(uri: string, position: TextPosition | TextRange) {
const uri_ = monaco.Uri.parse(uri);
let selection: monaco.Range;
if (monaco.Range.isIRange(position)) {
selection = monaco.Range.lift(position as TextRange);
} else {
selection = new monaco.Range(
position.lineNumber,
position.column,
position.lineNumber,
position.column,
);
}
if (!this.set_model(uri)) {
console.log("EditWidget: No model for URL", uri, "found.");
if (!this.set_model(uri_)) {
return;
}
this.#editor?.setSelection({
startLineNumber: start_line,
startColumn: start_character,
endLineNumber: end_line,
endColumn: end_character,
} as monaco.IRange);
this.#editor?.revealLine(start_line);
this.#editor?.setSelection(selection);
this.#editor?.revealLine(selection.startLineNumber);
}
compile() {
@ -269,39 +255,37 @@ class EditorPaneWidget extends Widget {
this.maybe_update_preview_automatically();
});
this.#editor_view_states.set(uri, null);
this.#onModelAdded?.(uri.toString());
this.#onModelAdded?.(uri);
if (monaco.editor.getModels().length === 1) {
this.#base_url = uri.toString();
this.set_model(uri.toString());
this.set_model(uri);
this.update_preview();
}
}
public remove_model(uri: string) {
const uri_ = monaco.Uri.parse(uri);
this.#editor_view_states.delete(uri_);
const model = monaco.editor.getModel(uri_);
public remove_model(uri: monaco.Uri) {
this.#editor_view_states.delete(uri);
const model = monaco.editor.getModel(uri);
if (model != null) {
model.dispose();
this.#onModelRemoved?.(uri_.toString());
this.#onModelRemoved?.(uri);
}
}
public set_model(uri: string): boolean {
const uri_ = monaco.Uri.parse(uri);
public set_model(uri: monaco.Uri): boolean {
const current_model = this.#editor?.getModel();
if (current_model != null) {
this.#editor_view_states.set(uri_, this.#editor?.saveViewState());
this.#editor_view_states.set(uri, this.#editor?.saveViewState());
}
const state = this.#editor_view_states.get(uri_);
const state = this.#editor_view_states.get(uri);
if (this.#editor != null) {
this.#editor.setModel(monaco.editor.getModel(uri_));
this.#editor.setModel(monaco.editor.getModel(uri));
if (state != null) {
this.#editor.restoreViewState(state);
}
this.#editor.focus();
this.#onModelSelected?.(uri_.toString());
this.#onModelSelected?.(uri);
return true;
}
return false;
@ -326,7 +310,8 @@ class EditorPaneWidget extends Widget {
source,
this.#base_url ?? "",
(url: string) => {
return this.fetch_url_content(era, url);
const uri = monaco.Uri.parse(url);
return this.fetch_url_content(era, uri);
},
).then((markers: monaco.editor.IMarkerData[]) => {
if (this.#editor != null) {
@ -378,7 +363,7 @@ class EditorPaneWidget extends Widget {
return Promise.resolve(editor);
}
if (!this.set_model(resource.toString())) {
if (!this.set_model(resource)) {
return Promise.resolve(null);
}
@ -528,37 +513,38 @@ class EditorPaneWidget extends Widget {
this.#onModelsCleared = f;
}
set onModelAdded(f: (_url: string) => void) {
set onModelAdded(f: (_url: monaco.Uri) => void) {
this.#onModelAdded = f;
}
set onModelRemoved(f: (_url: string) => void) {
set onModelRemoved(f: (_url: monaco.Uri) => void) {
this.#onModelRemoved = f;
}
set onModelSelected(f: (_url: string) => void) {
set onModelSelected(f: (_url: monaco.Uri) => void) {
this.#onModelSelected = f;
}
protected async fetch_url_content(era: number, uri: string): Promise<string> {
const uri_ = monaco.Uri.parse(uri);
let model = monaco.editor.getModel(uri_);
protected async fetch_url_content(
era: number,
uri: monaco.Uri,
): Promise<string> {
let model = monaco.editor.getModel(uri);
if (model != null) {
return model.getValue();
}
const response = await fetch(uri);
const response = await fetch(uri.toString());
if (!response.ok) {
return "Failed to access URL: " + response.statusText;
}
const doc = await response.text();
model = monaco.editor.getModel(uri_);
model = monaco.editor.getModel(uri);
if (model != null) {
return model.getValue();
}
if (era == this.#edit_era) {
createModel(doc, uri_);
createModel(doc, uri);
}
return doc;
}
@ -569,14 +555,15 @@ class EditorPaneWidget extends Widget {
}
async read_from_url(url: string): Promise<string> {
return this.fetch_url_content(this.#edit_era, url);
const uri = monaco.Uri.parse(url);
return this.fetch_url_content(this.#edit_era, uri);
}
}
export class EditorWidget extends Widget {
#tab_bar: TabBar<Widget>;
#editor: EditorPaneWidget;
#tab_map: Map<string, Title<Widget>>;
#tab_map: Map<monaco.Uri, Title<Widget>>;
private static createNode(): HTMLDivElement {
const node = document.createElement("div");
@ -607,21 +594,21 @@ export class EditorWidget extends Widget {
this.#tab_bar.clearTabs();
this.#tab_map.clear();
};
this.#editor.onModelAdded = (url: string) => {
this.#editor.onModelAdded = (url: monaco.Uri) => {
const title = this.#tab_bar.addTab({
owner: this,
label: tabTitleFromURL(url),
});
this.#tab_map.set(url, title);
};
this.#editor.onModelRemoved = (url: string) => {
this.#editor.onModelRemoved = (url: monaco.Uri) => {
const title = this.#tab_map.get(url);
if (title != null) {
this.#tab_bar.removeTab(title);
this.#tab_map.delete(url);
}
};
this.#editor.onModelSelected = (url: string) => {
this.#editor.onModelSelected = (url: monaco.Uri) => {
const title = this.#tab_map.get(url);
if (title != null && this.#tab_bar.currentTitle != title) {
this.#tab_bar.currentTitle = title;
@ -713,20 +700,8 @@ export class EditorWidget extends Widget {
];
}
goto_position(
uri: string,
start_line: number,
start_character: number,
end_line?: number,
end_character?: number,
) {
this.#editor?.goto_position(
uri,
start_line,
start_character,
end_line,
end_character,
);
goto_position(uri: string, position: TextPosition | TextRange) {
this.#editor?.goto_position(uri, position);
}
async set_demo(location: string) {

View file

@ -20,6 +20,8 @@ import { OutlineWidget } from "./outline_widget";
import { PropertiesWidget } from "./properties_widget";
import { WelcomeWidget } from "./welcome_widget";
import { TextPosition, TextRange } from "./text";
const commands = new CommandRegistry();
const local_storage_key_layout = "layout_v1";
@ -316,13 +318,9 @@ function main() {
outline.on_goto_position = (
uri: string,
sl: number,
sc: number,
el: number,
ec: number,
pos: TextPosition | TextRange,
) => {
console.log("Index: goto_position called", uri, sl, sc, el, ec);
editor.goto_position(uri, sl, sc, el, ec);
editor.goto_position(uri, pos);
};
return outline;

View file

@ -7,6 +7,7 @@ import { Message } from "@lumino/messaging";
import { Widget } from "@lumino/widgets";
import { MonacoLanguageClient } from "monaco-languageclient";
import { GotoPositionCallback, TextPosition, TextRange } from "./text";
import {
DocumentSymbolRequest,
@ -50,13 +51,7 @@ function set_data(
data: DocumentSymbol[],
parent: HTMLUListElement,
uri: string,
goto_position: (
_uri: string,
_start_line: number,
_start_character: number,
_end_line: number,
_end_column: number,
) => void,
goto_position: GotoPositionCallback,
) {
for (const d of data) {
const row = document.createElement("li");
@ -66,24 +61,25 @@ function set_data(
if (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(d as any).deprecated ||
(d.tags != null && SymbolTag.Deprecated in d.tags)
SymbolTag.Deprecated in (d.tags ?? [])
) {
row.classList.add("deprecated");
}
row.classList.add(SYMBOL_KIND_MAP.get(d.kind) ?? "kind-unknown");
row.innerText = d.name;
row.addEventListener("click", () =>
goto_position(
uri,
d.selectionRange.start.line + 1,
d.selectionRange.start.character,
d.selectionRange.end.line + 1,
d.selectionRange.end.character,
),
const span = document.createElement("span");
span.innerText = d.name;
span.addEventListener("click", () =>
goto_position(uri, {
startLineNumber: d.selectionRange.start.line + 1,
startColumn: d.selectionRange.start.character + 1,
endLineNumber: d.selectionRange.end.line + 1,
endColumn: d.selectionRange.end.character + 1,
} as TextRange),
);
row.appendChild(span);
if (d.children != null) {
const children_parent = document.createElement("ul");
set_data(d.children, children_parent, uri, goto_position);
@ -97,21 +93,8 @@ function set_data(
export class OutlineWidget extends Widget {
#callback: () => [MonacoLanguageClient | undefined, string | undefined];
#intervalId = -1;
#onGotoPosition = (
uri: string,
start_line: number,
start_column: number,
end_line: number,
end_column: number,
) => {
console.log(
"Goto Position ignored:",
uri,
start_line,
start_column,
end_line,
end_column,
);
#onGotoPosition = (uri: string, position: TextPosition | TextRange) => {
console.log("Goto Position ignored:", uri, position);
};
static createNode(): HTMLElement {
@ -154,15 +137,7 @@ export class OutlineWidget extends Widget {
}, 5000);
}
set on_goto_position(
callback: (
_uri: string,
_start_line: number,
_start_character: number,
_end_line: number,
_end_column: number,
) => void,
) {
set on_goto_position(callback: GotoPositionCallback) {
this.#onGotoPosition = callback;
}

View file

@ -0,0 +1,16 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
export type TextRange = monaco.IRange;
export type TextPosition = monaco.IPosition;
export type Uri = monaco.Uri;
export type DocumentAndTextPosition = { uri: string; position: TextPosition };
export type GotoPositionCallback = (
_uri: string,
_position: TextPosition | TextRange,
) => void;
export type PositionChangeCallback = (_pos: DocumentAndTextPosition) => void;