diff --git a/src/ScadaLink.CentralUI/ScriptAnalysis/ISharedScriptCatalog.cs b/src/ScadaLink.CentralUI/ScriptAnalysis/ISharedScriptCatalog.cs
new file mode 100644
index 0000000..ba19970
--- /dev/null
+++ b/src/ScadaLink.CentralUI/ScriptAnalysis/ISharedScriptCatalog.cs
@@ -0,0 +1,25 @@
+using ScadaLink.TemplateEngine;
+
+namespace ScadaLink.CentralUI.ScriptAnalysis;
+
+///
+/// Indirection so ScriptAnalysisService can be unit-tested without standing
+/// up SharedScriptService and its EF Core repository chain.
+///
+public interface ISharedScriptCatalog
+{
+ Task> GetNamesAsync();
+}
+
+public class SharedScriptCatalog : ISharedScriptCatalog
+{
+ private readonly SharedScriptService _service;
+
+ public SharedScriptCatalog(SharedScriptService service) => _service = service;
+
+ public async Task> GetNamesAsync()
+ {
+ var scripts = await _service.GetAllSharedScriptsAsync();
+ return scripts.Select(s => s.Name).ToList();
+ }
+}
diff --git a/src/ScadaLink.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs b/src/ScadaLink.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs
index 4eaa35a..e62fd75 100644
--- a/src/ScadaLink.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs
+++ b/src/ScadaLink.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs
@@ -1,23 +1,34 @@
+using System.Security.Cryptography;
+using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Scripting;
-using ScadaLink.TemplateEngine;
+using Microsoft.Extensions.Caching.Memory;
namespace ScadaLink.CentralUI.ScriptAnalysis;
///
/// Compiles user scripts as Roslyn C# Scripting fragments against
/// globals and surfaces diagnostics + completions
-/// in the shape Monaco's provider APIs expect. Lightweight — no caching;
-/// each request rebuilds the script. Acceptable for human-paced edits.
+/// in the shape Monaco's provider APIs expect.
+///
+/// Diagnostics are cached by code hash via IMemoryCache — Monaco debounces
+/// keystrokes at 500 ms but a typing-then-pausing flow can still re-issue
+/// requests for the same content (window blur/focus, etc.), so the cache
+/// short-circuits repeats. Completions aren't cached: position + form
+/// context vary too much for the hit rate to be useful.
///
/// Beyond plain C# analysis, layers SCADA-specific extensions:
/// - In-string completion of Parameters["..."] keys (from the request's
-/// DeclaredParameters), CallShared("...") names (from SharedScriptService),
-/// and CallScript("...") names (from the request's SiblingScripts).
-/// - Forbidden-API diagnostic for the documented script trust model.
+/// DeclaredParameters), CallShared("...") names (from
+/// ), and CallScript("...") names
+/// (from the request's SiblingScripts).
+/// - Forbidden-API diagnostic for the documented script trust model,
+/// resolved against the SemanticModel so user identifiers that happen
+/// to share names with forbidden types (e.g. var File = ...)
+/// do not false-positive.
///
public class ScriptAnalysisService
{
@@ -47,20 +58,13 @@ public class ScriptAnalysisService
"System.Threading.Tasks.Sources",
};
- private static readonly HashSet ForbiddenTypeNames = new(StringComparer.Ordinal)
- {
- "File", "Directory", "Path", "StreamReader", "StreamWriter", "FileStream",
- "Process", "ProcessStartInfo",
- "Assembly", "Type", "MethodInfo", "PropertyInfo", "FieldInfo",
- "Socket", "TcpClient", "UdpClient", "TcpListener",
- "Thread", "ThreadPool", "Mutex", "Semaphore",
- };
+ private readonly ISharedScriptCatalog _sharedScripts;
+ private readonly IMemoryCache _cache;
- private readonly SharedScriptService _sharedScripts;
-
- public ScriptAnalysisService(SharedScriptService sharedScripts)
+ public ScriptAnalysisService(ISharedScriptCatalog sharedScripts, IMemoryCache cache)
{
_sharedScripts = sharedScripts;
+ _cache = cache;
}
public DiagnoseResponse Diagnose(DiagnoseRequest request)
@@ -68,6 +72,10 @@ public class ScriptAnalysisService
if (string.IsNullOrEmpty(request.Code))
return new DiagnoseResponse(Array.Empty());
+ var cacheKey = "diag:" + HashCode(request.Code);
+ if (_cache.TryGetValue(cacheKey, out DiagnoseResponse? cached) && cached is not null)
+ return cached;
+
Script