Fix issue with file executable path auto-completion

This commit is contained in:
Namhyeon Go 2025-05-24 15:33:07 +09:00
parent 7e29cc649f
commit d9f9d3e38b
2 changed files with 127 additions and 152 deletions

View File

@ -17,19 +17,16 @@ namespace WelsonJS.Launcher.ResourceTools
private readonly ResourceServer Server; private readonly ResourceServer Server;
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private const string Prefix = "completion/"; private const string Prefix = "completion/";
private List<string> Executables = new List<string>(); private List<string> DiscoverdExecutables = new List<string>();
public Completion(ResourceServer server, HttpClient httpClient) public Completion(ResourceServer server, HttpClient httpClient)
{ {
Server = server; Server = server;
_httpClient = httpClient; _httpClient = httpClient;
new Task(() => Task.Run(() => DiscoverFromInstalledSoftware());
{ Task.Run(() => DiscoverFromPathVariable());
Executables.AddRange(GetInstalledSoftwareExecutables()); Task.Run(() => DiscoverFromProgramDirectories());
Executables.AddRange(GetExecutablesFromPath());
Executables.AddRange(GetExecutablesFromNetFx());
}).Start();
} }
public bool CanHandle(string path) public bool CanHandle(string path)
@ -45,7 +42,7 @@ namespace WelsonJS.Launcher.ResourceTools
try try
{ {
CompletionItem[] completionItems = Executables CompletionItem[] completionItems = DiscoverdExecutables
.Where(exec => exec.IndexOf(word, 0, StringComparison.OrdinalIgnoreCase) > -1) .Where(exec => exec.IndexOf(word, 0, StringComparison.OrdinalIgnoreCase) > -1)
.Take(100) // Limit the number of results .Take(100) // Limit the number of results
.Select(exec => new CompletionItem .Select(exec => new CompletionItem
@ -70,137 +67,109 @@ namespace WelsonJS.Launcher.ResourceTools
} }
catch (Exception ex) catch (Exception ex)
{ {
Server.ServeResource(context, $"<error>Failed to process completion request. {ex.Message}</error>", "application/xml", 500); Server.ServeResource(context, $"<error>Failed to try completion. {ex.Message}</error>", "application/xml", 500);
} }
} }
private List<string> GetInstalledSoftwareExecutables() private void DiscoverFromInstalledSoftware()
{ {
List<string> executables = new List<string>(); const string registryPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryKey)) using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryPath))
{ {
if (key != null) if (key == null) return;
{
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); foreach (string subKeyName in key.GetSubKeyNames())
executables.AddRange(executablePaths); {
RegistryKey subKey = key.OpenSubKey(subKeyName);
if (subKey == null) continue;
using (subKey)
{
string installLocation = subKey.GetValue("InstallLocation") as string;
if (!string.IsNullOrEmpty(installLocation))
{
SearchAllExecutables(installLocation);
}
string uninstallString = subKey.GetValue("UninstallString") as string;
if (!string.IsNullOrEmpty(uninstallString))
{
var match = Regex.Match(uninstallString, @"(?<=""|^)([a-zA-Z]:\\[^""]+\.exe)", RegexOptions.IgnoreCase);
if (match.Success && File.Exists(match.Value))
{
DiscoverdExecutables.Add(match.Value);
}
} }
} }
} }
} }
return executables;
} }
private List<string> FindExecutables(string installLocation, string uninstallString) private void DiscoverFromPathVariable() {
{ var paths = (Environment.GetEnvironmentVariable("PATH") ?? string.Empty)
List<string> executables = new List<string>(); .Split(';')
.Select(p => p.Trim())
.Where(p => !string.IsNullOrEmpty(p));
if (!string.IsNullOrEmpty(installLocation) && Directory.Exists(installLocation)) foreach (string path in paths)
{ {
try SearchAllExecutables(path, SearchOption.TopDirectoryOnly);
{
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) private void DiscoverFromProgramDirectories()
{ {
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[] paths = Environment.GetEnvironmentVariable("PATH")?.Split(';');
if (paths != null)
{
foreach (string path in paths)
{
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;
}
private List<string> GetExecutablesFromNetFx()
{
List<string> executables = new List<string>();
string windir = Environment.GetEnvironmentVariable("WINDIR"); string windir = Environment.GetEnvironmentVariable("WINDIR");
string programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (!string.IsNullOrEmpty(windir)) var paths = new[]
{ {
string[] paths = new string[] // Default directory
{ Path.Combine(Program.GetAppDataPath(), "bin"),
Path.Combine(windir, "Microsoft.NET", "Framework"),
Path.Combine(windir, "Microsoft.NET", "Framework64")
};
foreach (string path in paths) // Standard program installation directories
{ Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
if (Directory.Exists(path)) Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
{
try // .NET Framework directories
{ Path.Combine(windir, "Microsoft.NET", "Framework"),
executables.AddRange(Directory.GetFiles(path, "*.exe", SearchOption.AllDirectories)); Path.Combine(windir, "Microsoft.NET", "Framework64"),
}
catch (Exception ex) // Chocolatey package directory
{ Path.Combine(programData, "chocolatey", "lib"),
Debug.WriteLine($"Error enumerating executables in '{path}': {ex}");
} // Scoop apps directory
} Path.Combine(userProfile, "scoop", "apps")
} };
foreach (string path in paths)
{
SearchAllExecutables(path);
}
}
private void SearchAllExecutables(string path, SearchOption searchOption = SearchOption.AllDirectories)
{
if (!Directory.Exists(path))
{
Trace.TraceWarning("Directory does not exist: {0}", path);
return;
} }
return executables; try
{
var executableFiles = Directory.GetFiles(path, "*.exe", searchOption)
.OrderByDescending(f => new FileInfo(f).Length)
.ToList();
DiscoverdExecutables.AddRange(executableFiles);
}
catch (Exception ex)
{
Trace.TraceWarning("Error enumerating executables in '{0}': {1}", path, ex.Message);
}
} }
} }

View File

@ -170,6 +170,31 @@
function Editor({ editorRef }) { function Editor({ editorRef }) {
const containerRef = React.useRef(null); const containerRef = React.useRef(null);
const getSuggestions = (word, range) => axios.get(`/completion/${encodeURIComponent(word)}`)
.then(response => {
const parser = new XMLParser();
const result = parser.parse(response.data);
const suggestions = ((item) => Array.isArray(item) ? item : [item])(result.suggestions.item).map((item) => {
return {
label: item.label,
kind: monaco.languages.CompletionItemKind.Text,
documentation: item.documentation || "",
insertText: '"' + item.insertText + '"',
range: range
};
});
return {
suggestions: suggestions
};
})
.catch(function () {
return {
suggestions: []
};
});
React.useEffect(() => { React.useEffect(() => {
if (!containerRef.current) if (!containerRef.current)
return; return;
@ -180,6 +205,20 @@
language: 'javascript' language: 'javascript'
}); });
monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems: function (model, position) {
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
};
return getSuggestions(word.word, range);
}
});
editorRef.current = instance; editorRef.current = instance;
}); });
}, []); }, []);
@ -234,14 +273,13 @@
} }
function App() { function App() {
const serverPrefix = "http://localhost:3000/";
const editorRef = React.useRef(null); const editorRef = React.useRef(null);
const promptEditorRef = React.useRef(null); const promptEditorRef = React.useRef(null);
const settingsRef = React.useRef({}); const settingsRef = React.useRef({});
const fileNameRef = React.useRef('sayhello.js'); const fileNameRef = React.useRef('sayhello.js');
const promptMessagesRef = React.useRef([]); const promptMessagesRef = React.useRef([]);
const fetchSettings = () => axios.get(`${serverPrefix}settings`) const fetchSettings = () => axios.get(`/settings`)
.then(response => { .then(response => {
const parser = new XMLParser(); const parser = new XMLParser();
const result = parser.parse(response.data); const result = parser.parse(response.data);
@ -259,38 +297,6 @@
} }
}; };
const getSuggestions = (word) => axios.get(`${serverPrefix}completion/${encodeURIComponent(word)}`)
.then(response => {
const parser = new XMLParser();
const result = parser.parse(response.data);
if (!result.suggestions || !result.suggestions.item) {
return {
suggestions: []
};
}
const items = Array.isArray(result.suggestions.item) ? result.suggestions.item : [result.suggestions.item];
const suggestions = items.map(function (item) {
return {
label: item.label,
kind: monaco.languages.CompletionItemKind.Text,
documentation: item.documentation || "",
insertText: '"' + item.insertText + '"',
range: range
};
});
return {
suggestions: suggestions
};
})
.catch(function () {
return {
suggestions: []
};
});
const pushPromptMessage = (role, content) => { const pushPromptMessage = (role, content) => {
promptMessagesRef.current.push({ promptMessagesRef.current.push({
role: role, role: role,
@ -442,7 +448,7 @@
}; };
const getTargetByUrl = async (urlPart) => { const getTargetByUrl = async (urlPart) => {
const response = await fetch(`${serverPrefix}devtools/json`); const response = await fetch(`/devtools/json`);
const targets = await response.json(); const targets = await response.json();
const target = targets.find(target => target.url.includes(urlPart)); const target = targets.find(target => target.url.includes(urlPart));
@ -542,7 +548,7 @@
return; return;
} }
axios.get(`${serverPrefix}whois/${hostname}`).then(response => { axios.get(`/whois/${hostname}`).then(response => {
const responseText = DOMPurify.sanitize(response.data, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }); const responseText = DOMPurify.sanitize(response.data, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] });
appendTextToEditor(`/*\nHostname:${hostname}\n\n${responseText}\n*/`); appendTextToEditor(`/*\nHostname:${hostname}\n\n${responseText}\n*/`);
pushPromptMessage("system", responseText); pushPromptMessage("system", responseText);
@ -558,7 +564,7 @@
return; return;
} }
axios.get(`${serverPrefix}dns-query/${hostname}`).then(response => { axios.get(`/dns-query/${hostname}`).then(response => {
const responseText = response.data; const responseText = response.data;
appendTextToEditor(`/*\nHostname:${hostname}\n\n${responseText}\n*/`); appendTextToEditor(`/*\nHostname:${hostname}\n\n${responseText}\n*/`);
pushPromptMessage("system", responseText); pushPromptMessage("system", responseText);
@ -583,7 +589,7 @@
const apiPrefix = settingsRef.current.CitiApiPrefix; const apiPrefix = settingsRef.current.CitiApiPrefix;
const ip = encodeURIComponent(hostname.trim()); const ip = encodeURIComponent(hostname.trim());
axios.get(`${serverPrefix}citi-query/${hostname}`).then(response => { axios.get(`/citi-query/${hostname}`).then(response => {
const parser = new XMLParser(); const parser = new XMLParser();
const result = parser.parse(response.data); const result = parser.parse(response.data);
const data = JSON.parse(result.json); const data = JSON.parse(result.json);