cf9548e9ed
Adds Microsoft.CodeAnalysis.CSharp.Scripting (4.13.0). Scripts are
compiled as C# script fragments against a ScriptHost globals type
that mirrors what the runtime exposes (Parameters bag, CallShared,
CallScript) — Roslyn reads the signatures so those identifiers are
in scope for analysis without executing anything.
ScriptAnalysisService:
- Diagnose(code): Compilation.GetDiagnostics() projected to
Monaco-shaped DiagnosticMarker records (severity 8/4/2/1).
- Complete(code, line, col): dot-member lookup via SemanticModel
when the token at position is part of a MemberAccessExpression;
falls back to LookupSymbols at position for the general case.
Two endpoints exposed by the existing CentralUI endpoint pipeline,
both behind RequireDesign policy:
POST /api/script-analysis/diagnostics
POST /api/script-analysis/completions
monaco-init.js registers a csharp CompletionItemProvider with dot/
paren/quote trigger chars, plus a 500 ms debounced diagnostics pass
on every keystroke that pushes markers via setModelMarkers. Initial
pass fires on editor create so existing scripts surface errors right
away. Auth uses the existing cookie via credentials: same-origin.
Smoke-verified:
- Typing `DateTimeOffset.UtcNow` (no semicolon) shows the missing
semicolon squiggle in real time.
- Ctrl-Space at file scope returns the full type universe
(AccessViolationException, Action, Akka, AppDomain, ...).
Wave 2 of three. SCADA-specific extensions (declared param keys,
shared/sibling script names, forbidden-API diagnostic) follow.
173 lines
6.2 KiB
JavaScript
173 lines
6.2 KiB
JavaScript
// Blazor bridge for Monaco editor.
|
|
// Exposes window.MonacoBlazor with createEditor / setValue / getValue / dispose / setMarkers.
|
|
// Lazy-loads Monaco's AMD bundle the first time createEditor is called.
|
|
|
|
(function () {
|
|
const VS_BASE = "/_content/ScadaLink.CentralUI/lib/monaco/vs";
|
|
|
|
const editors = {};
|
|
let readyPromise = null;
|
|
|
|
function ensureLoaded() {
|
|
if (readyPromise) return readyPromise;
|
|
readyPromise = new Promise(function (resolve, reject) {
|
|
const loader = document.createElement("script");
|
|
loader.src = VS_BASE + "/loader.js";
|
|
loader.onload = function () {
|
|
// eslint-disable-next-line no-undef
|
|
require.config({ paths: { vs: VS_BASE } });
|
|
// eslint-disable-next-line no-undef
|
|
require(["vs/editor/editor.main"], function () {
|
|
registerCSharpProviders();
|
|
resolve();
|
|
});
|
|
};
|
|
loader.onerror = function (e) { reject(e); };
|
|
document.head.appendChild(loader);
|
|
});
|
|
return readyPromise;
|
|
}
|
|
|
|
// ---- Roslyn-backed C# language providers --------------------------------
|
|
|
|
const KIND_MAP = {
|
|
Method: 0, Field: 4, Property: 9, Event: 10, Class: 6,
|
|
Module: 8, Variable: 4, Text: 18
|
|
};
|
|
|
|
function registerCSharpProviders() {
|
|
// Completion: triggered on ".", "(", "\"" and on demand (Ctrl-Space).
|
|
monaco.languages.registerCompletionItemProvider("csharp", {
|
|
triggerCharacters: [".", "(", "\""],
|
|
provideCompletionItems: async function (model, position) {
|
|
try {
|
|
const resp = await fetch("/api/script-analysis/completions", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
codeText: model.getValue(),
|
|
line: position.lineNumber,
|
|
column: position.column
|
|
})
|
|
});
|
|
if (!resp.ok) return { suggestions: [] };
|
|
const data = await resp.json();
|
|
const word = model.getWordUntilPosition(position);
|
|
const range = {
|
|
startLineNumber: position.lineNumber,
|
|
endLineNumber: position.lineNumber,
|
|
startColumn: word.startColumn,
|
|
endColumn: word.endColumn
|
|
};
|
|
return {
|
|
suggestions: (data.items || []).map(function (it) {
|
|
return {
|
|
label: it.label,
|
|
insertText: it.insertText,
|
|
detail: it.detail,
|
|
kind: KIND_MAP[it.kind] != null ? KIND_MAP[it.kind] : 18,
|
|
range: range
|
|
};
|
|
})
|
|
};
|
|
} catch (e) {
|
|
return { suggestions: [] };
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async function fetchDiagnostics(code) {
|
|
try {
|
|
const resp = await fetch("/api/script-analysis/diagnostics", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ code: code })
|
|
});
|
|
if (!resp.ok) return [];
|
|
const data = await resp.json();
|
|
return data.markers || [];
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function createEditor(id, host, options, dotNetRef) {
|
|
await ensureLoaded();
|
|
if (!host) return;
|
|
const editor = monaco.editor.create(host, {
|
|
value: options.value || "",
|
|
language: options.language || "csharp",
|
|
theme: "vs",
|
|
minimap: { enabled: false },
|
|
scrollBeyondLastLine: false,
|
|
automaticLayout: true,
|
|
fontSize: 13,
|
|
lineNumbers: "on",
|
|
renderLineHighlight: "line",
|
|
readOnly: !!options.readOnly,
|
|
tabSize: 4,
|
|
insertSpaces: true,
|
|
wordWrap: "off",
|
|
fixedOverflowWidgets: true
|
|
});
|
|
let diagTimer = null;
|
|
const scheduleDiagnostics = function () {
|
|
if (diagTimer) clearTimeout(diagTimer);
|
|
diagTimer = setTimeout(async function () {
|
|
const markers = await fetchDiagnostics(editor.getValue());
|
|
const model = editor.getModel();
|
|
if (model) monaco.editor.setModelMarkers(model, "scadalink", markers);
|
|
}, 500);
|
|
};
|
|
|
|
editor.onDidChangeModelContent(function () {
|
|
const value = editor.getValue();
|
|
dotNetRef.invokeMethodAsync("OnValueChanged", value).catch(function () {});
|
|
if (options.language === "csharp") scheduleDiagnostics();
|
|
});
|
|
editors[id] = { editor: editor, dotNetRef: dotNetRef };
|
|
|
|
// Run an initial diagnostic pass so existing scripts show their markers.
|
|
if (options.language === "csharp") scheduleDiagnostics();
|
|
}
|
|
|
|
function setValue(id, value) {
|
|
const entry = editors[id];
|
|
if (!entry) return;
|
|
if (entry.editor.getValue() !== value) {
|
|
entry.editor.setValue(value || "");
|
|
}
|
|
}
|
|
|
|
function getValue(id) {
|
|
const entry = editors[id];
|
|
return entry ? entry.editor.getValue() : null;
|
|
}
|
|
|
|
function setMarkers(id, markers) {
|
|
const entry = editors[id];
|
|
if (!entry) return;
|
|
const model = entry.editor.getModel();
|
|
if (!model) return;
|
|
monaco.editor.setModelMarkers(model, "scadalink", markers || []);
|
|
}
|
|
|
|
function dispose(id) {
|
|
const entry = editors[id];
|
|
if (!entry) return;
|
|
try { entry.editor.dispose(); } catch (e) {}
|
|
delete editors[id];
|
|
}
|
|
|
|
window.MonacoBlazor = {
|
|
createEditor: createEditor,
|
|
setValue: setValue,
|
|
getValue: getValue,
|
|
setMarkers: setMarkers,
|
|
dispose: dispose
|
|
};
|
|
})();
|