Skip to content

Commit

Permalink
Implement basic LSPGraphProvider and associated models #686
Browse files Browse the repository at this point in the history
This adds an LSPGraphProvider which can create a graph using
LSP-provided information. For this purpose, an LSPHandler has been added
whose primary purpose is to manage the communication between SEE and
language servers. Additionally, LSPServer and LSPLanguage records have
been added using the "enhanced enum" pattern. These contains as static
attributes all language servers and programming languages supported by
SEE, respectively. Currently, just the rust-analyzer and pyright have
been added, of which only the first works to retrieve hierarchic symbols
from software projects.

Also, a TeeStream class has been handled, which is a stream that works
similar to the Unix `tee` command (or, visually, like a T-pipe)—that is,
it redirects anything read and written through it is (in addition to its
primary stream) piped to a secondary stream. The specific reason this
class has been added is so that I can view the input and output of LSP
processes for debugging purposes.
  • Loading branch information
falko17 committed Mar 3, 2024
1 parent d07b0af commit 3bd68f2
Show file tree
Hide file tree
Showing 14 changed files with 611 additions and 2 deletions.
1 change: 1 addition & 0 deletions Assets/SEE/GraphProviders/GraphProviderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal static GraphProvider NewInstance(GraphProviderKind kind)
GraphProviderKind.Pipeline => new PipelineGraphProvider(),
GraphProviderKind.JaCoCo => new JaCoCoGraphProvider(),
GraphProviderKind.MergeDiff => new MergeDiffGraphProvider(),
GraphProviderKind.LSP => new LSPGraphProvider(),
_ => throw new NotImplementedException($"Not implemented for {kind}")
};
}
Expand Down
6 changes: 5 additions & 1 deletion Assets/SEE/GraphProviders/GraphProviderKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public enum GraphProviderKind
/// <summary>
/// For <see cref="MergeDiffGraphProvider"/>.
/// </summary>
MergeDiff
MergeDiff,
/// <summary>
/// For <see cref="LSPGraphProvider"/>.
/// </summary>
LSP
}
}
156 changes: 156 additions & 0 deletions Assets/SEE/GraphProviders/LSPGraphProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Cysharp.Threading.Tasks;
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using SEE.DataModel.DG;
using SEE.Game.City;
using SEE.GO;
using SEE.Tools.LSP;
using SEE.UI.RuntimeConfigMenu;
using SEE.Utils.Config;
using SEE.Utils.Paths;
using Sirenix.OdinInspector;
using UnityEngine;
using Debug = UnityEngine.Debug;

namespace SEE.GraphProviders
{
/// <summary>
/// A graph provider that uses a language server to create a graph.
/// </summary>
public class LSPGraphProvider : GraphProvider
{
/// <summary>
/// The path to the software project for which the graph shall be generated.
/// </summary>
[Tooltip("Path to the project to be analyzed."), RuntimeTab(GraphProviderFoldoutGroup), HideReferenceObjectPicker]
public DirectoryPath ProjectPath = new();

/// <summary>
/// The language server to be used for the analysis.
/// </summary>
[Tooltip("The language server to be used for the analysis."),
RuntimeTab(GraphProviderFoldoutGroup),
HideReferenceObjectPicker, ValueDropdown(nameof(ServerDropdown), ExpandAllMenuItems = true)]
public LSPServer Server = LSPServer.Pyright;

/// <summary>
/// If true, the communication between the language server and SEE will be logged.
/// </summary>
[Tooltip("If true, the communication between the language server and SEE will be logged."), RuntimeTab(GraphProviderFoldoutGroup)]
[InfoBox("@\"Logfiles can be found in \" + System.IO.Path.GetTempPath()", InfoMessageType.Info, nameof(LogLSP))]
public bool LogLSP;

/// <summary>
/// Returns the available language servers as a dropdown list, grouped by language.
/// </summary>
/// <returns>The available language servers as a dropdown list.</returns>
private IEnumerable<ValueDropdownItem<LSPServer>> ServerDropdown()
{
return LSPLanguage.All.Select(language => (language, LSPServer.All.Where(server => server.Languages.Contains(language))))
.SelectMany(pair => pair.Item2.Select(server => new ValueDropdownItem<LSPServer>($"{pair.language.Name}/{server.Name}", server)));
}

public override GraphProviderKind GetKind()
{
return GraphProviderKind.LSP;
}

public override async UniTask<Graph> ProvideAsync(Graph graph, AbstractSEECity city)
{
if (string.IsNullOrEmpty(ProjectPath.Path))
{
throw new ArgumentException("Empty project path.\n");
}
if (!Directory.Exists(ProjectPath.Path))
{
throw new ArgumentException($"Directory {ProjectPath.Path} does not exist.\n");
}
if (city == null)
{
throw new ArgumentException("The given city is null.\n");
}

LSPHandler handler = city.gameObject.AddOrGetComponent<LSPHandler>();
handler.enabled = true;
handler.Server = Server;
handler.ProjectPath = ProjectPath.Path;
handler.LogLSP = LogLSP;
if (Application.isPlaying)
{
await handler.WaitUntilReadyAsync();
}
else
{
// Since OnEnable is not called in the editor, we have to initialize the handler manually.
await handler.InitializeAsync();
}

SymbolInformationOrDocumentSymbolContainer result = await handler.Client.RequestDocumentSymbol(new DocumentSymbolParams
{
// TODO: Use root path to query all relevant filetypes.
TextDocument = new TextDocumentIdentifier(Path.Combine(ProjectPath.Path, "src/token/mod.rs"))
});
foreach (SymbolInformationOrDocumentSymbol symbol in result)
{
if (symbol.IsDocumentSymbolInformation)
{
Debug.LogError("This language server emits SymbolInformation, which is deprecated and not "
+ "supported by SEE. Please choose a language server that is capable of returning "
+ "hierarchic DocumentSymbols.\n");
break;
}

// TODO: Use algorithm 1 from master's thesis.
}

// We shut down the LSP server for now. If it is needed again, it can still be restarted.
if (Application.isPlaying)
{
handler.enabled = false;
}
else
{
await handler.ShutdownAsync();
}
return null;
}


#region Config I/O

/// <summary>
/// The label for <see cref="ProjectPath"/> in the configuration file.
/// </summary>
private const string pathLabel = "path";

/// <summary>
/// The label for <see cref="Server"/> in the configuration file.
/// </summary>
private const string serverLabel = "server";

/// <summary>
/// The label for <see cref="LogLSP"/> in the configuration file.
/// </summary>
private const string logLSPLabel = "logLSP";

protected override void SaveAttributes(ConfigWriter writer)
{
ProjectPath.Save(writer, pathLabel);
writer.Save(Server.Name, serverLabel);
writer.Save(LogLSP, logLSPLabel);
}

protected override void RestoreAttributes(Dictionary<string, object> attributes)
{
ProjectPath.Restore(attributes, pathLabel);
Server = LSPServer.GetByName((string)attributes[serverLabel]);
LogLSP = (bool)attributes[logLSPLabel];
}

#endregion
}
}
3 changes: 3 additions & 0 deletions Assets/SEE/GraphProviders/LSPGraphProvider.cs.meta
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 33fdae5bd80b4a2d969f3ffe997cd904
timeCreated: 1707409534
7 changes: 6 additions & 1 deletion Assets/SEE/SEE.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@
"Newtonsoft.Json.dll",
"NetworkCommsDotNet.dll",
"LibGit2Sharp.dll",
"CsvHelper.dll"
"CsvHelper.dll",
"OmniSharp.Extensions.LanguageClient.dll",
"OmniSharp.Extensions.LanguageProtocol.dll",
"OmniSharp.Extensions.LanguageServer.Shared.dll",
"OmniSharp.Extensions.JsonRpc.dll",
"System.IO.Pipelines.dll"
],
"autoReferenced": false,
"defineConstraints": [],
Expand Down
3 changes: 3 additions & 0 deletions Assets/SEE/Tools/LSP.meta
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3e06e7cebf7a49ebabb200dc61e2687b
timeCreated: 1709240001
Loading

0 comments on commit 3bd68f2

Please sign in to comment.