Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

Commit

Permalink
Add Language Server Tests Back In (#1576)
Browse files Browse the repository at this point in the history
  • Loading branch information
ScottCarda-MS authored Nov 30, 2022
1 parent 16c5e86 commit 82e9304
Show file tree
Hide file tree
Showing 22 changed files with 347 additions and 161 deletions.
19 changes: 17 additions & 2 deletions QsCompiler.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28809.33
# Visual Studio Version 17
VisualStudioVersion = 17.4.33103.184
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompilationManager", "src\QsCompiler\CompilationManager\CompilationManager.csproj", "{8990670B-B9D2-4485-AA5B-34A301A11CD3}"
EndProject
Expand Down Expand Up @@ -73,6 +73,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "App", "src\QsFmt\App\App.fs
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Telemetry", "src\Telemetry\Library\Telemetry.csproj", "{2A562128-2FD0-47CF-B457-DDF3C19CCDAC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.LanguageServer", "src\QsCompiler\Tests.LanguageServer\Tests.LanguageServer.csproj", "{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -431,6 +433,18 @@ Global
{2A562128-2FD0-47CF-B457-DDF3C19CCDAC}.Release|x64.Build.0 = Release|Any CPU
{2A562128-2FD0-47CF-B457-DDF3C19CCDAC}.Release|x86.ActiveCfg = Release|Any CPU
{2A562128-2FD0-47CF-B457-DDF3C19CCDAC}.Release|x86.Build.0 = Release|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Debug|x64.ActiveCfg = Debug|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Debug|x64.Build.0 = Debug|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Debug|x86.ActiveCfg = Debug|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Debug|x86.Build.0 = Debug|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Release|Any CPU.Build.0 = Release|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Release|x64.ActiveCfg = Release|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Release|x64.Build.0 = Release|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Release|x86.ActiveCfg = Release|Any CPU
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -454,6 +468,7 @@ Global
{4D1F507F-7382-4F5B-9923-58398A565D8B} = {755A6971-AD80-4519-A64E-0333B89C1E9D}
{AD350766-EE10-4DE3-A834-129D026487FB} = {755A6971-AD80-4519-A64E-0333B89C1E9D}
{2A562128-2FD0-47CF-B457-DDF3C19CCDAC} = {755A6971-AD80-4519-A64E-0333B89C1E9D}
{A9E3087B-F21A-45D4-A0A8-DF1DB0043233} = {B4A9484D-31FC-4A27-9E26-4C8DE3E02D77}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B921C36B-4574-4025-8FE3-E5BD2D3D2B81}
Expand Down
2 changes: 1 addition & 1 deletion src/QsCompiler/CompilationManager/ProcessingQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public void QueueForExecution(Action processing) =>
/// </remarks>
public bool QueueForExecution<T>(Func<T> execute, [MaybeNull] out T result)
{
T res = default(T);
T res = default;
var succeeded = true;
this.QueueForExecutionAsync(() =>
{
Expand Down
49 changes: 36 additions & 13 deletions src/QsCompiler/CompilationManager/ProjectManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public class ProjectProperties
? path ?? string.Empty
: string.Empty;

private ImmutableDictionary<string, string?> BuildProperties { get; }
internal ImmutableDictionary<string, string?> BuildProperties { get; }

public static ProjectProperties Empty =>
new ProjectProperties(ImmutableDictionary<string, string?>.Empty);
Expand All @@ -123,7 +123,7 @@ public class ProjectInformation
{
public delegate bool Loader(Uri projectFile, [NotNullWhen(true)] out ProjectInformation? projectInfo);

internal ProjectProperties Properties { get; }
public ProjectProperties Properties { get; }

public ImmutableArray<string> SourceFiles { get; }

Expand Down Expand Up @@ -309,6 +309,16 @@ private void SetProjectInformation(ProjectInformation projectInfo)
.ToImmutableHashSet();
}

/// <summary>
/// Retrieves the currently set project information that is used to load the project.
/// </summary>
internal ProjectInformation GetProjectInformation() =>
new ProjectInformation(
sourceFiles: this.specifiedSourceFiles.Select(uri => uri.AbsolutePath),
projectReferences: this.specifiedProjectReferences.Select(uri => uri.AbsolutePath),
references: this.specifiedReferences.Select(uri => uri.AbsolutePath),
buildProperties: this.Properties.BuildProperties);

/// <summary>
/// If the project is not yet loaded, loads all specified source file, dll references and project references
/// using <paramref name="projectOutputPaths"/> to resolve the dll output paths for project references.
Expand Down Expand Up @@ -1441,24 +1451,37 @@ static CodeAction CreateAction(string title, WorkspaceEdit edit) =>
/// This method waits for all currently running or queued tasks to finish
/// before getting the file content.
/// </remarks>
public string[]? FileContentInMemory(TextDocumentIdentifier textDocument)
{
if (textDocument?.Uri == null)
{
return null;
}

this.load.QueueForExecution(
public string[]? FileContentInMemory(TextDocumentIdentifier textDocument) =>
textDocument?.Uri != null && this.load.QueueForExecution(
() =>
{
// NOTE: the call below prevents any consolidating of the processing queues
// of the project manager and the compilation unit manager (dead locks)!
var manager = this.Manager(textDocument.Uri);
return manager?.FileContentInMemory(textDocument);
},
out var content);
return content;
}
out var content)
? content : null;

/// <summary>
/// Returns the project information stored for the project defined by
/// the given <paramref name="projectFile"/>, if it is listed as a
/// project with this manager.
/// </summary>
/// <remarks>
/// Returns null if the given project file is null or the project is not
/// registered with this manager.
/// <para/>
/// This method waits for all currently running or queued tasks to finish
/// before getting the file content.
/// </remarks>
public ProjectInformation? GetProjectInformation(TextDocumentIdentifier projectFile) =>
projectFile?.Uri != null && this.load.QueueForExecution(
() => this.projects.TryGetValue(projectFile.Uri, out Project project)
? project.GetProjectInformation()
: null,
out var info)
? info : null;

/* static routines related to loading the content needed for compilation */

Expand Down
1 change: 1 addition & 0 deletions src/QsCompiler/LanguageServer/Communication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public static class CommandIds
// commands for diagnostic purposes
internal const string FileContentInMemory = "qsLanguageServer/fileContentInMemory";
internal const string FileDiagnostics = "qsLanguageServer/fileDiagnostics";
internal const string ProjectInformation = "qsLanguageServer/projectInformation";
}

public class ProtocolError
Expand Down
50 changes: 45 additions & 5 deletions src/QsCompiler/LanguageServer/EditorState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ internal class EditorState : IDisposable
/// needed to determine if the reality of a source file that has changed on disk is indeed given by the content on disk,
/// or whether its current state as it is in the editor needs to be preserved
/// </summary>
private readonly ConcurrentDictionary<Uri, FileContentManager> openFiles =
new ConcurrentDictionary<Uri, FileContentManager>();
private readonly ConcurrentDictionary<Uri, FileContentManager> openFiles = new();

private FileContentManager? GetOpenFile(Uri key) => this.openFiles.TryGetValue(key, out var file) ? file : null;

Expand Down Expand Up @@ -203,7 +202,7 @@ static bool GeneratePackageInfo(string packageName) =>
/// and publishes suitable diagnostics for it.
/// </summary>
public Task LoadProjectsAsync(IEnumerable<Uri> projects) =>
this.projectLoader is object ?
this.projectLoader is not null ?
this.projects.LoadProjectsAsync(projects, this.QsProjectLoader, this.GetOpenFile) :
Task.CompletedTask;

Expand All @@ -212,7 +211,7 @@ this.projectLoader is object ?
/// updates that project in the list of tracked projects or adds it if needed, and publishes suitable diagnostics for it.
/// </summary>
public Task ProjectDidChangeOnDiskAsync(Uri project) =>
this.projectLoader is object ?
this.projectLoader is not null ?
this.projects.ProjectChangedOnDiskAsync(project, this.QsProjectLoader, this.GetOpenFile) :
Task.CompletedTask;

Expand Down Expand Up @@ -545,7 +544,48 @@ internal Task CloseFileAsync(TextDocumentIdentifier textDocument, Action<string,
internal Diagnostic[]? FileDiagnostics(TextDocumentIdentifier textDocument)
{
var allDiagnostics = this.projects.GetDiagnostics(textDocument?.Uri);
return allDiagnostics?.Count() == 1 ? allDiagnostics.Single().Diagnostics : null; // count is > 1 if the given uri corresponds to a project file
return allDiagnostics?.Length == 1 ? allDiagnostics.Single().Diagnostics : null; // count is > 1 if the given uri corresponds to a project file
}

/// <summary>
/// Waits for all currently running or queued tasks to finish before getting the project information.
/// -> Method to be used for testing/diagnostic purposes only!
/// </summary>
internal string? ProjectInformation(TextDocumentIdentifier textDocument)
{
var projectInfo = this.projects.GetProjectInformation(textDocument);
if (projectInfo is null)
{
return null;
}

var stringWriter = new StringWriter();
var writer = System.Xml.XmlWriter.Create(stringWriter);
void WriteElementGroup(string groupName, IEnumerable<string> paths)
{
writer.WriteStartElement(groupName);
foreach (var path in paths)
{
writer.WriteStartElement("File");
writer.WriteAttributeString("Path", path);
writer.WriteEndElement();
}

writer.WriteEndElement();
}

writer.WriteStartDocument();
writer.WriteStartElement("ProjectInfo");
writer.WriteAttributeString("OutputPath", projectInfo.Properties.DllOutputPath);
writer.WriteAttributeString("TargetCapability", projectInfo.Properties.TargetCapability.Name);
writer.WriteAttributeString("ProcessorArchitecture", projectInfo.Properties.ProcessorArchitecture);
WriteElementGroup("Sources", projectInfo.SourceFiles);
WriteElementGroup("ProjectReferences", projectInfo.ProjectReferences);
WriteElementGroup("References", projectInfo.References);
writer.WriteEndElement();
writer.WriteEndDocument();
writer.Flush();
return stringWriter.ToString();
}
}
}
15 changes: 7 additions & 8 deletions src/QsCompiler/LanguageServer/FileSystemWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Security.Permissions;
using System.Threading.Tasks;
using Microsoft.Quantum.QsCompiler;
using Microsoft.Quantum.QsCompiler.CompilationBuilder;
Expand All @@ -19,16 +18,16 @@ namespace Microsoft.Quantum.QsLanguageServer
/// <summary>
/// This class provides a basic file watcher for the LSP that sends notifications on watched files
/// when they change on disk (i.e. are added, removed, or edited).
/// The implemeneted mechanism is far from perfect and will fail in some cases -
/// in particular I expect it to fail (silently) for autogenerated file system edits generated in rapid succession,
/// The implemented mechanism is far from perfect and will fail in some cases -
/// in particular I expect it to fail (silently) for auto-generated file system edits generated in rapid succession,
/// especially in combination with large batch edits in the file system.
/// However, all in all splitting out the logic to generate notifications for individual files here (even if it is less than perfect),
/// seems better and overall less error prone than having to deal with less abstraction on the server side.
/// I am fully aware that this mechanism is not a neat solution, and it should probably be revised at some point in the future.
/// </summary>
internal class FileWatcher
{
private readonly ConcurrentBag<System.IO.FileSystemWatcher> watchers;
private readonly ConcurrentBag<FileSystemWatcher> watchers;
private readonly Action<Exception> onException;

private void OnBufferOverflow(object sender, ErrorEventArgs e)
Expand Down Expand Up @@ -61,7 +60,7 @@ private void OnBufferOverflow(object sender, ErrorEventArgs e)
public FileWatcher(Action<Exception> onException)
{
this.onException = onException;
this.watchers = new ConcurrentBag<System.IO.FileSystemWatcher>();
this.watchers = new ConcurrentBag<FileSystemWatcher>();
this.watchedDirectories = new Dictionary<Uri, ImmutableHashSet<string>>();
this.processing = new ProcessingQueue(this.onException, "error in file system watcher");
this.globPatterns = new ConcurrentDictionary<Uri, IEnumerable<string>>();
Expand All @@ -71,9 +70,9 @@ public FileWatcher(Action<Exception> onException)
/// Returns a file system watcher for the given folder and pattern, with the proper event handlers added.
/// IMPORTANT: The returned watcher is disabled and needs to be enabled by setting EnableRaisingEvents to true.
/// </summary>
private System.IO.FileSystemWatcher GetWatcher(string folder, string pattern, NotifyFilters notifyOn)
private FileSystemWatcher GetWatcher(string folder, string pattern, NotifyFilters notifyOn)
{
var watcher = new System.IO.FileSystemWatcher
var watcher = new FileSystemWatcher
{
NotifyFilter = notifyOn,
Filter = pattern,
Expand Down Expand Up @@ -206,7 +205,7 @@ private void RecurCreated(string fullPath, IDictionary<Uri, ImmutableHashSet<str
public void OnCreated(object source, FileSystemEventArgs e)
{
var directories = new Dictionary<Uri, ImmutableHashSet<string>>();
if (source is System.IO.FileSystemWatcher watcher &&
if (source is FileSystemWatcher watcher &&
this.globPatterns.TryGetValue(new Uri(watcher.Path), out var globPatterns))
{
var maxNrTries = 10; // copied directories need some time until they are on disk -> todo: better solution?
Expand Down
1 change: 1 addition & 0 deletions src/QsCompiler/LanguageServer/LanguageServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ public object OnCodeAction(JToken arg)
this.rpc.InvokeWithParameterObjectAsync<ApplyWorkspaceEditResponse>(Methods.WorkspaceApplyEditName, edit)) :
param.Command == CommandIds.FileContentInMemory ? CastAndExecute<TextDocumentIdentifier>(this.editorState.FileContentInMemory) :
param.Command == CommandIds.FileDiagnostics ? CastAndExecute<TextDocumentIdentifier>(this.editorState.FileDiagnostics) :
param.Command == CommandIds.ProjectInformation ? CastAndExecute<TextDocumentIdentifier>(this.editorState.ProjectInformation) :
null;
}
catch
Expand Down
34 changes: 30 additions & 4 deletions src/QsCompiler/LanguageServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,27 @@ public class Options
HelpText = "Port to use for TCP/IP connections.")]
public int Port { get; set; }

[Option(
"unnamed",
Required = false,
SetName = ConnectionViaPipe,
HelpText = "Connect via anonymous pipes.")]
internal bool UseAnonymousPipes { get; set; }

[Option(
'w',
"writer",
Required = true,
SetName = ConnectionViaPipe,
HelpText = "Named pipe to write to.")]
HelpText = "Name of handle of the pipe to write to.")]
public string? WriterPipeName { get; set; }

[Option(
'r',
"reader",
Required = true,
SetName = ConnectionViaPipe,
HelpText = "Named pipe to read from.")]
HelpText = "Name of handle of the pipe to read from.")]
public string? ReaderPipeName { get; set; }

[Option(
Expand Down Expand Up @@ -133,7 +140,7 @@ private static int Run(Options options)
server = options.UseStdInOut
? ConnectViaStdInOut(options.LogFile)
: options.ReaderPipeName != null && options.WriterPipeName != null
? ConnectViaNamedPipe(options.WriterPipeName, options.ReaderPipeName, options.LogFile)
? ConnectViaPipes(options.WriterPipeName, options.ReaderPipeName, options.UseAnonymousPipes, options.LogFile)
: ConnectViaSocket(port: options.Port, logFile: options.LogFile);
}
catch (Exception ex)
Expand Down Expand Up @@ -181,7 +188,12 @@ internal static QsLanguageServer ConnectViaStdInOut(string? logFile = null)
return new QsLanguageServer(Console.OpenStandardOutput(), Console.OpenStandardInput());
}

internal static QsLanguageServer ConnectViaNamedPipe(string writerName, string readerName, string? logFile = null)
internal static QsLanguageServer ConnectViaPipes(string writer, string reader, bool useAnonymousPipes, string? logFile = null) =>
useAnonymousPipes
? ConnectViaAnonymousPipes(writer, reader, logFile)
: ConnectViaNamedPipes(writer, reader, logFile);

internal static QsLanguageServer ConnectViaNamedPipes(string writerName, string readerName, string? logFile = null)
{
Log($"Connecting via named pipe. {Environment.NewLine}ReaderPipe: \"{readerName}\" {Environment.NewLine}WriterPipe: \"{writerName}\"", logFile, stdout: true);
var writerPipe = new NamedPipeClientStream(writerName);
Expand All @@ -202,6 +214,20 @@ internal static QsLanguageServer ConnectViaNamedPipe(string writerName, string r
return new QsLanguageServer(writerPipe, readerPipe);
}

internal static QsLanguageServer ConnectViaAnonymousPipes(string writerHandle, string readerHandle, string? logFile = null)
{
Log($"Connecting via anonymous pipe.", logFile, stdout: true);

var writerPipe = new AnonymousPipeClientStream(PipeDirection.Out, writerHandle);
var readerPipe = new AnonymousPipeClientStream(PipeDirection.In, readerHandle);
if (!writerPipe.IsConnected || !readerPipe.IsConnected)
{
Log($"[ERROR] Connection failed.", logFile);
}

return new QsLanguageServer(writerPipe, readerPipe);
}

internal static QsLanguageServer ConnectViaSocket(string hostname = "localhost", int port = 8008, string? logFile = null)
{
Log($"Connecting via socket. {Environment.NewLine}Port number: {port}", logFile, stdout: true);
Expand Down
Loading

0 comments on commit 82e9304

Please sign in to comment.