Skip to content

Commit

Permalink
⚡️ C# script editor intellisense #92
Browse files Browse the repository at this point in the history
  • Loading branch information
jxnkwlp committed Feb 2, 2024
1 parent c7a4600 commit c599727
Show file tree
Hide file tree
Showing 16 changed files with 606 additions and 677 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ dotnet_diagnostic.CA1848.severity = silent
dotnet_diagnostic.CA1305.severity = silent
dotnet_diagnostic.CA2016.severity = warning
dotnet_diagnostic.ide0011.severity = warning
dotnet_diagnostic.RCS1251.severity = silent

[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}]
indent_style = space
Expand Down
Original file line number Diff line number Diff line change
@@ -1,81 +1,91 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using Passingwind.Abp.ElsaModule.CSharp;
using Passingwind.Abp.ElsaModule.Scripting.CSharp;
using Passingwind.CSharpScriptEngine.Roslyn;
using Passingwind.CSharpScriptEngine;

namespace Passingwind.Abp.ElsaModule.Services;

public class WorkflowCSharpEditorService : IWorkflowCSharpEditorService
{
private const string _generatedTypeClassName = "GeneratedTypes";
private const string GeneratedTypeClassName = "GeneratedTypes";

private readonly ILogger<WorkflowCSharpEditorService> _logger;
private readonly IRoslynHost _roslynHost;
private readonly ICSharpTypeDefinitionService _cSharpTypeDefinitionService;
private readonly ICSharpScriptWorkspace _cSharpScriptWorkspace;

public WorkflowCSharpEditorService(ILogger<WorkflowCSharpEditorService> logger, IRoslynHost roslynHost, ICSharpTypeDefinitionService cSharpTypeDefinitionService)
public WorkflowCSharpEditorService(ILogger<WorkflowCSharpEditorService> logger, ICSharpTypeDefinitionService cSharpTypeDefinitionService, ICSharpScriptWorkspace cSharpScriptWorkspace)
{
_logger = logger;
_roslynHost = roslynHost;
_cSharpTypeDefinitionService = cSharpTypeDefinitionService;
_cSharpScriptWorkspace = cSharpScriptWorkspace;
}

public async Task<WorkflowCSharpEditorCodeAnalysisResult> GetCodeAnalysisAsync(WorkflowDefinition workflowDefinition, string textId, string text, CancellationToken cancellationToken = default)
{
var generated = await _cSharpTypeDefinitionService.GenerateAsync(workflowDefinition, cancellationToken);

// one workflow to on adhoc project
var project = _roslynHost.GetOrCreateProject(workflowDefinition.DefinitionId.Replace("-", null), generated.Assemblies, generated.Imports);
var projectName = workflowDefinition.DefinitionId.Replace("-", null);

_roslynHost.CreateOrUpdateDocument(project.Name, _generatedTypeClassName, generated.Text);
_roslynHost.CreateOrUpdateDocument(project.Name, textId, text, true);
var project = _cSharpScriptWorkspace.GetOrCreateProject(projectName);

var diagnostics = await _roslynHost.GetDocumentDiagnosticsAsync(project.Name, textId, cancellationToken);
project.CreateOrUpdateDocument(GeneratedTypeClassName, generated.Text);

project.CreateOrUpdateDocument(textId, text);

var tmpFile = Path.GetTempFileName();

var compilation = await _cSharpScriptWorkspace.CreateCompilationAsync(project, cancellationToken);

var emitResult = compilation.Emit(tmpFile, cancellationToken: cancellationToken);

var result = new List<WorkflowCSharpEditorCodeAnalysis>();

foreach (var diagnostic in diagnostics)
if (emitResult?.Success == false)
{
var severity = MapDiagnosticSeverity(diagnostic);

var msg = new WorkflowCSharpEditorCodeAnalysis()
foreach (var diagnostic in emitResult.Diagnostics)
{
Id = diagnostic.Id,
Message = diagnostic.GetMessage(),
OffsetFrom = diagnostic.Location.SourceSpan.Start,
OffsetTo = diagnostic.Location.SourceSpan.End,
Severity = severity,
SeverityNumeric = (int)severity,
};
result.Add(msg);
var severity = MapDiagnosticSeverity(diagnostic);

var msg = new WorkflowCSharpEditorCodeAnalysis()
{
Id = diagnostic.Id,
Message = diagnostic.GetMessage(),
OffsetFrom = diagnostic.Location.SourceSpan.Start,
OffsetTo = diagnostic.Location.SourceSpan.End,
Severity = severity,
SeverityNumeric = (int)severity,
};
result.Add(msg);
}
}

return new WorkflowCSharpEditorCodeAnalysisResult() { Items = result };
}

public async Task<WorkflowCSharpEditorFormatterResult> CodeFormatterAsync(string textId, string text, CancellationToken cancellationToken = default)
{
var project = _roslynHost.GetOrCreateProject(Guid.NewGuid().ToString());
var documentId = _roslynHost.CreateOrUpdateDocument(project.Name, textId, text, true);
var document = _roslynHost.GetDocument(project.Name, documentId);
var project = ProjectInfo.Create(ProjectId.CreateNewId(), VersionStamp.Create(), "tmp", "tmp", LanguageNames.CSharp);
var documentInfo = DocumentInfo.Create(DocumentId.CreateNewId(project.Id), "tmp");

using AdhocWorkspace workspce = new AdhocWorkspace();
var document = workspce.AddDocument(documentInfo);

var formattedDocument = await Formatter.FormatAsync(document, cancellationToken: cancellationToken);
var sourceText = await formattedDocument.GetTextAsync(cancellationToken);

_roslynHost.DeleteProject(project.Name);

return new WorkflowCSharpEditorFormatterResult
{
Text = sourceText?.ToString()
Expand All @@ -86,72 +96,38 @@ public async Task<WorkflowCSharpEditorCompletionResult> GetCompletionAsync(Workf
{
var generated = await _cSharpTypeDefinitionService.GenerateAsync(workflowDefinition, cancellationToken);

var project = _roslynHost.GetOrCreateProject(workflowDefinition.DefinitionId.Replace("-", null), generated.Assemblies, generated.Imports);

_ = _roslynHost.CreateOrUpdateDocument(project.Name, _generatedTypeClassName, generated.Text);
var documentId = _roslynHost.CreateOrUpdateDocument(project.Name, textId, text, true);
var projectName = workflowDefinition.DefinitionId.Replace("-", null);

await _roslynHost.AnalysisProjectAsync(project.Name, cancellationToken);
var project = _cSharpScriptWorkspace.GetOrCreateProject(projectName);

var document = _roslynHost.GetDocument(project.Name, documentId);
project.CreateOrUpdateDocument(GeneratedTypeClassName, generated.Text);

var completionService = CompletionService.GetService(document);
//var helper = Microsoft.CodeAnalysis.Completion.CompletionHelper.GetHelper(document);
var document = project.CreateOrUpdateDocument(textId, text);

if (completionService == null)
{
return new WorkflowCSharpEditorCompletionResult();
}
var completionItems = await project.GetCompletionsAsync(document, position);

var sourceText = await document.GetTextAsync(cancellationToken);
var result = new List<WorkflowCSharpEditorCompletionItem>();

//CompletionTrigger completionTrigger = CompletionTrigger.Invoke;
//var triggerText = sourceText.GetSubText(position <= 0 ? 0 : position - 1)?.ToString();
//if (triggerText.Length > 0)
//{
// if (triggerText[0] != '.')
// completionTrigger = CompletionTrigger.CreateInsertionTrigger(triggerText[0]);
//}

var completionResult = await completionService.GetCompletionsAsync(document, position, cancellationToken: cancellationToken);

var results = new List<WorkflowCSharpEditorCompletionItem>();

if (completionResult.ItemsList.Count > 0)
foreach (var item in completionItems)
{
var textSpanToTextCache = new Dictionary<TextSpan, string>();

//.Where(x =>
//{
// if (!textSpanToTextCache.TryGetValue(x.Span, out var spanTxt))
// {
// spanTxt = textSpanToTextCache[x.Span] = sourceText.GetSubText(x.Span).ToString();
// }

// return helper.MatchesPattern(x, spanTxt, CultureInfo.InvariantCulture);
//});

foreach (var item in (IReadOnlyList<CompletionItem>)completionResult.ItemsList)
SymbolKind symbolKind = SymbolKind.Local;
if (item.Properties.TryGetValue(nameof(SymbolKind), out var kindValue))
{
SymbolKind symbolKind = SymbolKind.Local;
if (item.Properties.TryGetValue(nameof(SymbolKind), out var kindValue))
{
symbolKind = Enum.Parse<SymbolKind>(kindValue);
}
symbolKind = Enum.Parse<SymbolKind>(kindValue);
}

var completionDescription = await completionService.GetDescriptionAsync(document, item, cancellationToken);
// var completionDescription = await CompletionService.GetDescriptionAsync(document, item, cancellationToken);

results.Add(new WorkflowCSharpEditorCompletionItem
{
Description = completionDescription.Text,
Suggestion = item.DisplayText,
SymbolKind = symbolKind.ToString(),
ItemKind = MapKind(symbolKind),
});
}
result.Add(new WorkflowCSharpEditorCompletionItem
{
// Description = completionDescription.Text,
Suggestion = item.DisplayText,
SymbolKind = symbolKind.ToString(),
ItemKind = MapKind(symbolKind),
});
}

return new WorkflowCSharpEditorCompletionResult(results);
return new WorkflowCSharpEditorCompletionResult(result);

WorkflowCSharpEditorCompletionItemKind MapKind(SymbolKind symbolKind)
{
Expand Down Expand Up @@ -182,16 +158,18 @@ public async Task<WorkflowCSharpEditorHoverInfoResult> GetHoverInfoAsync(Workflo
{
var generated = await _cSharpTypeDefinitionService.GenerateAsync(workflowDefinition, cancellationToken);

var project = _roslynHost.GetOrCreateProject(workflowDefinition.DefinitionId.Replace("-", null), generated.Assemblies, generated.Imports);
var projectName = workflowDefinition.DefinitionId.Replace("-", null);

_ = _roslynHost.CreateOrUpdateDocument(project.Name, _generatedTypeClassName, generated.Text);
var documentId = _roslynHost.CreateOrUpdateDocument(project.Name, textId, text, true);
var project = _cSharpScriptWorkspace.GetOrCreateProject(projectName);

var document = _roslynHost.GetDocument(project.Name, documentId);
project.CreateOrUpdateDocument(GeneratedTypeClassName, generated.Text);

var compilation = await _roslynHost.GetCompilationAsync(project.Name, cancellationToken);
var document = project.CreateOrUpdateDocument(textId, text);

var compilation = await _cSharpScriptWorkspace.CreateCompilationAsync(project, cancellationToken);

var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken);

var semanticModel = compilation.GetSemanticModel(syntaxTree, true);

var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken);
Expand Down Expand Up @@ -273,14 +251,15 @@ public async Task<WorkflowCSharpEditorSignatureResult> GetSignaturesAsync(Workfl
{
var generated = await _cSharpTypeDefinitionService.GenerateAsync(workflowDefinition, cancellationToken);

var project = _roslynHost.GetOrCreateProject(workflowDefinition.DefinitionId.Replace("-", null), generated.Assemblies, generated.Imports);
var projectName = workflowDefinition.DefinitionId.Replace("-", null);

var project = _cSharpScriptWorkspace.GetOrCreateProject(projectName);

_ = _roslynHost.CreateOrUpdateDocument(project.Name, _generatedTypeClassName, generated.Text);
var documentId = _roslynHost.CreateOrUpdateDocument(project.Name, textId, text, true);
project.CreateOrUpdateDocument(GeneratedTypeClassName, generated.Text);

var document = _roslynHost.GetDocument(project.Name, documentId);
var document = project.CreateOrUpdateDocument(textId, text);

var compilation = await _roslynHost.GetCompilationAsync(project.Name, cancellationToken);
var compilation = await _cSharpScriptWorkspace.CreateCompilationAsync(project, cancellationToken);

var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken);
var semanticModel = compilation.GetSemanticModel(syntaxTree, true);
Expand Down
34 changes: 12 additions & 22 deletions src/Passingwind.CSharpScript/CSharpScriptHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public class CSharpScriptHost : ICSharpScriptHost, IDisposable
"System.ValueTuple",
}.ToImmutableArray();

private static readonly ConcurrentDictionary<string, CSharpScriptContextState> _scriptStateCache = new ConcurrentDictionary<string, CSharpScriptContextState>();
private static readonly ConcurrentDictionary<string, CSharpScriptContextState> ScriptStateCache = new ConcurrentDictionary<string, CSharpScriptContextState>();

private readonly IScriptReferenceResolver _scriptReferenceResolver;

Expand All @@ -109,15 +109,15 @@ public CSharpScriptHost(IScriptReferenceResolver scriptReferenceResolver)
CSharpScriptContextState? currentState = null;

bool changed = false;
if (_scriptStateCache.TryGetValue(scriptId, out var previousContext))
if (ScriptStateCache.TryGetValue(scriptId, out var previousContext))
{
changed = context.IsChanged(previousContext.Context);
currentState = previousContext;
}

if (changed || currentState == null)
{
var options = CreateOptions(context.Imports, context.Assemblies);
var options = CreateScriptOptions(context.Imports, context.Assemblies);
scriptOptions?.Invoke(options);

options = options.AddReferences(StandardMetadataReference);
Expand All @@ -127,14 +127,14 @@ public CSharpScriptHost(IScriptReferenceResolver scriptReferenceResolver)
Script = await CreateScriptAsync(context.SourceText, options, context.EvaluationGlobal.GetType(), cancellationToken)
};

_scriptStateCache[scriptId] = currentState;
ScriptStateCache[scriptId] = currentState;
}

script = currentState.Script;
}
else
{
var options = CreateOptions(context.Imports, context.Assemblies);
var options = CreateScriptOptions(context.Imports, context.Assemblies);
scriptOptions?.Invoke(options);

options = options.AddReferences(StandardMetadataReference);
Expand All @@ -160,18 +160,15 @@ public CSharpScriptHost(IScriptReferenceResolver scriptReferenceResolver)

public async Task<EmitResult?> CompileAsync(CSharpScriptCompileContext context, CancellationToken cancellationToken = default)
{
var scriptOptions = CreateOptions(context.Imports, context.Assemblies);
var scriptOptions = CreateScriptOptions(context.Imports, context.Assemblies);
var parseOptions = CreateParseOptions();

scriptOptions = scriptOptions.AddReferences(StandardPortableReference);

var syntaxTree = SyntaxFactory.ParseSyntaxTree(context.SourceText, options: parseOptions, encoding: Encoding.UTF8, cancellationToken: cancellationToken);

// remove reference directive
var syntaxRoot = syntaxTree.GetRoot(cancellationToken);
var nodeRemove = syntaxTree.GetCompilationUnitRoot(cancellationToken: cancellationToken).GetReferenceDirectives();
syntaxRoot = syntaxRoot.RemoveNodes(nodeRemove, SyntaxRemoveOptions.KeepExteriorTrivia);
syntaxTree = syntaxTree.WithRootAndOptions(syntaxRoot!, parseOptions);
syntaxTree = syntaxTree.RemoveReferenceDirectives(parseOptions, cancellationToken: cancellationToken);

var name = Guid.NewGuid().ToString("N");
var rootFolder = Path.Combine(Path.GetTempPath(), "workflow", "csharp", name);
Expand Down Expand Up @@ -201,14 +198,7 @@ public CSharpScriptHost(IScriptReferenceResolver scriptReferenceResolver)
await using var peStream = File.OpenWrite($"{file}.dll");
await using var pdbStream = File.OpenWrite($"{file}.pdb");

var emitResult = compilation.Emit(peStream, pdbStream, options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb), cancellationToken: cancellationToken);

if (emitResult != null && emitResult?.Success != true)
{
throw new AggregateException(emitResult!.Diagnostics.Select(x => new Exception(x.ToString())));
}

return emitResult;
return compilation.Emit(peStream, pdbStream, options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb), cancellationToken: cancellationToken);
}

private async Task<Script<object>> CreateScriptAsync(string code, ScriptOptions options, Type globalType, CancellationToken cancellationToken = default)
Expand All @@ -222,12 +212,12 @@ private async Task<Script<object>> CreateScriptAsync(string code, ScriptOptions
return CSharpScript.Create(code, options: options, globalsType: globalType, assemblyLoader: loader);
}

private static ScriptOptions CreateOptions(IEnumerable<string> imports, IEnumerable<Assembly> assemblies)
public static ScriptOptions CreateScriptOptions(IEnumerable<string>? imports = null, IEnumerable<Assembly>? assemblies = null)
{
var parseOptions = CreateParseOptions();

var options = ScriptOptions.Default
.AddReferences(DefaultRuntimeMetadataReference)
.WithReferences(DefaultRuntimeMetadataReference)
.AddImports(DefaultImports)
.WithMetadataResolver(CSharpScriptMetadataReferenceResolver.Instance)
.WithSourceResolver(SourceFileResolver.Default)
Expand All @@ -254,7 +244,7 @@ private static ScriptOptions CreateOptions(IEnumerable<string> imports, IEnumera
return options;
}

private static ParseOptions CreateParseOptions()
public static ParseOptions CreateParseOptions()
{
return DefaultParseOptions;
}
Expand All @@ -271,6 +261,6 @@ private static IEnumerable<Assembly> GetRuntimeAssembly()

public void Dispose()
{
_scriptStateCache.Clear();
ScriptStateCache.Clear();
}
}
Loading

0 comments on commit c599727

Please sign in to comment.