1- // PlaygroundLanguageServices — long-lived AdhocWorkspace that backs Monaco's
2- // completion + hover providers. Separate from SnippetCompiler (which builds
3- // fresh CSharpCompilations per keystroke for the run path); the workspace's
4- // incremental semantic model gives sub-ms per-keystroke completion.
5- //
6- // Architecture inspired by DotNetLab/src/Compiler/LanguageServices.cs (MIT):
7- // one AdhocWorkspace, one Project, one Document per <expressive-playground>
8- // instance routed by Monaco model URI. The DefaultPersistentStorageConfiguration
9- // cctor PNSE is bypassed by the WorkspaceShim project — see its header.
1+ // Long-lived AdhocWorkspace backing Monaco's completion + hover providers; the
2+ // incremental semantic model gives sub-ms per-keystroke completion. Separate
3+ // from SnippetCompiler, which builds fresh CSharpCompilations per run.
4+ // One Document per <expressive-playground> instance, routed by Monaco model
5+ // URI. The DefaultPersistentStorageConfiguration cctor PNSE on WASM is bypassed
6+ // by the WorkspaceShim project — see its header.
7+ // Architecture inspired by DotNetLab/src/Compiler/LanguageServices.cs (MIT).
108
119using ExpressiveSharp . Docs . Playground . Core . Services ;
1210using ExpressiveSharp . Docs . Playground . Core . Services . Scenarios ;
@@ -38,11 +36,10 @@ public PlaygroundLanguageServices(PlaygroundReferences references)
3836 throw new InvalidOperationException (
3937 "PlaygroundReferences must be loaded before PlaygroundLanguageServices is constructed." ) ;
4038
41- // Append the WorkspaceShim assembly AFTER MefHostServices.DefaultAssemblies.
42- // The shim's NoOpPersistentStorageConfiguration is exported with
43- // ServiceLayer.Test which gives it MEF priority over Roslyn's broken
44- // DefaultPersistentStorageConfiguration, so the latter's cctor never
45- // runs and Process.GetCurrentProcess() (PNSE on WASM) is never called.
39+ // Append WorkspaceShim AFTER DefaultAssemblies. Its NoOpPersistentStorageConfiguration
40+ // exports with ServiceLayer.Test, gaining MEF priority over Roslyn's
41+ // DefaultPersistentStorageConfiguration so the latter's cctor never runs
42+ // (it calls Process.GetCurrentProcess() — PNSE on WASM).
4643 var hostServices = MefHostServices . Create (
4744 MefHostServices . DefaultAssemblies
4845 . Append ( typeof ( NoOpPersistentStorageConfiguration ) . Assembly ) ) ;
@@ -56,8 +53,7 @@ public PlaygroundLanguageServices(PlaygroundReferences references)
5653 assemblyName : "PlaygroundProject" ,
5754 language : LanguageNames . CSharp ,
5855 metadataReferences : references . References ,
59- // WithConcurrentBuild(false): WASM is single-threaded, parallel
60- // Roslyn compile threads deadlock or throw.
56+ // WASM is single-threaded; parallel Roslyn compile threads deadlock or throw.
6157 compilationOptions : new CSharpCompilationOptions ( OutputKind . DynamicallyLinkedLibrary )
6258 . WithNullableContextOptions ( NullableContextOptions . Enable )
6359 . WithConcurrentBuild ( false ) ,
@@ -67,12 +63,9 @@ public PlaygroundLanguageServices(PlaygroundReferences references)
6763 _projectId = projectInfo . Id ;
6864 }
6965
70- /// <summary>
71- /// Forces MEF composition during page load instead of lazily on the first
72- /// keystroke. Adds a throwaway document, asks for its CompletionService
73- /// (the act of resolving it triggers MEF + the cctor moment WorkspaceShim
74- /// guards against), discards. Subsequent real completions hit warm caches.
75- /// </summary>
66+ // Forces MEF composition during page load instead of lazily on the first
67+ // keystroke. Resolving CompletionService triggers MEF + the cctor moment
68+ // WorkspaceShim guards against; subsequent real completions hit warm caches.
7669 public async Task PrewarmAsync ( )
7770 {
7871 await _lock . WaitAsync ( ) ;
@@ -117,8 +110,7 @@ public async Task RegisterEditorAsync(string modelUri, string snippetText, strin
117110 await _lock . WaitAsync ( ) ;
118111 try
119112 {
120- // Drop any prior document under the same URI (rare — happens if
121- // an instance disposes and remounts) to avoid duplicate routing.
113+ // Drop any prior document under the same URI (instance dispose + remount).
122114 if ( _modelToDocument . TryGetValue ( modelUri , out var existingId ) )
123115 {
124116 _workspace . TryApplyChanges ( _workspace . CurrentSolution . RemoveDocument ( existingId ) ) ;
@@ -137,7 +129,7 @@ public async Task RegisterEditorAsync(string modelUri, string snippetText, strin
137129 }
138130 }
139131
140- // Per -keystroke ( no debounce — Roslyn diffs the syntax tree, sub-ms).
132+ // Called per -keystroke; no debounce — Roslyn diffs the syntax tree ( sub-ms).
141133 public async Task UpdateEditorAsync ( string modelUri , string snippetText , string ? setupText , IPlaygroundScenario scenario )
142134 {
143135 var wrap = SnippetWrap . Build ( scenario . WrapperTemplate , snippetText , setupText ) ;
@@ -158,7 +150,6 @@ public async Task UpdateEditorAsync(string modelUri, string snippetText, string?
158150 }
159151 }
160152
161- // Best-effort: silently no-ops if the model URI is unknown.
162153 public async Task UnregisterEditorAsync ( string modelUri )
163154 {
164155 await _lock . WaitAsync ( ) ;
@@ -236,9 +227,9 @@ public async Task UnregisterEditorAsync(string modelUri)
236227 }
237228
238229 // Returns -1 if the position falls outside the snippet region.
230+ // Monaco is 1-based, LinePosition is 0-based.
239231 private static int MonacoPositionToCaretOffset ( MonacoPosition position , SourceText text , SnippetWrap wrap )
240232 {
241- // Monaco is 1-based, LinePosition is 0-based.
242233 var snippetRelative = new LinePosition (
243234 line : Math . Max ( 0 , position . LineNumber - 1 ) ,
244235 character : Math . Max ( 0 , position . Column - 1 ) ) ;
0 commit comments