Initial commit to new repository
This commit is contained in:
539
.decompiledproj/AxCopilot/Services/LspClientService.cs
Normal file
539
.decompiledproj/AxCopilot/Services/LspClientService.cs
Normal file
@@ -0,0 +1,539 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
public class LspClientService : IDisposable
|
||||
{
|
||||
private Process? _process;
|
||||
|
||||
private StreamWriter? _writer;
|
||||
|
||||
private StreamReader? _reader;
|
||||
|
||||
private int _requestId;
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
private readonly string _language;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOpts = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
public bool IsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
Process process = _process;
|
||||
return process != null && !process.HasExited && _initialized;
|
||||
}
|
||||
}
|
||||
|
||||
public LspClientService(string language)
|
||||
{
|
||||
_language = language.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public async Task<bool> StartAsync(string workspacePath, CancellationToken ct = default(CancellationToken))
|
||||
{
|
||||
string command;
|
||||
string[] args;
|
||||
(command, args) = GetServerCommand();
|
||||
if (command == null)
|
||||
{
|
||||
LogService.Warn("LSP: " + _language + "에 대한 서버를 찾을 수 없습니다.");
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
ProcessStartInfo psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = command,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8
|
||||
};
|
||||
string[] array = args;
|
||||
foreach (string arg in array)
|
||||
{
|
||||
psi.ArgumentList.Add(arg);
|
||||
}
|
||||
_process = Process.Start(psi);
|
||||
if (_process == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_writer = _process.StandardInput;
|
||||
_reader = _process.StandardOutput;
|
||||
if (!(await SendRequestAsync("initialize", new
|
||||
{
|
||||
processId = Environment.ProcessId,
|
||||
rootUri = "file:///" + workspacePath.Replace('\\', '/').TrimStart('/'),
|
||||
capabilities = new
|
||||
{
|
||||
textDocument = new
|
||||
{
|
||||
definition = new
|
||||
{
|
||||
dynamicRegistration = false
|
||||
},
|
||||
references = new
|
||||
{
|
||||
dynamicRegistration = false
|
||||
},
|
||||
documentSymbol = new
|
||||
{
|
||||
dynamicRegistration = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}, ct)).HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
await SendNotificationAsync("initialized", ct);
|
||||
_initialized = true;
|
||||
LogService.Info($"LSP [{_language}]: 서버 시작 완료 ({command})");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn("LSP [" + _language + "] 시작 실패: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LspLocation?> GotoDefinitionAsync(string filePath, int line, int character, CancellationToken ct = default(CancellationToken))
|
||||
{
|
||||
return ParseLocation(await SendRequestAsync("textDocument/definition", new
|
||||
{
|
||||
textDocument = new
|
||||
{
|
||||
uri = FileToUri(filePath)
|
||||
},
|
||||
position = new { line, character }
|
||||
}, ct));
|
||||
}
|
||||
|
||||
public async Task<List<LspLocation>> FindReferencesAsync(string filePath, int line, int character, CancellationToken ct = default(CancellationToken))
|
||||
{
|
||||
return ParseLocations(await SendRequestAsync("textDocument/references", new
|
||||
{
|
||||
textDocument = new
|
||||
{
|
||||
uri = FileToUri(filePath)
|
||||
},
|
||||
position = new { line, character },
|
||||
context = new
|
||||
{
|
||||
includeDeclaration = true
|
||||
}
|
||||
}, ct));
|
||||
}
|
||||
|
||||
public async Task<List<LspSymbol>> GetDocumentSymbolsAsync(string filePath, CancellationToken ct = default(CancellationToken))
|
||||
{
|
||||
return ParseSymbols(await SendRequestAsync("textDocument/documentSymbol", new
|
||||
{
|
||||
textDocument = new
|
||||
{
|
||||
uri = FileToUri(filePath)
|
||||
}
|
||||
}, ct));
|
||||
}
|
||||
|
||||
private async Task<JsonElement?> SendRequestAsync(string method, object parameters, CancellationToken ct)
|
||||
{
|
||||
if (_writer == null || _reader == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
int id = Interlocked.Increment(ref _requestId);
|
||||
var request = new
|
||||
{
|
||||
jsonrpc = "2.0",
|
||||
id = id,
|
||||
method = method,
|
||||
@params = parameters
|
||||
};
|
||||
string json = JsonSerializer.Serialize(request, JsonOpts);
|
||||
byte[] content = Encoding.UTF8.GetBytes(json);
|
||||
string header = $"Content-Length: {content.Length}\r\n\r\n";
|
||||
await _writer.BaseStream.WriteAsync(Encoding.ASCII.GetBytes(header), ct);
|
||||
await _writer.BaseStream.WriteAsync(content, ct);
|
||||
await _writer.BaseStream.FlushAsync(ct);
|
||||
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(15.0));
|
||||
while (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
string headerLine = await ReadHeaderAsync(cts.Token);
|
||||
if (headerLine == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int bodyLength = ParseContentLength(headerLine);
|
||||
if (bodyLength <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
byte[] body = new byte[bodyLength];
|
||||
int n;
|
||||
for (int read = 0; read < bodyLength; read += n)
|
||||
{
|
||||
n = await _reader.BaseStream.ReadAsync(body.AsMemory(read, bodyLength - read), cts.Token);
|
||||
if (n == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
string responseJson = Encoding.UTF8.GetString(body);
|
||||
try
|
||||
{
|
||||
JsonDocument doc = JsonDocument.Parse(responseJson);
|
||||
JsonElement root = doc.RootElement;
|
||||
if (root.TryGetProperty("id", out var _))
|
||||
{
|
||||
if (root.TryGetProperty("result", out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
if (root.TryGetProperty("error", out var error))
|
||||
{
|
||||
JsonElement m;
|
||||
string msg = (error.TryGetProperty("message", out m) ? m.GetString() : "Unknown");
|
||||
LogService.Warn("LSP RPC 오류: " + msg);
|
||||
return null;
|
||||
}
|
||||
result = default(JsonElement);
|
||||
error = default(JsonElement);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task SendNotificationAsync(string method, CancellationToken ct)
|
||||
{
|
||||
if (_writer != null)
|
||||
{
|
||||
var notification = new
|
||||
{
|
||||
jsonrpc = "2.0",
|
||||
method = method,
|
||||
@params = new { }
|
||||
};
|
||||
string json = JsonSerializer.Serialize(notification, JsonOpts);
|
||||
byte[] content = Encoding.UTF8.GetBytes(json);
|
||||
string header = $"Content-Length: {content.Length}\r\n\r\n";
|
||||
await _writer.BaseStream.WriteAsync(Encoding.ASCII.GetBytes(header), ct);
|
||||
await _writer.BaseStream.WriteAsync(content, ct);
|
||||
await _writer.BaseStream.FlushAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string?> ReadHeaderAsync(CancellationToken ct)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
byte[] buf = new byte[1];
|
||||
bool prevCr = false;
|
||||
int emptyLines = 0;
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
if (await _reader.BaseStream.ReadAsync(buf, ct) == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
char ch = (char)buf[0];
|
||||
switch (ch)
|
||||
{
|
||||
case '\r':
|
||||
prevCr = true;
|
||||
break;
|
||||
case '\n':
|
||||
if (prevCr && sb.Length > 0)
|
||||
{
|
||||
return sb.ToString();
|
||||
}
|
||||
if (prevCr)
|
||||
{
|
||||
emptyLines++;
|
||||
if (emptyLines >= 1 && sb.Length > 0)
|
||||
{
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
prevCr = false;
|
||||
break;
|
||||
default:
|
||||
prevCr = false;
|
||||
sb.Append(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int ParseContentLength(string header)
|
||||
{
|
||||
if (header.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
int length = "Content-Length:".Length;
|
||||
if (int.TryParse(header.Substring(length, header.Length - length).Trim(), out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static LspLocation? ParseLocation(JsonElement? result)
|
||||
{
|
||||
if (!result.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
JsonElement jsonElement = result.Value;
|
||||
if (jsonElement.ValueKind == JsonValueKind.Array && jsonElement.GetArrayLength() > 0)
|
||||
{
|
||||
jsonElement = jsonElement[0];
|
||||
}
|
||||
if (jsonElement.TryGetProperty("uri", out var value) && jsonElement.TryGetProperty("range", out var value2))
|
||||
{
|
||||
JsonElement property = value2.GetProperty("start");
|
||||
return new LspLocation
|
||||
{
|
||||
FilePath = UriToFile(value.GetString() ?? ""),
|
||||
Line = property.GetProperty("line").GetInt32(),
|
||||
Character = property.GetProperty("character").GetInt32()
|
||||
};
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<LspLocation> ParseLocations(JsonElement? result)
|
||||
{
|
||||
List<LspLocation> list = new List<LspLocation>();
|
||||
if (!result.HasValue || result.GetValueOrDefault().ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
foreach (JsonElement item in result.Value.EnumerateArray())
|
||||
{
|
||||
if (item.TryGetProperty("uri", out var value) && item.TryGetProperty("range", out var value2))
|
||||
{
|
||||
JsonElement property = value2.GetProperty("start");
|
||||
list.Add(new LspLocation
|
||||
{
|
||||
FilePath = UriToFile(value.GetString() ?? ""),
|
||||
Line = property.GetProperty("line").GetInt32(),
|
||||
Character = property.GetProperty("character").GetInt32()
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<LspSymbol> ParseSymbols(JsonElement? result)
|
||||
{
|
||||
List<LspSymbol> list = new List<LspSymbol>();
|
||||
if (!result.HasValue || result.GetValueOrDefault().ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
foreach (JsonElement item in result.Value.EnumerateArray())
|
||||
{
|
||||
JsonElement value;
|
||||
string name = (item.TryGetProperty("name", out value) ? (value.GetString() ?? "") : "");
|
||||
JsonElement value2;
|
||||
string kind = (item.TryGetProperty("kind", out value2) ? SymbolKindName(value2.GetInt32()) : "unknown");
|
||||
int line = 0;
|
||||
JsonElement value4;
|
||||
if (item.TryGetProperty("range", out var value3))
|
||||
{
|
||||
line = value3.GetProperty("start").GetProperty("line").GetInt32();
|
||||
}
|
||||
else if (item.TryGetProperty("location", out value4))
|
||||
{
|
||||
line = value4.GetProperty("range").GetProperty("start").GetProperty("line")
|
||||
.GetInt32();
|
||||
}
|
||||
list.Add(new LspSymbol
|
||||
{
|
||||
Name = name,
|
||||
Kind = kind,
|
||||
Line = line
|
||||
});
|
||||
if (!item.TryGetProperty("children", out var value5))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<LspSymbol> list2 = ParseSymbols(value5);
|
||||
foreach (LspSymbol item2 in list2)
|
||||
{
|
||||
list.Add(new LspSymbol
|
||||
{
|
||||
Name = " " + item2.Name,
|
||||
Kind = item2.Kind,
|
||||
Line = item2.Line
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static string SymbolKindName(int kind)
|
||||
{
|
||||
if (1 == 0)
|
||||
{
|
||||
}
|
||||
string result = kind switch
|
||||
{
|
||||
1 => "file",
|
||||
2 => "module",
|
||||
3 => "namespace",
|
||||
4 => "package",
|
||||
5 => "class",
|
||||
6 => "method",
|
||||
7 => "property",
|
||||
8 => "field",
|
||||
9 => "constructor",
|
||||
10 => "enum",
|
||||
11 => "interface",
|
||||
12 => "function",
|
||||
13 => "variable",
|
||||
14 => "constant",
|
||||
23 => "struct",
|
||||
24 => "event",
|
||||
_ => "symbol",
|
||||
};
|
||||
if (1 == 0)
|
||||
{
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string FileToUri(string path)
|
||||
{
|
||||
return "file:///" + path.Replace('\\', '/').TrimStart('/');
|
||||
}
|
||||
|
||||
private static string UriToFile(string uri)
|
||||
{
|
||||
if (uri.StartsWith("file:///"))
|
||||
{
|
||||
return uri.Substring(8, uri.Length - 8).Replace('/', '\\');
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private (string? Command, string[] Args) GetServerCommand()
|
||||
{
|
||||
string language = _language;
|
||||
if (1 == 0)
|
||||
{
|
||||
}
|
||||
(string, string[]) result;
|
||||
switch (language)
|
||||
{
|
||||
case "csharp":
|
||||
case "c#":
|
||||
result = FindCommand("OmniSharp", new string[2] { "omnisharp", "OmniSharp.exe" }, new string[1] { "--languageserver" });
|
||||
break;
|
||||
case "typescript":
|
||||
case "javascript":
|
||||
case "ts":
|
||||
case "js":
|
||||
result = FindCommand("TypeScript", new string[1] { "typescript-language-server" }, new string[1] { "--stdio" });
|
||||
break;
|
||||
case "python":
|
||||
case "py":
|
||||
result = FindCommand("Python", new string[2] { "pyright-langserver", "pylsp" }, new string[1] { "--stdio" });
|
||||
break;
|
||||
case "cpp":
|
||||
case "c++":
|
||||
case "c":
|
||||
result = FindCommand("C/C++", new string[1] { "clangd" }, Array.Empty<string>());
|
||||
break;
|
||||
case "java":
|
||||
result = FindCommand("Java", new string[1] { "jdtls" }, Array.Empty<string>());
|
||||
break;
|
||||
default:
|
||||
result = (null, Array.Empty<string>());
|
||||
break;
|
||||
}
|
||||
if (1 == 0)
|
||||
{
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static (string? Command, string[] Args) FindCommand(string label, string[] candidates, string[] defaultArgs)
|
||||
{
|
||||
foreach (string text in candidates)
|
||||
{
|
||||
try
|
||||
{
|
||||
using Process process = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = ((Environment.OSVersion.Platform == PlatformID.Win32NT) ? "where" : "which"),
|
||||
Arguments = text,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true
|
||||
});
|
||||
process?.WaitForExit(3000);
|
||||
if (process != null && process.ExitCode == 0)
|
||||
{
|
||||
LogService.Info($"LSP [{label}]: {text} 발견");
|
||||
return (Command: text, Args: defaultArgs);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Debug($"LSP [{label}]: {text} 탐색 실패 — {ex.Message}");
|
||||
}
|
||||
}
|
||||
return (Command: null, Args: Array.Empty<string>());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
_writer?.Dispose();
|
||||
_reader?.Dispose();
|
||||
Process process = _process;
|
||||
if (process != null && !process.HasExited)
|
||||
{
|
||||
_process.Kill(entireProcessTree: true);
|
||||
_process.Dispose();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user