diff --git a/package.json b/package.json
index 2309445..54c5e9c 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@monaco-editor/loader": "^1.3.2",
"@monaco-editor/react": "^4.4.6",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
diff --git a/src/App.tsx b/src/App.tsx
index 6e5c2ab..2bf2210 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,7 @@
+// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
+
import React from 'react';
-import logo from '../public/logo.png';
+import SCADEditor from './Editor';
import './App.css';
export function App() {
@@ -18,6 +20,8 @@ export function App() {
>
Learn OpenSCAD
+
+
);
diff --git a/src/BrowserFS.ts b/src/BrowserFS.ts
new file mode 100644
index 0000000..7924e58
--- /dev/null
+++ b/src/BrowserFS.ts
@@ -0,0 +1,18 @@
+// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
+
+import { FS } from "./filesystem";
+
+interface EmscriptenFS extends FS {
+
+};
+
+export let BrowserFS = (window as any)['BrowserFS'] as {
+ BFSRequire: (name: string) => any,
+
+ install: (windowOrSelf: Window) => void,
+ configure: (options: any, callback: (e?: any) => void) => void,
+
+ EmscriptenFS: {
+ new(fs: FS, path: string, errnoCodes: object): EmscriptenFS
+ }
+};
diff --git a/src/Editor.tsx b/src/Editor.tsx
new file mode 100644
index 0000000..cc8944e
--- /dev/null
+++ b/src/Editor.tsx
@@ -0,0 +1,36 @@
+// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
+
+import React from 'react';
+import Editor from '@monaco-editor/react';
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+const options: monaco.editor.IStandaloneEditorConstructionOptions = {
+ lineNumbers: 'on',
+ automaticLayout: true,
+ scrollBeyondLastLine: false,
+ fontSize: 12,
+ language: 'openscad',
+ wordWrap: 'on',
+ wrappingStrategy: 'advanced',
+ suggest: {
+ // snippetsPreventQuickSuggestions: false,
+ showStatusBar: true,
+ preview: true,
+ },
+ codeLens: true,
+function SCADEditor({value, ...props}: {value: string, height: string}) {
+
+ // let editor: monaco.editor.IStandaloneCodeEditor;
+
+ // function editorDidMount(e) {
+ // editor = e;
+ // console.log('editorDidMount', monaco.languages.getLanguages(), editor);
+
+ // onMount={editorDidMount} />
+ );
+}
+
+// export function createEditor() {
+// monaco.editor.create(document.getElementById('openscad-editor') as any, options);
+// }
+
+export default SCADEditor;
diff --git a/src/filesystem.ts b/src/filesystem.ts
new file mode 100644
index 0000000..a2c1a22
--- /dev/null
+++ b/src/filesystem.ts
@@ -0,0 +1,104 @@
+// import FS from "browserfs/dist/node/core/FS";
+import { BrowserFS } from "./BrowserFS";
+import { zipArchives } from "./zip-archives";
+
+export interface FS {
+ readdir(path: string, cb: (err: any, files: string[]) => void): void;
+ symlink(target: string, source: string): void;
+}
+
+export type FSMounts = {
+ [n: string]: {fs: string, options: {zipData: Buffer}}
+}
+
+export type Symlinks = {[alias: string]: string};
+
+export function readDirAsArray(fs: FS, path: string): Promise {
+ return new Promise((res, rej) => fs.readdir(path, (err, files) => err ? rej(err) : res(files)));
+}
+
+export async function getBrowserFSLibrariesMounts(archiveNames: string[]) {
+ const Buffer = BrowserFS.BFSRequire('buffer').Buffer;
+ const fetchData = async (url: string) => (await fetch(url)).arrayBuffer();
+ const results: [string, ArrayBuffer][] =
+ await Promise.all(archiveNames.map(async (n: string) => [n, await fetchData(`./libraries/${n}.zip`)]));
+
+ const zipMounts: FSMounts = {};
+ for (const [n, zipData] of results) {
+ zipMounts[n] = {
+ fs: "ZipFS",
+ options: {
+ zipData: Buffer.from(zipData)
+ }
+ }
+ }
+ return zipMounts;
+}
+
+export async function symlinkLibraries(archiveNames: string[], fs: FS, prefix='/libraries', cwd='/tmp') {
+ const createSymlink = async (target: string, source: string) => {
+ // console.log('symlink', target, source);
+ await fs.symlink(target, source);
+ // await symlink(target, source);
+ };
+
+ await Promise.all(archiveNames.map(n => (async () => {
+ if (!(n in zipArchives)) throw `Archive named ${n} invalid (valid ones: ${Object.keys(zipArchives).join(', ')})`;
+ const {symlinks} = (zipArchives)[n];
+ if (symlinks) {
+ for (const from in symlinks) {
+ const to = symlinks[from];
+ const target = to == '.' ? `${prefix}/${n}` : `${prefix}/${n}/${to}`;
+ const source = from.startsWith('/') ? from : `${cwd}/${from}`;
+ await createSymlink(target, source);
+ }
+ } else {
+ await createSymlink(`${prefix}/${n}`, `${cwd}/${n}`);
+ }
+ })()));
+}
+
+function configureAndInstallFS(windowOrSelf: Window, options: any) {
+ return new Promise(async (resolve, reject) => {
+ BrowserFS.install(windowOrSelf);
+ try {
+ BrowserFS.configure(options, function (e: any) {
+ if (e) reject(e);
+ else resolve(null);
+ });
+ } catch (e) {
+ console.error(e);
+ reject(e);
+ }
+ });
+}
+
+export async function createEditorFS(workingDir='/home') {
+ const archiveNames = Object.keys(zipArchives);
+ const librariesMounts = await getBrowserFSLibrariesMounts(archiveNames);
+ const allMounts: FSMounts = {};
+ for (const n in librariesMounts) {
+ allMounts[`${workingDir}/${n}`] = librariesMounts[n];
+ }
+
+ await configureAndInstallFS(window, {
+ fs: "OverlayFS",
+ options: {
+ readable: {
+ fs: "MountableFileSystem",
+ options: {
+ ...allMounts,
+ }
+ },
+ writable: {
+ fs: "InMemory"
+ },
+ },
+ });
+
+ var fs = BrowserFS.BFSRequire('fs');
+ // const symlink = (target, source) => new Promise((res, rej) => fs.symlink(target, source, (err) => err ? rej(err) : res()));
+
+ // await setupLibraries(archiveNames, symlink, '/libraries', workingDir);
+ return fs;
+}
diff --git a/src/index.tsx b/src/index.tsx
index 1fd12b7..ae195a1 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,13 +1,31 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
-import App from './App';
+import {App} from './App';
+import reportWebVitals from './reportWebVitals';
+import { createEditorFS } from './filesystem';
+import { registerOpenSCADLanguage } from './language/openscad-register-language';
+import { zipArchives } from './zip-archives';
+
+(async () => {
+
+ const workingDir = '/home';
+ const fs = await createEditorFS(workingDir);
+ await registerOpenSCADLanguage(fs, workingDir, zipArchives);
+
+ const root = ReactDOM.createRoot(
+ document.getElementById('root') as HTMLElement
+ );
+ root.render(
+
+
+
+ );
+
+ // If you want to start measuring performance in your app, pass a function
+ // to log results (for example: reportWebVitals(console.log))
+ // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
+ reportWebVitals();
+})();
+
-const root = ReactDOM.createRoot(
- document.getElementById('root') as HTMLElement
-);
-root.render(
-
-
-
-);
diff --git a/src/language/openscad-builtins.ts b/src/language/openscad-builtins.ts
new file mode 100644
index 0000000..601af8c
--- /dev/null
+++ b/src/language/openscad-builtins.ts
@@ -0,0 +1,106 @@
+// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
+export default `
+ $fa=undef;
+ $fs=undef;
+ $fn=undef;
+ $t=undef;
+
+ $preview=undef;
+
+ // shows rotation
+ $vpr=undef;
+ // shows translation (i.e. won't be affected by rotate and zoom)
+ $vpt=undef;
+ // shows the FOV of the view [Note: Requires version 2021.01]
+ $vpf=undef;
+ // shows the camera distance [Note: Requires version 2015.03]
+ $vpd=undef;
+
+ PI=undef;
+
+ function abs(x) = x;
+ function acos(x) = x;
+ function asin(x) = x;
+ function atan(x) = x;
+ function atan2(y, x) = x;
+ function ceil(x) = x;
+ function chr(x) = 0;
+ function len(assignments) = $children;
+ function let(x) = x;
+ function ln(x) = x;
+ function log(x) = x;
+ function lookup(key, array) = x;
+ function max(values) = x;
+ function min(values) = x;
+ function sqrt(x) = x;
+ function tan(degrees) = x;
+ function rands(min_value, max_value, value_count, seed_value=undef) = x;
+ function search(match_value, string_or_vector, num_returns_per_match=1, index_col_num=0) = x;
+ function ord(x) = x;
+ function round(x) = x;
+ function sign(x) = x;
+ function sin(degrees) = x;
+ function str(values) = x;
+ function norm(x) = x;
+ function pow(base, exponent) = x;
+ function concat(values) = x;
+ function cos(degrees) = x;
+ function cross(a, b) = x;
+ function floor(x) = x;
+ function exp(x) = x;
+ function chr(x) = x;
+ function is_undef(x) = x;
+ function is_list(x) = x;
+ function is_num(x) = x;
+ function is_bool(x) = x;
+ function is_string(x) = x;
+ function is_function(x) = x;
+
+ function version() = '';
+ function version_num() = 0;
+
+ $parent_modules=0;
+ module parent_module(n) {}
+
+ module children() {}
+
+ module render(convexity=undef) {}
+ module surface(file, center=false, invert=false, convexity=undef) {}
+
+ function assert(condition, message=undef) = children();
+ module assert(condition, message=undef) children();
+
+ module cube(size, center=false) {}
+ module sphere(r, d=undef, $fa, $fs, $fn) {}
+ module cylinder(h, r, r1=undef, r2, d, d1, d2, center=false, $fa, $fs, $fn) {}
+ module polyhedron(points, faces, convexity=undef) {}
+
+ module square(size, center=false) {}
+ module circle(r, d=undef, $fa, $fs, $fn) {}
+ module polygon(points, paths, convexity=undef) {}
+ module linear_extrude(height, center=false, twist=undef, slices=undef, scale=undef, convexity=undef) children();
+ module rotate_extrude(degrees, convexity=undef, $fa, $fs, $fn) children();
+
+ module scale(v) children();
+ module resize(newsize, auto=false) children();
+ module rotate(a, v=undef) children();
+ module translate(v) children();
+ module mirror(v) children();
+ module multmatrix(m) children();
+
+ module color(c, alpha) children();
+
+ module offset(r, delta=undef, chamfer) children();
+
+ module minkowski() children();
+ module union() children();
+ module difference() children();
+ module intersection() children();
+ module hull() children();
+
+ module children() {}
+
+ // module for(i=undef) children();
+
+ module import(file, convexity=undef, $fn, $fa, $fs) {}
+`;
\ No newline at end of file
diff --git a/src/language/openscad-completions.ts b/src/language/openscad-completions.ts
new file mode 100644
index 0000000..10221da
--- /dev/null
+++ b/src/language/openscad-completions.ts
@@ -0,0 +1,288 @@
+// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
+import FS from 'browserfs/dist/node/core/FS';
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+import { FSMounts, readDirAsArray, Symlinks } from '../filesystem';
+import { ParsedFile, ParsedFunctionoidDef, parseOpenSCAD, stripComments } from './openscad-pseudoparser';
+import builtinSignatures from './openscad-builtins'
+import { mapObject } from '../utils';
+import { ZipArchives } from '../zip-archives';
+import openscadLanguage from './openscad-language';
+
+function makeFunctionoidSuggestion(name: string, mod: ParsedFunctionoidDef) {
+ const argSnippets: string[] = [];
+ const namedArgs: string[] = [];
+ let collectingPosArgs = true;
+ let i = 0;
+ for (const param of mod.params ?? []) {
+ if (collectingPosArgs) {
+ if (param.defaultValue != null) {
+ collectingPosArgs = false;
+ } else {
+ //argSnippets.push(`${param.name}=${'${' + (i + 1) + ':' + param.name + '}'}`);
+ argSnippets.push(`${param.name.replaceAll('$', '\\$')}=${'${' + (++i) + ':' + param.name + '}'}`);
+ continue;
+ }
+ }
+ namedArgs.push(param.name);
+ }
+ if (namedArgs.length) {
+ argSnippets.push(`${'${' + (++i) + ':' + namedArgs.join('|') + '=}'}`);
+ }
+ let insertText = `${name.replaceAll('$', '\\$')}(${argSnippets.join(', ')})`;
+ if (mod.referencesChildren !== null) {
+ insertText += mod.referencesChildren ? ' ${' + (++i) + ':children}' : ';';
+ }
+ return {
+ label: mod.signature,//`${name}(${(mod.params ?? []).join(', ')})`,
+ kind: monaco.languages.CompletionItemKind.Function,
+ insertText,
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
+ };
+}
+
+const builtinCompletions = [
+ ...[true, false].map(v => ({
+ label: `${v}`,
+ kind: monaco.languages.CompletionItemKind.Value,
+ insertText: `${v}`,
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
+ })),
+ ...openscadLanguage.language.keywords.map(v => ({
+ label: v,
+ kind: monaco.languages.CompletionItemKind.Function,
+ insertText: v,
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
+ }))
+];
+
+const keywordSnippets = [
+ 'for(${1:variable}=[${2:start}:${3:end}) ${4:body}',
+ 'for(${1:variable}=[${2:start}:${3:increment}:${4:end}) ${5:body}',
+ 'if (${1:condition}) {\n\t$0\n} else {\n\t\n}'
+];
+
+function cleanupVariables(snippet: string) {
+ return snippet
+ .replaceAll(/\$\{\d+:([$\w]+)\}/g, '$1')
+ .replaceAll(/\$\d+/g, '')
+ .replaceAll(/\s+/g, ' ')
+ .trim();
+}
+
+// https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages
+export async function buildOpenSCADCompletionItemProvider(fs: FS, workingDir: string, zipArchives: ZipArchives) {
+
+ const parsedFiles: {[path: string]: Promise} = {};
+ const toAbsolutePath = (path: string) => path.startsWith('/') ? path : `${workingDir}/${path}`;
+
+ const allSymlinks: Symlinks = {};
+ for (const n of Object.keys(zipArchives)) {
+ if (n == 'fonts') {
+ continue;
+ }
+ const { symlinks } = zipArchives[n];
+ for (const s in symlinks) {
+ allSymlinks[s] = `${n}/${symlinks[s]}`;
+ }
+ }
+ async function readFile(path: string) {
+ if (path in allSymlinks) {
+ path = allSymlinks[path];
+ }
+ path = toAbsolutePath(path);
+ try {
+ const bytes = await fs.readFileSync(path);
+ const src = new TextDecoder("utf-8").decode(bytes);
+ return src;
+ } catch (e) {
+ throw e;
+ }
+ }
+ const builtinsPath = '';
+ let builtinsDefs: ParsedFile;
+
+ function getParsed(path: string, src: string, {skipPrivates, addBuiltins}: {skipPrivates: boolean, addBuiltins: boolean}) {
+ return parsedFiles[path] ??= new Promise(async (res, rej) => {
+ if (src == null) {
+ src = await readFile(path);
+ }
+ const result: ParsedFile = {
+ functions: {},
+ modules: {},
+ vars: [],
+ includes: [],
+ uses: [],
+ }
+
+ const mergeDefinitions = (isUse: boolean, defs: ParsedFile) => {
+ result.functions = {...result.functions, ...defs.functions }
+ result.modules = {...result.modules, ...defs.modules }
+ if (!isUse) {
+ result.vars = [...result.vars, ...defs.vars]
+ }
+ };
+ const dir = (path.split('/').slice(0, -1).join('/') || '.') + '/';
+
+ const handleInclude = async (isUse: boolean, otherPath: string) => {
+ for (const path of [`${dir}/${otherPath}`, otherPath]) {
+ try {
+ const otherSrc = await readFile(otherPath);
+ const sub = await getParsed(otherPath, otherSrc, {skipPrivates: true, addBuiltins: false});
+ mergeDefinitions(isUse, sub);
+ } catch (e) {
+ // console.warn(path, e);
+ }
+ }
+ console.error('Failed to find ', otherPath, '(context imported in ', path, ')');
+ };
+
+ if (addBuiltins && path != builtinsPath) {
+ mergeDefinitions(false, builtinsDefs);
+ }
+
+ const ownDefs = parseOpenSCAD(path, src, skipPrivates);
+
+ await Promise.all(
+ [
+ ...(ownDefs.uses ?? []).map(p => [p, true] as [string, boolean]),
+ ...(ownDefs.includes ?? []).map(p => [p, false] as [string, boolean])
+ ].map(([otherPath, isUse]) => handleInclude(isUse, otherPath)));
+
+ mergeDefinitions(false, ownDefs);
+
+ res(result);
+ });
+ }
+
+ builtinsDefs = await getParsed(builtinsPath, builtinSignatures, {skipPrivates: false, addBuiltins: false});
+
+ return {
+ triggerCharacters: ["<", "/"], //, "\n"],
+ //provideCompletionItems: (async (model, position, context, token) => {
+ provideCompletionItems: ((async (model: monaco.editor.ITextModel, position: monaco.Position, context: monaco.languages.CompletionContext, token: monaco.CancellationToken) => {
+ try {
+ const {word} = model.getWordUntilPosition(position);
+ const offset = model.getOffsetAt(position);
+ const text = model.getValue();
+ let previous = text.substring(0, offset);
+ let i = previous.lastIndexOf('\n');
+ previous = previous.substring(i + 1);
+
+ const includeMatch = /\b(include|use)\s*<([^<>\n"]*)$/.exec(previous);
+ if (includeMatch) {
+ const prefix = includeMatch[2];
+ let folder, filePrefix, folderPrefix;
+ const i = prefix.lastIndexOf('/');
+ if (i < 0) {
+ folderPrefix = '';
+ filePrefix = prefix;
+ } else {
+ folderPrefix = prefix.substring(0, i);
+ filePrefix = prefix.substring(i + 1);
+ }
+ folder = workingDir + (folderPrefix == '' ? '' : '/' + folderPrefix);
+ let files = folderPrefix == '' ? [...Object.keys(allSymlinks)] : [];
+ try {
+ files = [...(await readDirAsArray(fs, folder) ?? []), ...files];
+ // console.log('readDir', folder, files);
+ } catch (e) {
+ console.error(e);
+ }
+
+ const suggestions = [];
+ for (const file of files) {
+ if (filePrefix != '' && !file.startsWith(filePrefix)) {
+ continue;
+ }
+ if (/^(LICENSE.*|fonts)$/.test(file)) {
+ continue;
+ }
+ if (folderPrefix == '' && (file in zipArchives) && zipArchives[file].symlinks) {
+ continue;
+ }
+ const isFolder = !file.endsWith('.scad');
+ const completion = file + (isFolder ? '' : '>\n'); // don't append '/' as it's a useful trigger char
+
+ console.log(JSON.stringify({
+ prefix,
+ folder,
+ filePrefix,
+ folderPrefix,
+ // files,
+ completion,
+ file,
+ }, null, 2));
+
+ suggestions.push({
+ label: file,
+ kind: isFolder ? monaco.languages.CompletionItemKind.Folder : monaco.languages.CompletionItemKind.File,
+ insertText: completion
+ });
+ }
+ suggestions.sort();
+
+ return { suggestions };
+ }
+
+ const inputFile = workingDir + "/foo.scad";
+ delete parsedFiles[inputFile];
+ const parsed = await getParsed(inputFile, text, {skipPrivates: false, addBuiltins: true});
+ // console.log("PARSED", JSON.stringify(parsed, null, 2));
+
+ type CompletionItem = monaco.languages.CompletionItem & {
+ range?: monaco.IRange
+ }
+
+ const previousWithoutComments = stripComments(previous);
+ // console.log('previousWithoutComments', previousWithoutComments);
+ const statementMatch = /(^|.*?[{});]|>\s*\n)\s*([$\w]*)$/m.exec(previousWithoutComments);
+ if (statementMatch) {
+ const start = statementMatch[1];
+ const suggestions: CompletionItem[] = [
+ ...builtinCompletions,
+ ...mapObject(
+ parsed.modules ?? {},
+ (name, mod) => makeFunctionoidSuggestion(name, mod),
+ name => name.indexOf(word) >= 0),
+ ...((parsed.vars ?? []).filter(name => name.indexOf(word) >= 0).map(name => ({
+ label: name,
+ kind: monaco.languages.CompletionItemKind.Variable,
+ insertText: name.replaceAll('$', '\\$'),
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
+ }))),
+ ...keywordSnippets.map(snippet => ({
+ label: cleanupVariables(snippet).replaceAll(/ body/g, ''),
+ kind: monaco.languages.CompletionItemKind.Keyword,
+ insertText: snippet,
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
+ })),
+ // ...getStatementSuggestions().filter(s => start == '' || s.insertText.indexOf(start) >= 0)
+ ];
+ suggestions.sort((a, b) => a.insertText.indexOf(start) - b.insertText.indexOf(start));
+ return { suggestions };
+ }
+
+ const allWithoutComments = stripComments(text);
+
+ const named: [string, CompletionItem][] = [
+ ...mapObject(parsed.functions ?? {},
+ (name, mod) => [name, makeFunctionoidSuggestion(name, mod)],
+ name => name.indexOf(word) >= 0)
+ ];
+ named.sort(([a], [b]) => a.indexOf(word) - b.indexOf(word));
+ // const suggestions = names.map(name => ({
+ // label: name,
+ // kind: monaco.languages.CompletionItemKind.Constant,
+ // insertText: name
+ // }));
+
+ const suggestions = named.map(([n, s]) => s as any as monaco.languages.CompletionItem);
+ return { suggestions };
+
+ } catch (e) {
+ console.error(e);//, (e as any).stackTrace);
+ return { suggestions: [] };
+ }
+ }) as any),
+ } as monaco.languages.CompletionItemProvider;
+}
diff --git a/src/language/openscad-language.ts b/src/language/openscad-language.ts
new file mode 100644
index 0000000..bc30d92
--- /dev/null
+++ b/src/language/openscad-language.ts
@@ -0,0 +1,186 @@
+// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
+import FS from 'browserfs/dist/node/core/FS';
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+
+const builtInFunctionNames = [
+ 'abs',
+ 'acos', 'asin', 'atan', 'atan2', 'ceil',
+ 'len', 'let', 'ln', 'log',
+ 'lookup', 'max', 'min', 'sqrt', 'tan', 'rands',
+ 'search', 'sign', 'sin', 'str', 'norm', 'pow',
+ 'concat', 'cos', 'cross', 'floor', 'exp',
+ 'chr',
+];
+const builtInModuleNames = [
+ 'children',
+ 'circle', 'color', 'cube', 'cylinder',
+ 'diameter', 'difference', 'echo', 'extrude',
+ 'for', 'function', 'hull', 'if', 'include',
+ 'intersection_for', 'intersection', 'linear', 'minkowski', 'mirror', 'module', 'multmatrix',
+ 'offset', 'polyhedron', 'projection', 'radius',
+ 'render', 'resize', 'rotate', 'round', 'scale',
+ 'sphere', 'square', 'surface', 'translate',
+ 'union', 'use', 'value', 'version',
+ // 'center', 'width', 'height',
+];
+const builtInVarNames = [
+ 'false', 'true', 'PI', 'undef', '$children',
+ '$fa', '$fn', '$fs', '$t', '$vpd', '$vpr', '$vpt',
+]
+
+var conf: monaco.languages.LanguageConfiguration = {
+
+ colorizedBracketPairs: [['{', '}'], ['(', ')'], ['[', ']']],
+
+ wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
+ comments: {
+ lineComment: "//",
+ blockComment: ["/*", "*/"]
+ },
+ brackets: [
+ ["{", "}"],
+ ["[", "]"],
+ ["(", ")"]
+ ],
+ onEnterRules: [
+ {
+ beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
+ afterText: /^\s*\*\/$/,
+ action: {
+ indentAction: monaco.languages.IndentAction.IndentOutdent,
+ appendText: " * "
+ }
+ },
+ {
+ beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
+ action: {
+ indentAction: monaco.languages.IndentAction.None,
+ appendText: " * "
+ }
+ },
+ {
+ beforeText: /^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/,
+ action: {
+ indentAction: monaco.languages.IndentAction.None,
+ appendText: "* "
+ }
+ },
+ {
+ beforeText: /^(\t|(\ \ ))*\ \*\/\s*$/,
+ action: {
+ indentAction: monaco.languages.IndentAction.None,
+ removeText: 1
+ }
+ }
+ ],
+ autoClosingPairs: [
+ { open: "{", close: "}" },
+ { open: "[", close: "]" },
+ { open: "(", close: ")" },
+ { open: '"', close: '"', notIn: ["string"] },
+ { open: "'", close: "'", notIn: ["string", "comment"] },
+ { open: "`", close: "`", notIn: ["string", "comment"] },
+ { open: "/**", close: " */", notIn: ["string"] }
+ ],
+ folding: {
+ markers: {
+ start: new RegExp("^\\s*//\\s*#?region\\b"),
+ end: new RegExp("^\\s*//\\s*#?endregion\\b")
+ }
+ }
+};
+
+var language: monaco.languages.IMonarchLanguage = {
+ defaultToken: "invalid",
+ tokenPostfix: ".js",
+ keywords: [...builtInFunctionNames, ...builtInModuleNames, ...builtInVarNames, 'each'],
+ typeKeywords: [],
+ operators: [
+ "<=",
+ ">=",
+ "==",
+ "!=",
+ "=>",
+ "+",
+ "-",
+ "*",
+ "/",
+ "%",
+ "<<",
+ ">>",
+ ">>>",
+ "&",
+ "|",
+ "^",
+ "!",
+ "&&",
+ "||",
+ "?",
+ ":",
+ "=",
+ ],
+ symbols: /[=>](?!@symbols)/, "@brackets"],
+ [/!(?=([^=]|$))/, "delimiter"],
+ [
+ /@symbols/,
+ {
+ cases: {
+ "@operators": "delimiter",
+ "@default": ""
+ }
+ }
+ ],
+ [/(@digits)[eE]([\-+]?(@digits))?/, "number.float"],
+ [/(@digits)\.(@digits)([eE][\-+]?(@digits))?/, "number.float"],
+ [/(@digits)n?/, "number"],
+ [/[;,.]/, "delimiter"],
+ [/"([^"\\]|\\.)*$/, "string.invalid"],
+ [/'([^'\\]|\\.)*$/, "string.invalid"],
+ [/"/, "string", "@string_double"],
+ ],
+ whitespace: [
+ [/[ \t\r\n]+/, ""],
+ [/\/\*/, "comment", "@comment"],
+ [/\/\/.*$/, "comment"]
+ ],
+ comment: [
+ [/[^\/*]+/, "comment"],
+ [/\*\//, "comment", "@pop"],
+ [/[\/*]/, "comment"]
+ ],
+ string_double: [
+ [/[^\\"]+/, "string"],
+ [/@escapes/, "string.escape"],
+ [/\\./, "string.escape.invalid"],
+ [/"/, "string", "@pop"]
+ ],
+ bracketCounting: [
+ [/\{/, "delimiter.bracket", "@bracketCounting"],
+ [/\}/, "delimiter.bracket", "@pop"],
+ { include: "common" }
+ ]
+ }
+};
+
+export default {
+ conf,
+ language,
+}
diff --git a/src/language/openscad-pseudoparser.ts b/src/language/openscad-pseudoparser.ts
new file mode 100644
index 0000000..10af1f6
--- /dev/null
+++ b/src/language/openscad-pseudoparser.ts
@@ -0,0 +1,68 @@
+// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
+export type ParsedFunctionoidDef = {
+ path: string,
+ name: string,
+ params?: {
+ name: string,
+ defaultValue: string,
+ }[],
+ signature: string,
+ referencesChildren: boolean | null,
+};
+export type ParsedFunctionoidDefs = {[name: string]: ParsedFunctionoidDef};
+
+export type ParsedFile = {
+ functions: ParsedFunctionoidDefs,
+ modules: ParsedFunctionoidDefs,
+ vars: string[],
+ includes: string[],
+ uses: string[],
+};
+
+export const stripComments = (src: string) => src.replaceAll(/\/\*(.|[\s\S])*?\*\/|\/\/.*?$/gm, '');
+
+export function parseOpenSCAD(path: string, src: string, skipPrivates: boolean): ParsedFile {
+ const withoutComments = stripComments(src);
+ const vars = [];
+ const functions: ParsedFunctionoidDefs = {};
+ const modules: ParsedFunctionoidDefs = {};
+ const includes: string[] = [];
+ const uses: string[] = [];
+ for (const m of withoutComments.matchAll(/(use|include)\s*<([^>]+)>/g)) {
+ (m[1] == 'use' ? uses : includes).push(m[2]);
+ }
+ for (const m of withoutComments.matchAll(/(?:^|[{};])\s*([$\w]+)\s*=/g)) {
+ vars.push(m[1]);
+ }
+ for (const m of withoutComments.matchAll(/(function|module)\s+([$\w]+)\s*\(([^)]*)\)(?:\s*(?:=\s*)?(\{\}|[^{}]+?;))?/gm)) {
+ const type = m[1];
+ const name = m[2];
+ if (skipPrivates && name.startsWith('_')) {
+ continue;
+ }
+ const paramsStr = m[3];
+ const optBody = m[4];
+ const params = [];
+ if (/^(\s*([$\w]+(\s*=[^,()[]+)?(\s*,\s*[$\w]+(\s*=[^,()[]+)?)*)?\s*)$/m.test(paramsStr)) {
+ for (const paramStr of paramsStr.split(',')) {
+ const am = /^\s*([$\w]+)(?:\s*=([^,()[]+))?\s*$/.exec(paramStr);
+ if (am) {
+ const paramName = am[1];
+ const defaultValue = am[2];
+ params.push({
+ name: paramName,
+ defaultValue
+ });
+ }
+ }
+ }
+ (type == 'function' ? functions : modules)[name] = {
+ path,
+ name,
+ signature: `${name}(${paramsStr.replaceAll(/[\s]+/gm, ' ').replaceAll(/\b | \b/g, '')})`,
+ params,
+ referencesChildren: optBody != null ? (optBody.indexOf('children()') >= 0) : null,
+ };
+ }
+ return {vars, functions, modules, includes, uses};
+}
diff --git a/src/language/openscad-register-language.ts b/src/language/openscad-register-language.ts
new file mode 100644
index 0000000..79ac6f2
--- /dev/null
+++ b/src/language/openscad-register-language.ts
@@ -0,0 +1,26 @@
+// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
+
+import loader from '@monaco-editor/loader';
+import { ZipArchives } from '../zip-archives';
+import { buildOpenSCADCompletionItemProvider } from './openscad-completions';
+import openscadLanguage from './openscad-language';
+// import jsLanguage from 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'
+
+// https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages
+export async function registerOpenSCADLanguage(fs: any, workingDir: string, zipArchives: ZipArchives) {
+ const monaco = await loader.init();
+
+ monaco.languages.register({
+ id: 'openscad',
+ extensions: ['.scad'],
+ mimetypes: ["text/openscad"],
+ });
+
+ //const [inheritLanguage] = monaco.languages.getLanguages().filter(l => l.id === 'javascript');
+ const { conf, language } = openscadLanguage;
+ monaco.languages.setLanguageConfiguration('openscad', conf);
+ monaco.languages.setMonarchTokensProvider('openscad', language);
+
+ monaco.languages.registerCompletionItemProvider('openscad',
+ await buildOpenSCADCompletionItemProvider(fs, workingDir, zipArchives));
+}
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..91bb486
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,12 @@
+// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
+
+export function mapObject(o: any, f: (key: string, value: any) => any, ifPred: (key: string) => boolean) {
+ const ret = [];
+ for (const key of Object.keys(o)) {
+ if (ifPred && !ifPred(key)) {
+ continue;
+ }
+ ret.push(f(key, o[key]));
+ }
+ return ret;
+}
diff --git a/src/zip-archives.ts b/src/zip-archives.ts
new file mode 100644
index 0000000..d2e0dc7
--- /dev/null
+++ b/src/zip-archives.ts
@@ -0,0 +1,50 @@
+// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
+
+import { Symlinks } from "./filesystem";
+
+export type ZipArchives = {
+ [name: string]: {
+ symlinks?: Symlinks
+ }
+};
+
+export const zipArchives: ZipArchives = {
+ 'fonts': {},
+ // @openscad
+ 'MCAD': {},
+ // @revarbat
+ 'BOSL': {},
+ 'BOSL2': {
+ // "includes": {
+ // "BOSL2/std.scad": "The Belfry OpenScad Library, v2.0. An OpenSCAD library of shapes, masks, and manipulators to make working with OpenSCAD easier. BETA"
+ // }
+ },
+ // @nophead
+ 'NopSCADlib': {},
+ // @thehans
+ 'FunctionalOpenSCAD': {},
+ 'funcutils': {},
+ // @colyer
+ 'smooth-prim': {
+ symlinks: {'smooth_prim.scad': 'smooth_prim.scad'},
+ },
+ 'closepoints': {
+ symlinks: {'closepoints.scad': 'closepoints.scad'},
+ },
+ 'plot-function': {
+ symlinks: {'plot_function.scad': 'plot_function.scad'},
+ },
+ // 'threads': {},
+ // @sofian
+ 'openscad-tray': {
+ symlinks: {'tray.scad': 'tray.scad'},
+ },
+ // @mrWheel
+ 'YAPP_Box': {},
+ // @Cantareus
+ 'Stemfie_OpenSCAD': {},
+ // @UBaer21
+ 'UB.scad': {
+ symlinks: {"ub.scad": "libraries/ub.scad"},
+ },
+};
diff --git a/tsconfig.json b/tsconfig.json
index 37b9b60..330f000 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,10 +1,10 @@
{
"compilerOptions": {
- "target": "ES2020",
+ "target": "ES2021",
"lib": [
"dom",
"dom.iterable",
- "esnext",
+ "ES2021",
"WebWorker",
],
"outDir": "js",