Wire web playground to LSP code

This commit is contained in:
2026-02-25 19:41:57 -08:00
parent f3b8b1b7b5
commit d5b5ee8265
10 changed files with 185 additions and 103 deletions

View File

@@ -234,32 +234,29 @@ export class CMEditor implements AbstractEditor {
});
}),
this.theme.of(EditorView.baseTheme({})),
hoverTooltip((view, pos) => {
hoverTooltip(async (view, pos) => {
let cursor = this.view.state.doc.lineAt(pos);
let line = cursor.number;
let range = this.view.state.wordAt(pos);
console.log(range);
if (range) {
let col = range.from - cursor.from;
let word = this.view.state.doc.sliceString(range.from, range.to);
let entry = this.delegate.getEntry(word, line, col);
if (!entry)
entry = this.delegate.getEntry("_" + word + "_", line, col);
console.log("entry for", word, "is", entry);
if (entry) {
let rval: Tooltip = {
pos: range.head,
above: true,
create: () => {
let dom = document.createElement("div");
dom.className = "tooltip";
dom.textContent = entry.type;
return { dom };
},
};
return rval;
}
let line = cursor.number - 1;
let col = pos - cursor.from;
// let range = this.view.state.wordAt(pos);
console.log('getting hover for ',line, col);
let entry = await this.delegate.getEntry('', line, col)
if (entry) {
let rval: Tooltip = {
// TODO pull in position from LSP (currently it only has the jump-to FC)
pos,
above: true,
create: () => {
let dom = document.createElement("div");
dom.className = "tooltip";
dom.textContent = entry.info;
return { dom };
},
};
return rval;
}
// we'll iterate the syntax tree for word.
// let entry = delegate.getEntry(word, line, col)
return null;

View File

@@ -1,11 +1,55 @@
//// Copy of LSP types
export interface Location { uri: string; range: Range; }
export interface Position { line: number; character: number; }
export interface Range { start: Position; end: Position; }
export interface HoverResult { info: string; location: Location; }
export interface TextEdit { range: Range; newText: string; }
export type DiagnosticSeverity = 1 | 2 | 3 | 4
export interface DiagnosticRelatedInformation { location: Location; message: string; }
export interface Diagnostic {
range: Range
message: string
severity?: DiagnosticSeverity
source?: string
// we don't emit this yet, but I think we will
relatedInformation?: DiagnosticRelatedInformation[]
}
export interface WorkspaceEdit {
changes?: {
[uri: string]: TextEdit[];
}
}
export interface CodeAction {
title: string;
edit?: WorkspaceEdit;
}
export interface BuildResult {
diags: Diagnostic[]
output: string
}
//// IPC Thinger
export type Result<A> =
| { status: "ok"; value: A }
| { status: "err"; error: string };
export interface API {
save(fileName: string, content: string): string;
typeCheck(fileName: string): string;
compile(fileName: string): string;
// Invalidates stuff and writes to an internal cache that overlays the "filesystem"
updateFile(fileName: string, content: string): unknown;
// Run checking, return diagnostics
typeCheck(fileName: string): BuildResult;
// returns True if we need to recheck - usually for files invalidating other files
// The playground rarely hits this situation at the moment
hoverInfo(fileName: string, row: number, col: number): HoverResult | boolean | null;
codeActionInfo(fileName: string, row: number, col: number): CodeAction[] | null;
// we need to add this to the LSP build
compile(fileName: string): string;
}
export interface Message<K extends keyof API> {
@@ -14,9 +58,9 @@ export interface Message<K extends keyof API> {
args: Parameters<API[K]>;
}
export interface ResponseMSG {
export interface ResponseMSG<K extends keyof API> {
id: number;
result: string;
result: Awaited<ReturnType<API[K]>>;
}
type Suspense = {
@@ -33,7 +77,8 @@ export class IPC {
this._postMessage = <K extends keyof API>(msg: Message<K>) =>
newtWorker.postMessage(msg);
// Safari/MobileSafari have small stacks in webworkers.
if (navigator.vendor.includes("Apple")) {
// But support for the frame needs to be fixed
if (navigator.vendor.includes("Apple") && false) {
const workerFrame = document.createElement("iframe");
workerFrame.src = "worker.html";
workerFrame.style.display = "none";
@@ -46,7 +91,7 @@ export class IPC {
}
// Need to handle messages from the other iframe too? Or at least ignore them.
}
onmessage = (ev: MessageEvent<ResponseMSG>) => {
onmessage = <K extends keyof API>(ev: MessageEvent<ResponseMSG<K>>) => {
console.log("GET", ev.data);
// Maybe key off of type
if (ev.data.id) {

View File

@@ -14,7 +14,7 @@ import {
import { CMEditor } from "./cmeditor.ts";
import { deflate } from "./deflate.ts";
import { inflate } from "./inflate.ts";
import { IPC } from "./ipc.ts";
import { IPC, Position } from "./ipc.ts";
import helpText from "./help.md?raw";
import { basicSetup, EditorView } from "codemirror";
import {Compartment, EditorState} from "@codemirror/state";
@@ -81,7 +81,7 @@ if (!state.javascript.value) {
console.log("SEND TO", iframe.contentWindow);
const fileName = state.currentFile.value;
// maybe send fileName, src?
await ipc.sendMessage("save", [fileName, src]);
await ipc.sendMessage("updateFile", [fileName, src]);
let js = await ipc.sendMessage("compile", [fileName]);
state.javascript.value = bundle(js);
}
@@ -210,8 +210,12 @@ interface EditorProps {
initialValue: string;
}
const language: EditorDelegate = {
getEntry(word, _row, _col) {
return topData?.context.find((entry) => entry.name === word);
async getEntry(word, row, col) {
let fileName = state.currentFile.value
let res = await ipc.sendMessage("hoverInfo", [fileName, row, col])
console.log('HOVER', res, 'for', row, col)
if (res == true) return null
return res || null
},
onChange(_value) {
// we're using lint() now
@@ -228,31 +232,37 @@ const language: EditorDelegate = {
let module = src.match(/module\s+([^\s]+)/)?.[1];
if (module) {
// This causes problems with stuff like aoc/...
state.currentFile.value = module.replace(".", "/") + ".newt";
state.currentFile.value = './' + module.replace(".", "/") + ".newt";
}
// This is a little flashy
// state.javascript.value = ''
let fileName = state.currentFile.value;
console.log("FN", fileName);
try {
await ipc.sendMessage("save", [fileName, src]);
let out = await ipc.sendMessage("typeCheck", [fileName]);
setOutput(out);
let markers = processOutput(out);
await ipc.sendMessage("updateFile", [fileName, src]);
let res = await ipc.sendMessage("typeCheck", [fileName]);
let diags: Diagnostic[] = [];
for (let marker of markers) {
let col = marker.startColumn;
for (let marker of res.diags) {
let {start,end} = marker.range
let xlate = (pos: Position): number =>
view.state.doc.line(pos.line + 1).from + pos.character
let line = view.state.doc.line(marker.startLineNumber);
const pos = line.from + col - 1;
let word = view.state.wordAt(pos);
// TODO double check the last two are right
const SEVERITY: Diagnostic["severity"][] = [ "error", "error", "warning", "info", "hint"]
console.error({
from: xlate(start),
to: xlate(end),
severity: SEVERITY[marker.severity ?? 1],
message: marker.message,
})
diags.push({
from: word?.from ?? pos,
to: word?.to ?? pos + 1,
severity: marker.severity,
from: xlate(start),
to: xlate(end),
severity: SEVERITY[marker.severity ?? 1],
message: marker.message,
});
}
setOutput(res.output)
// less flashy version
ipc.sendMessage("compile", [fileName]).then(js => state.javascript.value = bundle(js));
return diags;

View File

@@ -1,7 +1,6 @@
import { EditorView } from "codemirror";
import { linter, Diagnostic } from "@codemirror/lint";
import { Diagnostic } from "@codemirror/lint";
import { HoverResult } from "./ipc";
export interface CompileReq {
id: string
@@ -55,7 +54,7 @@ export interface TopData {
context: TopEntry[];
}
export interface EditorDelegate {
getEntry(word: string, row: number, col: number): TopEntry | undefined
getEntry(word: string, row: number, col: number): Promise<HoverResult | null>
onChange(value: string): unknown
getFileName(): string
lint(view: EditorView): Promise<Diagnostic[]> | Diagnostic[]

View File

@@ -1,52 +1,50 @@
import { shim } from "./emul";
import { API, Message, ResponseMSG } from "./ipc";
import { archive, preload } from "./preload";
import { Main_main } from './newt';
import { LSP_checkFile, LSP_codeActionInfo, LSP_compileJS, LSP_hoverInfo, LSP_updateFile } from './newt';
const LOG = console.log
console.log = (m) => {
LOG(m)
shim.stdout += "\n" + m;
};
// console.log = (m) => {
// LOG(m)
// shim.stdout += "\n" + m;
// };
const invoke = <T extends (...args: any[]) => any>(fun:T, args: Parameters<T>): ReturnType<T> => {
return fun.apply(undefined, args)
}
const api: API = {
// none of these are promises...
updateFile: LSP_updateFile,
typeCheck(filename) {
shim.stdout = ""
let diags = LSP_checkFile(filename);
let output = shim.stdout
return {diags,output}
},
hoverInfo: LSP_hoverInfo,
codeActionInfo: LSP_codeActionInfo,
compile: LSP_compileJS,
}
const handleMessage = async function <K extends keyof API>(ev: { data: Message<K> }) {
LOG("HANDLE", ev.data);
await preload;
shim.archive = archive;
let key = ev.data.key
if (key === 'typeCheck' || key === 'compile') {
let {id, args: [fileName]} = ev.data
LOG(key, fileName)
const outfile = "out.js";
const isCompile = key === 'compile';
if (isCompile)
shim.process.argv = ["browser", "newt", fileName, "-o", outfile, "--top"];
else
shim.process.argv = ["browser", "newt", fileName, "--top"];
shim.stdout = "";
shim.files[outfile] = new TextEncoder().encode("No JS output");
try {
Main_main();
} catch (e) {
// make it clickable in console
console.error(e);
// make it visable on page
shim.stdout += "\n" + String(e);
}
let result = isCompile ? new TextDecoder().decode(shim.files[outfile]) : shim.stdout
sendResponse({id, result})
} else if (key === 'save') {
let {id, args: [fileName, content]} = ev.data
LOG(`SAVE ${content?.length} to ${fileName}`)
shim.files[fileName] = new TextEncoder().encode(content)
LOG('send', {id, result: ''})
sendResponse({id, result: ''})
try {
shim.stdout = ''
let {id, key, args} = ev.data
let result = await invoke(api[key], args)
LOG('got', result)
sendResponse<typeof key>({id, result})
} catch (e) {
console.error(e)
}
console.log(shim.stdout)
};
// hooks for worker.html to override
let sendResponse: <K extends keyof API>(_: ResponseMSG) => void = postMessage;
let sendResponse: <K extends keyof API>(_: ResponseMSG<K>) => void = postMessage;
onmessage = handleMessage;