如何使CompletionService了解项目中的其他文档?

发布时间:2022-08-19 / 作者:清心寡欲
本文介绍了如何使CompletionService了解项目中的其他文档?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在构建一个允许用户定义、编辑和执行C#脚本的应用程序。

定义由方法名、参数名数组和方法的内部代码组成,例如:

  • 名称:脚本1
  • 参数名称:arg1、arg2
  • 代码:返回$";arg1:{arg1},arg2:{arg2}";;

根据此定义,可以生成以下代码:

public static object Script1(object arg1, object arg2)
{
return $"Arg1: {arg1}, Arg2: {arg2}";
}

我已经成功地设置了AdhocWorkspaceProject,如下所示:

private readonly CSharpCompilationOptions _options = new CSharpCompilationOptions(OutputKind.ConsoleApplication,
        moduleName: "MyModule",
        mainTypeName: "MyMainType",
        scriptClassName: "MyScriptClass"
    )
    .WithUsings("System");

private readonly MetadataReference[] _references = {
    MetadataReference.CreateFromFile(typeof(object).assembly.Location)
};

private void InitializeWorkspaceAndProject(out AdhocWorkspace ws, out ProjectId projectId)
{
    var assemblies = new[]
    {
        Assembly.Load("Microsoft.CodeAnalysis"),
        Assembly.Load("Microsoft.CodeAnalysis.CSharp"),
        Assembly.Load("Microsoft.CodeAnalysis.Features"),
        Assembly.Load("Microsoft.CodeAnalysis.CSharp.Features")
    };

    var partTypes = MefHostServices.DefaultAssemblies.Concat(assemblies)
        .Distinct()
        .SelectMany(x => x.GetTypes())
        .ToArray();

    var compositionContext = new ContainerConfiguration()
        .WithParts(partTypes)
        .CreateContainer();

    var host = MefHostServices.Create(compositionContext);

    ws = new AdhocWorkspace(host);

    var projectInfo = ProjectInfo.Create(
            ProjectId.CreateNewId(),
            VersionStamp.Create(),
            "MyProject",
            "MyProject",
            LanguageNames.CSharp,
            compilationOptions: _options, parseOptions: new CSharpParseOptions(LanguageVersion.CSharp7_3, DocumentationMode.None, SourceCodeKind.Script)).
        WithMetadataReferences(_references);
    
    projectId = ws.AddProject(projectInfo).Id;
}

我可以创建这样的文档:

var document = _workspace.AddDocument(_projectId, "MyFile.cs", SourceText.From(code)).WithSourceCodeKind(SourceCodeKind.Script);

对于用户定义的每个脚本,我当前正在创建一个单独的Document

执行代码也可以,使用以下方法:

首先,汇编所有文档:

public async Task GetCompilations(params Document[] documents)
{
    var treeTasks = documents.Select(async (d) => await d.GetSyntaxTreeAsync());

    var trees = await Task.WhenAll(treeTasks);

    return CSharpCompilation.Create("MyAssembly", trees, _references, _options);
}

然后,要在编译外创建程序集:

public Assembly GetAssembly(Compilation compilation)
    {
        try
        {
            using (MemoryStream ms = new MemoryStream())
            {
                var emitResult = compilation.Emit(ms);

                if (!emitResult.Success)
                {
                    foreach (Diagnostic diagnostic in emitResult.Diagnostics)
                    {
                        Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
                    }
                }
                else
                {
                    ms.Seek(0, SeekOrigin.Begin);
                    var buffer = ms.GetBuffer();
                    var assembly = Assembly.Load(buffer);

                    return assembly;
                }

                return null;
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }

    }
    

最后,执行脚本:

    public async Task Execute(string method, object[] params)
    {
        var compilation = await GetCompilations(_documents);

        var a = GetAssembly(compilation);

        try
        {
            Type t = a.GetTypes().First();
            var res = t.GetMethod(method)?.Invoke(null, params);

            return res;
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }
    }
    

到目前为止,一切顺利。这允许用户定义可以彼此共享的脚本

对于编辑,我想提供代码完成功能,目前正在这样做:

public async Task GetCompletionList(Document doc, string code, int offset)
    {
        var newDoc = doc.WithText(SourceText.From(code));
        _workspace.TryApplyChanges(newDoc.Project.Solution);
        
        var completionService = CompletionService.GetService(newDoc);
                    
        return await completionService.GetCompletionsAsync(newDoc, offset);
    }

注意:上面的代码片段已更新,修复了Jason在回答中提到的有关使用docdocument的错误。事实上,这是因为这里显示的代码是从我的实际应用程序代码中提取(并因此修改)的。您可以在他的回答中找到我发布的原始错误代码片段,也可以在更下面的新版本中找到导致我的问题的实际问题。

现在的问题是GetCompletionsAsync只知道相同的Document中的定义和创建工作区和项目时使用的引用,但它显然没有对同一项目中的其他文档的任何引用。因此CompletionList不包含其他用户脚本的符号。

这似乎很奇怪,因为在&q;Live&q;Visual Studio项目中,项目中的所有文件都相互识别。

我错过了什么?项目和/或工作区设置是否不正确?有没有其他调用CompletionService的方法?生成的文档代码是否缺少某些内容,如通用命名空间?

我最后的办法是将从用户脚本定义生成的所有方法合并到一个文件中--还有其他方法吗?

仅供参考,这里有几个有用的链接帮助我走到了这一步:

https://www.strathweb.com/2018/12/using-roslyn-c-completion-service-programmatically/

Roslyn throws The language 'C#' is not supported

Roslyn service is null

Updating AdHocWorkspace is slow

Roslyn: is it possible to pass variables to documents (with SourceCodeKind.Script)

更新1: 由于Jason的回答,我已经更新了GetCompletionList方法如下:

public async Task GetCompletionList(Document doc, string code, int offset)
{
    var docId = doc.Id;
    var newDoc = doc.WithText(SourceText.From(code));
    _workspace.TryApplyChanges(newDoc.Project.Solution);
    
    var currentDoc = _workspace.CurrentSolution.GetDocument(docId);
    
    var completionService = CompletionService.GetService(currentDoc);
                
    return await completionService.GetCompletionsAsync(currentDoc, offset);
}

正如Jason指出的,主要的错误是没有充分考虑项目及其文档的不变性。我调用CompletionService.GetService(doc)所需的Document实例必须是当前解决方案中包含的实际实例,而不是doc.WithText(...)创建的实例,因为该实例不知道任何

通过存储原始实例的DocumentId并使用它在解决方案,currentDoc中检索更新后的实例,在应用更改后,完成服务可以(与";live";解决方案一样)引用其他文档。

更新2:在我最初的问题中,代码片段使用了SourceCodeKind.Regular,但-至少在本例中-它必须是SourceCodeKind.Script,否则编译器将报告不允许顶级静态方法(当使用C#7.3时)。我现在已经更新了帖子。

推荐答案

所以这里有一件事看起来有点可疑:

public async Task GetCompletionList(Document doc, string code, int offset)
{
    var newDoc = document.WithText(SourceText.From(code));
    _workspace.TryApplyChanges(newDoc.Project.Solution);
    
    var completionService = CompletionService.GetService(newDoc);
                
    return await completionService.GetCompletionsAsync(document, offset);
}

(注意:您的参数名称是";doc";,但您使用的是";Document";,所以我猜这段代码是您从完整示例中删减的。但我只是想指出这一点,因为您可能在这样做时引入了错误。)

所以主要的疑点是:Roslyn文档是快照;文档是整个解决方案的整个快照中的指针。您的&newDoc";是一个新文档,其中包含您替换的文本,并且您正在更新工作区以包含该文本。然而,您仍然将原始文档提交给GetCompletionsAsync,这意味着在这种情况下您仍然需要旧文档,这可能具有陈旧的代码。此外,因为它都是快照,所以通过调用TryApplyChanges对主工作区所做的更改不会以任何方式反映在新的Document对象中。因此,我猜测这里可能发生的情况是,您传入的Document对象实际上并没有同时更新所有文本文档,但其中大多数文档仍然是空的或类似的内容。

这篇关于如何使CompletionService了解项目中的其他文档?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持吉威生活!



[英文标题]How can I make the CompletionService aware of other documents in the project?


声明:本媒体部分图片、文章来源于网络,版权归原作者所有,如有侵权,请联系QQ:330946442删除。