Merge pull request #184 from gnh1201/dev

Add some code for #183 (Auto completion)
This commit is contained in:
Namhyeon Go 2025-03-15 00:55:40 +09:00 committed by GitHub
commit 7947d91c3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 208 additions and 2 deletions

View File

@ -0,0 +1,122 @@
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace WelsonJS.Launcher
{
public class ExecutablesCollector
{
private List<string> executables = new List<string>();
public ExecutablesCollector()
{
executables.AddRange(GetInstalledSoftwareExecutables());
executables.AddRange(GetExecutablesFromPath());
}
public List<string> GetExecutables()
{
return executables;
}
private List<string> GetInstalledSoftwareExecutables()
{
List<string> executables = new List<string>();
string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryKey))
{
if (key != null)
{
foreach (string subKeyName in key.GetSubKeyNames())
{
using (RegistryKey subKey = key.OpenSubKey(subKeyName))
{
string installLocation = subKey?.GetValue("InstallLocation") as string;
string uninstallString = subKey?.GetValue("UninstallString") as string;
List<string> executablePaths = FindExecutables(installLocation, uninstallString);
executables.AddRange(executablePaths);
}
}
}
}
return executables;
}
private List<string> FindExecutables(string installLocation, string uninstallString)
{
List<string> executables = new List<string>();
if (!string.IsNullOrEmpty(installLocation) && Directory.Exists(installLocation))
{
try
{
List<string> executableFiles = Directory.GetFiles(installLocation, "*.exe", SearchOption.AllDirectories)
.OrderByDescending(f => new FileInfo(f).Length)
.ToList();
executables.AddRange(executableFiles);
}
catch (Exception ex)
{
Debug.WriteLine($"Error enumerating executables in '{installLocation}': {ex}");
}
}
if (!string.IsNullOrEmpty(uninstallString))
{
if (TryParseExecutablePath(uninstallString, out string executablePath))
{
executables.Add(executablePath);
}
}
return executables;
}
private static bool TryParseExecutablePath(string s, out string path)
{
Match match = Regex.Match(s, @"(?<=""|^)([a-zA-Z]:\\[^""]+\.exe)", RegexOptions.IgnoreCase);
if (match.Success)
{
path = match.Value;
return true;
}
path = null;
return false;
}
private List<string> GetExecutablesFromPath()
{
List<string> executables = new List<string>();
string pathEnv = Environment.GetEnvironmentVariable("PATH");
if (!string.IsNullOrEmpty(pathEnv))
{
foreach (string path in pathEnv.Split(';'))
{
if (Directory.Exists(path))
{
try
{
executables.AddRange(Directory.GetFiles(path, "*.exe", SearchOption.TopDirectoryOnly));
}
catch (Exception ex)
{
Debug.WriteLine($"Error enumerating executables in '{path}': {ex}");
}
}
}
}
return executables;
}
}
}

View File

@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Linq;
namespace WelsonJS.Launcher
{
@ -18,6 +21,7 @@ namespace WelsonJS.Launcher
private bool _isRunning;
private string _prefix;
private string _resourceName;
private ExecutablesCollector _executablesCollector;
public ResourceServer(string prefix, string resourceName)
{
@ -25,6 +29,7 @@ namespace WelsonJS.Launcher
_listener = new HttpListener();
_listener.Prefixes.Add(prefix);
_resourceName = resourceName;
_executablesCollector = new ExecutablesCollector();
}
public string GetPrefix()
@ -81,15 +86,82 @@ namespace WelsonJS.Launcher
{
string path = context.Request.Url.AbsolutePath.TrimStart('/');
// Serve the favicon.ico file
if ("favicon.ico".Equals(path, StringComparison.OrdinalIgnoreCase))
{
ServeResource(context, GetResource("favicon"), "image/x-icon");
return;
}
// Serve the code completion (word suggestion)
if (path.StartsWith("completion/", StringComparison.OrdinalIgnoreCase))
{
ServeCompletion(context, path.Substring("completion/".Length));
return;
}
// Serve a resource
ServeResource(context, GetResource(_resourceName), "text/html");
}
private void ServeCompletion(HttpListenerContext context, string word)
{
int statusCode = 200;
try
{
List<string> executables = _executablesCollector.GetExecutables();
CompletionItem[] completionItems = executables
.Where(exec => exec.IndexOf(word, 0, StringComparison.OrdinalIgnoreCase) > -1)
.Take(100) // Limit results to prevent excessive response sizes
.Select(exec => new CompletionItem
{
Label = Path.GetFileName(exec),
Kind = "Text",
Documentation = "An executable file",
InsertText = exec
})
.ToArray();
XElement response = new XElement("suggestions",
completionItems.Select(item => new XElement("item",
new XElement("label", item.Label),
new XElement("kind", item.Kind),
new XElement("documentation", item.Documentation),
new XElement("insertText", item.InsertText)
))
);
byte[] data = Encoding.UTF8.GetBytes(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" +
response.ToString()
);
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/xml";
context.Response.ContentLength64 = data.Length;
using (Stream outputStream = context.Response.OutputStream)
{
outputStream.Write(data, 0, data.Length);
}
}
catch (Exception ex)
{
byte[] errorData = Encoding.UTF8.GetBytes(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" +
$"<error>Failed to process completion request. {ex.Message}</error>"
);
context.Response.StatusCode = 500;
context.Response.ContentType = "application/xml";
context.Response.ContentLength64 = errorData.Length;
using (Stream outputStream = context.Response.OutputStream)
{
outputStream.Write(errorData, 0, errorData.Length);
}
}
}
private void ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html")
{
int statusCode = 200;
@ -105,8 +177,10 @@ namespace WelsonJS.Launcher
context.Response.StatusCode = statusCode;
context.Response.ContentType = mimeType;
context.Response.ContentLength64 = data.Length;
context.Response.OutputStream.Write(data, 0, data.Length);
context.Response.OutputStream.Close();
using (Stream outputStream = context.Response.OutputStream)
{
outputStream.Write(data, 0, data.Length);
}
}
private byte[] GetResource(string resourceName)
@ -159,4 +233,12 @@ namespace WelsonJS.Launcher
}
}
}
public class CompletionItem
{
public string Label { get; set; }
public string Kind { get; set; }
public string Documentation { get; set; }
public string InsertText { get; set; }
}
}

View File

@ -68,6 +68,7 @@
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Compile Include="EnvForm.cs">
@ -76,6 +77,7 @@
<Compile Include="EnvForm.Designer.cs">
<DependentUpon>EnvForm.cs</DependentUpon>
</Compile>
<Compile Include="ExecutablesCollector.cs" />
<Compile Include="InstancesForm.cs">
<SubType>Form</SubType>
</Compile>