首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >如何使CompletionService知道项目中的其他文档?

如何使CompletionService知道项目中的其他文档?
EN

Stack Overflow用户
提问于 2022-02-15 13:53:04
回答 1查看 196关注 0票数 1

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

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

Script1

  • Parameter名称: arg1,arg2

  • 代码:返回$"Arg1:{arg1},Arg2:{arg2}";

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

代码语言:javascript
运行
复制
public static object Script1(object arg1, object arg2)
{
return $"Arg1: {arg1}, Arg2: {arg2}";
}

我成功地设置了一个AdhocWorkspace和一个像这样的Project

代码语言:javascript
运行
复制
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;
}

我可以创建这样的文件:

代码语言:javascript
运行
复制
var document = _workspace.AddDocument(_projectId, "MyFile.cs", SourceText.From(code)).WithSourceCodeKind(SourceCodeKind.Script);

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

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

首先,汇编所有文件:

代码语言:javascript
运行
复制
public async Task<Compilation> 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);
}

然后,从编译中创建程序集:

代码语言:javascript
运行
复制
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;
        }

    }

最后,执行脚本:

代码语言:javascript
运行
复制
    public async Task<object> 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;
        }
    }

到现在为止还好。这使得用户可以定义可以彼此都可以使用的脚本。

为了编辑,我想提供代码完成,目前正在这样做:

代码语言:javascript
运行
复制
public async Task<CompletionList> 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不包含其他用户脚本的符号。

这似乎很奇怪,因为在“活动”Visual项目中,当然,项目中的所有文件都相互了解。

我遗漏了什么?项目和/或工作区设置是否不正确?还有其他方法叫CompletionService吗?生成的文档代码是否遗漏了一些东西,比如公共名称空间?

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

FYI,这里有几个有用的链接帮助我走到这一步:

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:感谢杰森的回答,我更新了GetCompletionList方法如下:

代码语言:javascript
运行
复制
public async Task<CompletionList> 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)所需的CompletionService.GetService(doc)实例必须是当前解决方案中包含的实际实例,而不是由doc.WithText(...)创建的实例,因为该实例不知道任何事情。

通过存储原始实例的DocumentId并使用它在解决方案currentDoc中检索更新的实例,在应用更改之后,完成服务可以(如“活动”解决方案)引用其他文档。

更新2:在我最初的问题中,代码片段使用了SourceCodeKind.Regular,但是--至少在本例中--它必须是SourceCodeKind.Script,因为否则编译器会抱怨不允许顶级静态方法(当使用C# 7.3时)。我现在更新了这篇文章。

EN

Stack Overflow用户

回答已采纳

发布于 2022-02-15 19:52:02

有一件事看上去有点可疑:

代码语言:javascript
运行
复制
public async Task<CompletionList> 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“,但是您使用的是”文档“,所以我猜这段代码是从完整示例中删减的。但我只是想把它叫停,因为你可能在做这件事时引入了错误。)

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

票数 1
EN
查看全部 1 条回答
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/71127654

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档