From d5b5ee8265ecd3cc263cc0b069fe2ae179afbce2 Mon Sep 17 00:00:00 2001 From: Steve Dunham Date: Wed, 25 Feb 2026 19:41:57 -0800 Subject: [PATCH] Wire web playground to LSP code --- Makefile | 9 +++-- playground/build | 5 +-- playground/src/cmeditor.ts | 45 ++++++++++++------------- playground/src/ipc.ts | 59 +++++++++++++++++++++++++++++---- playground/src/main.ts | 44 ++++++++++++++---------- playground/src/types.ts | 7 ++-- playground/src/worker.ts | 68 ++++++++++++++++++-------------------- src/Commands.newt | 11 ++++-- src/LSP.newt | 24 +++++++++++++- src/Lib/Derive.newt | 16 ++++----- 10 files changed, 185 insertions(+), 103 deletions(-) diff --git a/Makefile b/Makefile index 50daa40..7eea2e8 100644 --- a/Makefile +++ b/Makefile @@ -60,11 +60,14 @@ audit: .PHONY lsp.js: ${SRCS} node newt.js src/LSP.newt -o lsp.js -newt-vscode-lsp/src/newt.js: ${SRCS} - node newt.js src/LSP.newt -o $@ +newt-vscode-lsp/src/newt.js: lsp.js + cp lsp.js $@ + +playground/src/newt.js: lsp.js + cp lsp.js $@ newt-vscode-lsp/dist/lsp.js: newt-vscode-lsp/src/lsp.ts newt-vscode-lsp/src/newt.js (cd newt-vscode-lsp && node esbuild.js) -lsp: newt-vscode-lsp/dist/lsp.js +lsp: newt-vscode-lsp/dist/lsp.js playground/src/newt.js diff --git a/playground/build b/playground/build index dc68271..8092c9d 100755 --- a/playground/build +++ b/playground/build @@ -1,9 +1,10 @@ #!/bin/sh mkdir -p public +echo copy newt +#cp ../lsp.js src/newt.js +(cd .. && make lsp) echo build newt worker esbuild src/worker.ts --bundle --format=esm --platform=browser > public/worker.js esbuild src/frame.ts --bundle --format=esm --platform=browser > public/frame.js -echo copy newt -cp ../newt.js src/newt.js cp -r static/* public (cd samples && zip -r ../public/files.zip .) diff --git a/playground/src/cmeditor.ts b/playground/src/cmeditor.ts index b38a50b..d22934f 100644 --- a/playground/src/cmeditor.ts +++ b/playground/src/cmeditor.ts @@ -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; diff --git a/playground/src/ipc.ts b/playground/src/ipc.ts index 335f02f..719bac5 100644 --- a/playground/src/ipc.ts +++ b/playground/src/ipc.ts @@ -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 = | { 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 { @@ -14,9 +58,9 @@ export interface Message { args: Parameters; } -export interface ResponseMSG { +export interface ResponseMSG { id: number; - result: string; + result: Awaited>; } type Suspense = { @@ -33,7 +77,8 @@ export class IPC { this._postMessage = (msg: Message) => 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) => { + onmessage = (ev: MessageEvent>) => { console.log("GET", ev.data); // Maybe key off of type if (ev.data.id) { diff --git a/playground/src/main.ts b/playground/src/main.ts index 2f353d6..3bf7232 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -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; diff --git a/playground/src/types.ts b/playground/src/types.ts index d581a2b..66844c8 100644 --- a/playground/src/types.ts +++ b/playground/src/types.ts @@ -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 onChange(value: string): unknown getFileName(): string lint(view: EditorView): Promise | Diagnostic[] diff --git a/playground/src/worker.ts b/playground/src/worker.ts index 6e316da..e951dcf 100644 --- a/playground/src/worker.ts +++ b/playground/src/worker.ts @@ -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 = any>(fun:T, args: Parameters): ReturnType => { + 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 (ev: { data: Message }) { 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({id, result}) + } catch (e) { + console.error(e) } + console.log(shim.stdout) }; // hooks for worker.html to override -let sendResponse: (_: ResponseMSG) => void = postMessage; +let sendResponse: (_: ResponseMSG) => void = postMessage; onmessage = handleMessage; diff --git a/src/Commands.newt b/src/Commands.newt index 3eda85d..98d7c74 100644 --- a/src/Commands.newt +++ b/src/Commands.newt @@ -29,7 +29,6 @@ decomposeName fn = then go (x :: acc) xs else (joinBy "/" (xs :< x <>> Nil), joinBy "." acc) - switchModule : FileSource → String → M (Maybe ModContext) switchModule repo modns = do -- TODO processing on hover is expensive, but info is not always there @@ -69,10 +68,18 @@ getHoverInfo repo modns row col = do pure $ HasHover e.fc ("\{show e.name} : \{rpprint Nil ty}") where + -- We don't want to pick up the paren token when on the border + isIdent : BTok → Bool + isIdent (MkBounded (Tok Ident _) _) = True + isIdent (MkBounded (Tok UIdent _) _) = True + isIdent (MkBounded (Tok MixFix _) _) = True + isIdent (MkBounded (Tok Projection _) _) = True + isIdent _ = False + getTok : List BTok → Maybe String getTok Nil = Nothing getTok (tok :: toks) = - if tok.bounds.startCol <= col && (col <= tok.bounds.endCol) + if tok.bounds.startCol <= col && col <= tok.bounds.endCol && isIdent tok then Just $ value tok else getTok toks data FileEdit = MkEdit FC String diff --git a/src/LSP.newt b/src/LSP.newt index 751c469..ac24670 100644 --- a/src/LSP.newt +++ b/src/LSP.newt @@ -16,6 +16,7 @@ import Commands import Lib.ProcessDecl import Lib.Prettier import Lib.Error +import Lib.Compile pfunc js_castArray : Array JSObject → JSObject := `x => x` pfunc js_castInt : Int → JSObject := `x => x` @@ -224,4 +225,25 @@ checkFile fn = unsafePerformIO $ do modifyIORef state $ [ topContext := top ] pure $ jsonToJObject $ JsonArray json -#export updateFile checkFile hoverInfo codeActionInfo +compileJS : String → JSObject +compileJS fn = unsafePerformIO $ do + let (base, modName) = decomposeName fn + st <- readIORef state + when (st.baseDir /= base) $ \ _ => resetState base + repo <- lspFileSource + (Right (top, src)) <- (do + putStrLn "woo" + mod <- processModule emptyFC repo Nil modName + docs <- compile + let src = unlines $ + ( "const bouncer = (f,ini) => { let obj = ini; while (obj.tag) obj = f(obj); return obj.h0 };" + :: Nil) + ++ map (render 90 ∘ noAlt) docs + pure src).runM st.topContext + | Left err => pure $ js_castStr "// \{errorMsg err}" + modifyIORef state [ topContext := top ] + pure $ js_castStr src + + + +#export updateFile checkFile hoverInfo codeActionInfo compileJS diff --git a/src/Lib/Derive.newt b/src/Lib/Derive.newt index 72ae218..399ef7a 100644 --- a/src/Lib/Derive.newt +++ b/src/Lib/Derive.newt @@ -37,10 +37,10 @@ deriveEq fc name = do where arr : Raw → Raw → Raw - arr a b = RPi emptyFC (BI fc "_" Explicit Many) a b + arr a b = RPi fc (BI fc "_" Explicit Many) a b rvar : String → Raw - rvar nm = RVar emptyFC nm + rvar nm = RVar fc nm getExplictNames : SnocList String → Tm → List String getExplictNames acc (Pi fc nm Explicit quant a b) = getExplictNames (acc :< nm) b @@ -49,7 +49,7 @@ deriveEq fc name = do getExplictNames acc _ = acc <>> Nil buildApp : String → List Raw → Raw - buildApp nm nms = foldl (\ t u => RApp emptyFC t u Explicit) (rvar nm) $ nms + buildApp nm nms = foldl (\ t u => RApp fc t u Explicit) (rvar nm) $ nms equate : (Raw × Raw) → Raw equate (a,b) = buildApp "_==_" (a :: b :: Nil) @@ -89,13 +89,13 @@ deriveShow fc name = do where arr : Raw → Raw → Raw - arr a b = RPi emptyFC (BI fc "_" Explicit Many) a b + arr a b = RPi fc (BI fc "_" Explicit Many) a b rvar : String → Raw - rvar nm = RVar emptyFC nm + rvar nm = RVar fc nm lstring : String → Raw - lstring s = RLit emptyFC (LString s) + lstring s = RLit fc (LString s) getExplictNames : SnocList String → Tm → List String getExplictNames acc (Pi fc nm Explicit quant a b) = getExplictNames (acc :< nm) b @@ -104,7 +104,7 @@ deriveShow fc name = do getExplictNames acc _ = acc <>> Nil buildApp : String → List Raw → Raw - buildApp nm nms = foldl (\ t u => RApp emptyFC t u Explicit) (rvar nm) $ nms + buildApp nm nms = foldl (\ t u => RApp fc t u Explicit) (rvar nm) $ nms equate : (Raw × Raw) → Raw equate (a,b) = buildApp "_==_" (a :: b :: Nil) @@ -118,7 +118,7 @@ deriveShow fc name = do let names = getExplictNames Lin ty anames <- map rvar <$> traverse freshName names let left = buildApp "show" $ buildApp nm anames :: Nil - let shows = map (\ nm => RApp emptyFC (rvar "show") nm Explicit) anames + let shows = map (\ nm => RApp fc (rvar "show") nm Explicit) anames let right = case anames of Nil => lstring nm _ =>