Add react-stl-viewer + fix preview vs. render flow
This commit is contained in:
parent
d8cc597eba
commit
42d0d0a5d2
17 changed files with 313 additions and 109 deletions
|
|
@ -36,3 +36,10 @@
|
|||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.logs-container {
|
||||
overflow-y: scroll;
|
||||
height: calc(min(200px, 30vh));
|
||||
position: relative;
|
||||
}
|
||||
108
src/App.tsx
108
src/App.tsx
|
|
@ -1,55 +1,103 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import {ModelContext, State} from './app-state'
|
||||
import Editor, { loader, Monaco } from '@monaco-editor/react';
|
||||
import './App.css';
|
||||
import openscadEditorOptions from './language/openscad-editor-options';
|
||||
import { Model } from './model';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import {StlViewer} from "react-stl-viewer";
|
||||
|
||||
let monacoInstance: Monaco
|
||||
loader.init().then(mi => monacoInstance = mi);
|
||||
|
||||
function EditorPanel() {
|
||||
const model = useContext(ModelContext);
|
||||
const [editor, setEditor] = useState(null as monaco.editor.IStandaloneCodeEditor | null)
|
||||
|
||||
if (editor) {
|
||||
const checkerRun = model.state.lastCheckerRun;
|
||||
const editorModel = editor.getModel();
|
||||
if (editorModel && checkerRun) {
|
||||
monacoInstance.editor.setModelMarkers(editorModel, 'openscad', checkerRun.markers);
|
||||
}
|
||||
}
|
||||
const onMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
editor.addAction({
|
||||
id: "openscad-render",
|
||||
label: "Render OpenSCAD",
|
||||
keybindings: [
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
|
||||
monaco.KeyCode.F6,
|
||||
],
|
||||
run: () => model.render()
|
||||
});
|
||||
setEditor(editor)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="editor-panel" style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<Editor
|
||||
className="openscad-editor"
|
||||
defaultLanguage="openscad"
|
||||
value={model.state.params.source}
|
||||
onChange={s => model.source = s ?? ''}
|
||||
onMount={onMount} // TODO: This looks a bit silly, does it trigger a re-render??
|
||||
options={openscadEditorOptions}
|
||||
height="50vh"/>
|
||||
|
||||
<div className="logs-container">
|
||||
<pre><code id="logs">{model.state.lastCheckerRun?.logText ?? 'No log yet!'}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function App({initialState}: {initialState: State}) {
|
||||
const [state, setState] = useState(initialState);
|
||||
const [editor, setEditor] = useState(null as monaco.editor.IStandaloneCodeEditor | null)
|
||||
const model = new Model(state, setState);
|
||||
|
||||
|
||||
if (editor) {
|
||||
const editorModel = editor.getModel();
|
||||
if (editorModel && state.checkerRun) {
|
||||
monacoInstance.editor.setModelMarkers(editorModel, 'openscad', state.checkerRun.markers);
|
||||
if (editorModel && state.lastCheckerRun) {
|
||||
monacoInstance.editor.setModelMarkers(editorModel, 'openscad', state.lastCheckerRun.markers);
|
||||
}
|
||||
}
|
||||
|
||||
const source = model.source;
|
||||
|
||||
const model = new Model(state, setState);
|
||||
useEffect(() => model.init());
|
||||
|
||||
return (
|
||||
<ModelContext.Provider value={model}>
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src="logo.png" className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://openscad.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn OpenSCAD
|
||||
</a>
|
||||
<Editor
|
||||
className="openscad-editor"
|
||||
defaultLanguage="openscad"
|
||||
value={source}
|
||||
onChange={s => model.source = s ?? ''}
|
||||
onMount={e => setEditor(e)} // TODO: This looks a bit silly, does it trigger a re-render??
|
||||
options={openscadEditorOptions}
|
||||
height="50vh"/>
|
||||
</header>
|
||||
<div style={{display: 'flex', flexDirection: 'column', flex: 1}}>
|
||||
<div style={{display: 'flex', flexDirection: 'row', flex: 1}}>
|
||||
<div style={{width: "50vw"}}>
|
||||
<EditorPanel/>
|
||||
</div>
|
||||
{state.output?.stlFileURL &&
|
||||
<StlViewer
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
showAxes={true}
|
||||
orbitControls
|
||||
shadows
|
||||
url={state.output?.stlFileURL ?? ''}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div style={{display: 'flex', flexDirection: 'row'}}>
|
||||
<button onClick={() => model.render()}>Render</button>
|
||||
{model.state.previewing && 'previewing... '}
|
||||
{model.state.rendering && 'rendering... '}
|
||||
{model.state.checkingSyntax && 'checking syntax... '}
|
||||
</div>
|
||||
</div>
|
||||
</ModelContext.Provider>
|
||||
);
|
||||
|
|
|
|||
13
src/BrowserFS.ts → src/BrowserFS.d.ts
vendored
13
src/BrowserFS.ts → src/BrowserFS.d.ts
vendored
|
|
@ -1,12 +1,15 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
import { FS } from "./filesystem";
|
||||
// import { FS } from "./filesystem";
|
||||
|
||||
interface EmscriptenFS extends FS {
|
||||
|
||||
};
|
||||
declare interface FS {
|
||||
readdir(path: string, cb: (err: any, files: string[]) => void): void;
|
||||
symlink(target: string, source: string): void;
|
||||
}
|
||||
|
||||
export let BrowserFS = (window as any)['BrowserFS'] as {
|
||||
declare interface EmscriptenFS extends FS {}
|
||||
|
||||
declare type BrowserFSInterface = {
|
||||
BFSRequire: (name: string) => any,
|
||||
|
||||
install: (windowOrSelf: Window) => void,
|
||||
|
|
@ -2,19 +2,22 @@
|
|||
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { spawnOpenSCAD } from "./openscad-runner";
|
||||
import { joinMergedOutputs, parseMergedOutputs } from "./output-parser";
|
||||
import { processMergedOutputs } from "./output-parser";
|
||||
import { AbortablePromise, turnIntoDelayableExecution } from './utils';
|
||||
|
||||
const syntaxDelay = 300;
|
||||
|
||||
type SyntaxCheckOutput = {logText: string, markers: monaco.editor.IMarkerData[]};
|
||||
export const checkSyntax = (source: string, callback: (out: SyntaxCheckOutput) => void) =>
|
||||
turnIntoDelayableExecution(syntaxDelay, () => {
|
||||
// const timestamp = Date.now();
|
||||
export const checkSyntax =
|
||||
turnIntoDelayableExecution(syntaxDelay, (source: string) => {
|
||||
// const timestamp = Date.now();
|
||||
|
||||
source = '$preview=true;\n' + source;
|
||||
const sourceFile = 'input.scad';
|
||||
|
||||
const job = spawnOpenSCAD({
|
||||
inputs: [['input.scad', source + '\n']],
|
||||
args: ["input.scad", "-o", "out.ast"],
|
||||
inputs: [[sourceFile, source + '\n']],
|
||||
args: [sourceFile, "-o", "out.ast"],
|
||||
});
|
||||
|
||||
return AbortablePromise<SyntaxCheckOutput>((res, rej) => {
|
||||
|
|
@ -22,9 +25,7 @@ export const checkSyntax = (source: string, callback: (out: SyntaxCheckOutput) =
|
|||
try {
|
||||
const result = await job;
|
||||
// console.log(result);
|
||||
const logText = joinMergedOutputs(result.mergedOutputs);
|
||||
const markers = parseMergedOutputs(result.mergedOutputs);
|
||||
res({logText, markers});
|
||||
res(processMergedOutputs(result.mergedOutputs, {shiftSourceLines: {[sourceFile]: 1}}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
rej(e);
|
||||
|
|
@ -32,25 +33,34 @@ export const checkSyntax = (source: string, callback: (out: SyntaxCheckOutput) =
|
|||
})()
|
||||
return () => job.kill();
|
||||
});
|
||||
}, callback);
|
||||
|
||||
var sourceFileName;
|
||||
// var editor;
|
||||
});
|
||||
|
||||
var renderDelay = 1000;
|
||||
type RenderOutput = {stlFile: File, logText: string, markers: monaco.editor.IMarkerData[], elapsedMillis?: number}
|
||||
export type RenderOutput = {stlFile: File, logText: string, markers: monaco.editor.IMarkerData[], elapsedMillis?: number}
|
||||
|
||||
export const render = (source: string, features: string[], callback: (result: RenderOutput) => void) =>
|
||||
turnIntoDelayableExecution(renderDelay, () => {
|
||||
export type RenderArgs = {
|
||||
source: string,
|
||||
features?: string[],
|
||||
extraArgs?: string[],
|
||||
isPreview?: boolean
|
||||
}
|
||||
export const render =
|
||||
turnIntoDelayableExecution(renderDelay, (args: RenderArgs) => {
|
||||
|
||||
const prefixLines: string[] = [];
|
||||
if (args.isPreview) prefixLines.push('$preview=false;');
|
||||
const source = args.isPreview ? [...prefixLines, args.source].join('\n') : args.source
|
||||
const inputFile = 'input.scad';
|
||||
|
||||
const job = spawnOpenSCAD({
|
||||
// wasmMemory,
|
||||
inputs: [['input.scad', source]],
|
||||
args: [
|
||||
"input.scad",
|
||||
inputFile,
|
||||
"-o", "out.stl",
|
||||
"--export-format=binstl",
|
||||
...features.map(f => `--enable=${f}`),
|
||||
...(args.features ?? []).map(f => `--enable=${f}`),
|
||||
...(args.extraArgs ?? [])
|
||||
],
|
||||
outputPaths: ['out.stl']
|
||||
});
|
||||
|
|
@ -61,8 +71,9 @@ export const render = (source: string, features: string[], callback: (result: Re
|
|||
const result = await job;
|
||||
console.log(result);
|
||||
|
||||
const logText = joinMergedOutputs(result.mergedOutputs);
|
||||
const markers = parseMergedOutputs(result.mergedOutputs);
|
||||
const {logText, markers} = processMergedOutputs(result.mergedOutputs, {
|
||||
shiftSourceLines: {[inputFile]: prefixLines.length}
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
reject(result.error);
|
||||
|
|
@ -87,4 +98,4 @@ export const render = (source: string, features: string[], callback: (result: Re
|
|||
|
||||
return () => job.kill()
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,23 +8,27 @@ import { Model } from "./model"
|
|||
export interface State {
|
||||
params: {
|
||||
source: string,
|
||||
features: string[],
|
||||
},
|
||||
checkerRun?: {
|
||||
lastCheckerRun?: {
|
||||
logText: string,
|
||||
markers: monaco.editor.IMarkerData[]
|
||||
}
|
||||
rendering?: boolean,
|
||||
previewing?: boolean,
|
||||
checkingSyntax?: boolean,
|
||||
|
||||
output?: {
|
||||
path: string,
|
||||
timestamp: number,
|
||||
sizeBytes: number,
|
||||
formattedSize: string,
|
||||
stlFile: File,
|
||||
stlFileURL: string,
|
||||
},
|
||||
};
|
||||
|
||||
export const ModelContext = createContext(new Model(
|
||||
{
|
||||
params: {
|
||||
source: ''
|
||||
source: '',
|
||||
features: ['manifold', 'lazy-union'],
|
||||
}
|
||||
},
|
||||
() => { throw new Error('Not implemented'); }
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
import { State } from "./app-state";
|
||||
import { validateString } from "./utils";
|
||||
import { validateArray, validateString } from "./utils";
|
||||
|
||||
export function writeStateInFragment(state: State) {
|
||||
window.location.hash = encodeURIComponent(JSON.stringify(state));
|
||||
window.location.hash = encodeURIComponent(JSON.stringify(state.params));
|
||||
}
|
||||
export function readStateFromFragment(): State | null {
|
||||
if (window.location.hash.startsWith('#') && window.location.hash.length > 1) {
|
||||
try {
|
||||
const state = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
|
||||
const params = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
|
||||
return {
|
||||
params: {
|
||||
source: validateString(state.params?.source),
|
||||
source: validateString(params?.source),
|
||||
features: validateArray(params?.features, validateString),
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import {App} from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { createEditorFS } from './filesystem';
|
||||
|
|
@ -10,6 +9,7 @@ import { registerOpenSCADLanguage } from './language/openscad-register-language'
|
|||
import { zipArchives } from './zip-archives';
|
||||
import {readStateFromFragment} from './fragment-state'
|
||||
import { State } from './app-state';
|
||||
import './index.css';
|
||||
|
||||
(async () => {
|
||||
|
||||
|
|
@ -23,6 +23,12 @@ import { State } from './app-state';
|
|||
}
|
||||
} as State;
|
||||
|
||||
const defaultFeatures = ['manifold', 'fast-csg', 'lazy-union'];
|
||||
defaultFeatures.forEach(f => {
|
||||
if (initialState.params.features.indexOf(f) < 0)
|
||||
initialState.params.features.push(f);
|
||||
});
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
export default `
|
||||
$fa=undef;
|
||||
$fs=undef;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { FS, readDirAsArray, Symlinks } from '../filesystem';
|
||||
import { readDirAsArray, Symlinks } from '../filesystem';
|
||||
import { ParsedFile, ParsedFunctionoidDef, parseOpenSCAD, stripComments } from './openscad-pseudoparser';
|
||||
import builtinSignatures from './openscad-builtins'
|
||||
import { mapObject } from '../utils';
|
||||
|
|
|
|||
90
src/model.ts
90
src/model.ts
|
|
@ -1,25 +1,32 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
import { checkSyntax } from "./actions";
|
||||
import { checkSyntax, render, RenderArgs, RenderOutput } from "./actions";
|
||||
import { State } from "./app-state";
|
||||
import { bubbleUpDeepMutations } from "./deep-mutate";
|
||||
import { writeStateInFragment } from "./fragment-state";
|
||||
|
||||
export class Model {
|
||||
constructor(private state: State, private setState_: (state: State) => void) {}
|
||||
constructor(public state: State, private setStateCallback?: (state: State) => void) {
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.state.output && !this.state.lastCheckerRun && !this.state.previewing && !this.state.checkingSyntax && !this.state.rendering &&
|
||||
this.state.params.source.trim() != '') {
|
||||
this.processSource();
|
||||
}
|
||||
}
|
||||
|
||||
private setState(state: State) {
|
||||
this.state = state;
|
||||
this.setState_(state);
|
||||
writeStateInFragment(state);
|
||||
this.setStateCallback && this.setStateCallback(state);
|
||||
}
|
||||
|
||||
mutate(f: (state: State) => void) {
|
||||
const state = this.state;
|
||||
const mutated = bubbleUpDeepMutations(state, f);
|
||||
const mutated = bubbleUpDeepMutations(this.state, f);
|
||||
// No matter how deep the mutation happened, the top-level object's identity
|
||||
// will have changed iff the mutated values are different.
|
||||
if (mutated !== state) {
|
||||
if (mutated !== this.state) {
|
||||
this.setState(mutated);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -27,16 +34,73 @@ export class Model {
|
|||
return false;
|
||||
}
|
||||
|
||||
get source(): string {
|
||||
return this.state.params.source;//.source, this.state.editor;//params.source.content;
|
||||
}
|
||||
// get features() { return this.state_.params.features; }
|
||||
|
||||
|
||||
set source(source) {
|
||||
if (this.mutate(s => s.params.source = source)) {
|
||||
checkSyntax(source, checkerRun => this.mutate(s => s.checkerRun = checkerRun))({now: false});
|
||||
// get source(): string { return this.state_.params.source; }
|
||||
|
||||
set source(source: string) {
|
||||
if (this.mutate(s => { s.params.source = source; })) {
|
||||
this.processSource();
|
||||
}
|
||||
}
|
||||
|
||||
private processSource() {
|
||||
this.mutate(s => {
|
||||
s.previewing = true;
|
||||
s.checkingSyntax = true;
|
||||
});
|
||||
checkSyntax(this.state.params.source)({now: false, callback: checkerRun => this.mutate(s => {
|
||||
s.lastCheckerRun = checkerRun;
|
||||
s.checkingSyntax = false;
|
||||
})});
|
||||
render({...this.renderArgs, isPreview: true})({now: false, callback: output => this.handleRenderOutput(output, s => {
|
||||
s.previewing = false;
|
||||
})});
|
||||
}
|
||||
|
||||
// checkSyntax() {
|
||||
// this.mutate(s => s.checkingSyntax = true);
|
||||
// checkSyntax(this.state.params.source)({now: false, callback: checkerRun => this.mutate(s => {
|
||||
// s.lastCheckerRun = checkerRun;
|
||||
// s.checkingSyntax = false;
|
||||
// })});
|
||||
// }
|
||||
// preview() {
|
||||
// this.mutate(s => s.previewing = true);
|
||||
// render({...this.renderArgs, isPreview: true})({now: false, callback: output => this.handleRenderOutput(output, s => {
|
||||
// s.previewing = false;
|
||||
// })});
|
||||
// }
|
||||
|
||||
private handleRenderOutput(output: RenderOutput, extraMutations: (s: State) => void) {
|
||||
this.mutate(s => {
|
||||
s.lastCheckerRun = {
|
||||
logText: output.logText,
|
||||
markers: output.markers,
|
||||
}
|
||||
if (s.output?.stlFileURL) {
|
||||
URL.revokeObjectURL(s.output.stlFileURL);
|
||||
}
|
||||
s.output = {
|
||||
stlFile: output.stlFile,
|
||||
stlFileURL: URL.createObjectURL(output.stlFile),
|
||||
};
|
||||
extraMutations(s);
|
||||
});
|
||||
}
|
||||
|
||||
private get renderArgs(): RenderArgs {
|
||||
const source = this.state.params.source;
|
||||
const features = this.state.params.features;
|
||||
return {source, features, extraArgs: ['-D$preview=true']};
|
||||
}
|
||||
|
||||
render() {
|
||||
this.mutate(s => s.rendering = true);
|
||||
|
||||
render(this.renderArgs)({now: true, callback: output => this.handleRenderOutput(output, s => {
|
||||
s.rendering = false;
|
||||
})})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,32 @@
|
|||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { MergedOutputs } from "./openscad-worker";
|
||||
|
||||
export function joinMergedOutputs(mergedOutputs: MergedOutputs) {
|
||||
const ignoredLogs = new Set([
|
||||
'Could not initialize localization.'
|
||||
]);
|
||||
|
||||
type MergedOutputsOptions = {
|
||||
shiftSourceLines: {[path: string]: number}
|
||||
}
|
||||
export const processMergedOutputs = (outputs: MergedOutputs, opts: MergedOutputsOptions) => ({
|
||||
logText: joinMergedOutputs(outputs, opts),
|
||||
markers: parseMergedOutputs(outputs, opts)
|
||||
});
|
||||
|
||||
export function joinMergedOutputs(mergedOutputs: MergedOutputs, opts: MergedOutputsOptions) {
|
||||
let allLines = [];
|
||||
for (const {stderr, stdout, error} of mergedOutputs){
|
||||
allLines.push(stderr ?? stdout ?? `EXCEPTION: ${error}`);
|
||||
const line = stderr ?? stdout ?? `EXCEPTION: ${error}`;
|
||||
if (ignoredLogs.has(line)) {
|
||||
continue;
|
||||
}
|
||||
allLines.push(line);
|
||||
}
|
||||
|
||||
return allLines.join("\n");
|
||||
}
|
||||
|
||||
export function parseMergedOutputs(mergedOutputs: MergedOutputs): monaco.editor.IMarkerData[] {
|
||||
export function parseMergedOutputs(mergedOutputs: MergedOutputs, opts: MergedOutputsOptions): monaco.editor.IMarkerData[] {
|
||||
let unmatchedLines = [];
|
||||
|
||||
const markers = [];
|
||||
|
|
@ -25,6 +41,10 @@ export function parseMergedOutputs(mergedOutputs: MergedOutputs): monaco.editor.
|
|||
severity: monaco.MarkerSeverity.Error
|
||||
})
|
||||
}
|
||||
const getLine = (path: string, lineStr: string) => {
|
||||
const shift = opts.shiftSourceLines[path] ?? 0;
|
||||
return Number(lineStr) - shift;
|
||||
}
|
||||
for (const {stderr, stdout, error} of mergedOutputs){
|
||||
if (stderr) {
|
||||
if (stderr.startsWith('ERROR:')) errorCount++;
|
||||
|
|
@ -33,14 +53,14 @@ export function parseMergedOutputs(mergedOutputs: MergedOutputs): monaco.editor.
|
|||
let m = /^ERROR: Parser error in file "([^"]+)", line (\d+): (.*)$/.exec(stderr)
|
||||
if (m) {
|
||||
const [_, file, line, error] = m
|
||||
addError(error, file, Number(line));
|
||||
addError(error, file, getLine(file, line));
|
||||
continue;
|
||||
}
|
||||
|
||||
m = /^ERROR: Parser error: (.*?) in file ([^",]+), line (\d+)$/.exec(stderr)
|
||||
if (m) {
|
||||
const [_, error, file, line] = m
|
||||
addError(error, file, Number(line));
|
||||
addError(error, file, getLine(file, line));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -48,9 +68,9 @@ export function parseMergedOutputs(mergedOutputs: MergedOutputs): monaco.editor.
|
|||
if (m) {
|
||||
const [_, warning, file, line] = m
|
||||
markers.push({
|
||||
startLineNumber: Number(line),
|
||||
startLineNumber: getLine(file, line),
|
||||
startColumn: 1,
|
||||
endLineNumber: Number(line),
|
||||
endLineNumber: getLine(file, line),
|
||||
endColumn: 1000,
|
||||
message: warning,
|
||||
severity: monaco.MarkerSeverity.Warning
|
||||
|
|
|
|||
34
src/utils.ts
34
src/utils.ts
|
|
@ -22,25 +22,27 @@ export function AbortablePromise<T>(f: (resolve: (result: T) => void, reject: (e
|
|||
return Object.assign(promise, {kill: kill!});
|
||||
}
|
||||
|
||||
|
||||
export function turnIntoDelayableExecution<T>(delay: number, job: () => AbortablePromise<T>, callback: (result: T) => void) {
|
||||
// <T extends any[]>(...args: T)
|
||||
export function turnIntoDelayableExecution<T extends any[], R>(
|
||||
delay: number,
|
||||
job: (...args: T) => AbortablePromise<R>) {
|
||||
let pendingId: number | null;
|
||||
let runningJobKillSignal: (() => void) | null;
|
||||
|
||||
const doExecute = async () => {
|
||||
if (runningJobKillSignal) {
|
||||
runningJobKillSignal();
|
||||
runningJobKillSignal = null;
|
||||
return (...args: T) => async ({now, callback}: {now: boolean, callback: (result: R) => void}) => {
|
||||
const doExecute = async () => {
|
||||
if (runningJobKillSignal) {
|
||||
runningJobKillSignal();
|
||||
runningJobKillSignal = null;
|
||||
}
|
||||
const abortablePromise = job(...args);
|
||||
runningJobKillSignal = abortablePromise.kill;
|
||||
try {
|
||||
callback(await abortablePromise);
|
||||
} finally {
|
||||
runningJobKillSignal = null;
|
||||
}
|
||||
}
|
||||
const abortablePromise = job();
|
||||
runningJobKillSignal = abortablePromise.kill;
|
||||
try {
|
||||
callback(await abortablePromise);
|
||||
} finally {
|
||||
runningJobKillSignal = null;
|
||||
}
|
||||
}
|
||||
return async ({now}: {now: boolean}) => {
|
||||
if (pendingId) {
|
||||
clearTimeout(pendingId);
|
||||
pendingId = null;
|
||||
|
|
@ -54,7 +56,7 @@ export function turnIntoDelayableExecution<T>(delay: number, job: () => Abortabl
|
|||
}
|
||||
|
||||
export const validateString = (s: string, orElse: () => string = () => '') => s != null && typeof s === 'string' ? s : orElse();
|
||||
export const validateArray = <T>(a: Array<T>, validateElement: (e: T) => T, orElse: () => T[]) => {
|
||||
export const validateArray = <T>(a: Array<T>, validateElement: (e: T) => T, orElse: () => T[] = () => []) => {
|
||||
if (!(a instanceof Array)) return orElse();
|
||||
return a.map(validateElement);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue