feat: add molc

* use molc for ELS tests
This commit is contained in:
Shunsuke Shibayama 2023-09-08 02:13:13 +09:00
parent 6ca5e07191
commit dcb42f68b9
30 changed files with 884 additions and 797 deletions

View file

@ -23,13 +23,11 @@ experimental = ["erg_common/experimental", "erg_compiler/experimental"]
[dependencies]
erg_common = { workspace = true, features = ["els"] }
erg_compiler = { workspace = true, features = ["els"] }
molc = { workspace = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85"
lsp-types = { version = "0.93.2", features = ["proposed"] }
[dev-dependencies]
gag = "1"
[lib]
path = "lib.rs"

View file

@ -11,7 +11,7 @@ use lsp_types::{
};
use crate::_log;
use crate::server::{ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::symbol::symbol_kind;
use crate::util::{abs_loc_to_lsp_loc, loc_to_pos, NormalizedUrl};
@ -35,7 +35,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
params: CallHierarchyIncomingCallsParams,
) -> ELSResult<Option<Vec<CallHierarchyIncomingCall>>> {
let mut res = vec![];
_log!("call hierarchy incoming calls requested: {params:?}");
_log!(self, "call hierarchy incoming calls requested: {params:?}");
let Some(data) = params.item.data.as_ref().and_then(|d| d.as_str()) else {
return Ok(None);
};
@ -80,7 +80,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: CallHierarchyOutgoingCallsParams,
) -> ELSResult<Option<Vec<CallHierarchyOutgoingCall>>> {
_log!("call hierarchy outgoing calls requested: {params:?}");
_log!(self, "call hierarchy outgoing calls requested: {params:?}");
Ok(None)
}
@ -88,7 +88,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: CallHierarchyPrepareParams,
) -> ELSResult<Option<Vec<CallHierarchyItem>>> {
_log!("call hierarchy prepare requested: {params:?}");
_log!(self, "call hierarchy prepare requested: {params:?}");
let mut res = vec![];
let uri = NormalizedUrl::new(params.text_document_position_params.text_document.uri);
let pos = params.text_document_position_params.position;

View file

@ -14,7 +14,7 @@ use lsp_types::{
Position, Range, TextEdit, Url, WorkspaceEdit,
};
use crate::server::{send_log, ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{self, NormalizedUrl};
impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
@ -29,11 +29,11 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
};
let mut map = HashMap::new();
let Some(visitor) = self.get_visitor(&uri) else {
send_log("visitor not found")?;
self.send_log("visitor not found")?;
return Ok(None);
};
let Some(result) = self.analysis_result.get(&uri) else {
send_log("artifact not found")?;
self.send_log("artifact not found")?;
return Ok(None);
};
let warns = result
@ -50,7 +50,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
match visitor.get_min_expr(pos) {
Some(Expr::Def(def)) => {
let Some(mut range) = util::loc_to_range(def.loc()) else {
send_log("range not found")?;
self.send_log("range not found")?;
continue;
};
let next = lsp_types::Range {
@ -72,7 +72,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
}
Some(";") => range.end.character += 1,
Some(other) => {
send_log(format!("? {other}"))?;
self.send_log(format!("? {other}"))?;
}
}
let edit = TextEdit::new(range, "".to_string());
@ -228,7 +228,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: CodeActionParams,
) -> ELSResult<Option<CodeActionResponse>> {
send_log(format!("code action requested: {params:?}"))?;
self.send_log(format!("code action requested: {params:?}"))?;
let result = match params
.context
.only
@ -238,7 +238,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
Some("quickfix") => self.send_quick_fix(&params)?,
None => self.send_normal_action(&params)?,
Some(other) => {
send_log(&format!("Unknown code action requested: {other}"))?;
self.send_log(&format!("Unknown code action requested: {other}"))?;
vec![]
}
};
@ -254,7 +254,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
action: CodeAction,
) -> ELSResult<CodeAction> {
send_log(format!("code action resolve requested: {action:?}"))?;
self.send_log(format!("code action resolve requested: {action:?}"))?;
match &action.title[..] {
"Extract into function" | "Extract into variable" => {
self.resolve_extract_action(action)

View file

@ -4,7 +4,7 @@ use erg_compiler::hir::Expr;
use lsp_types::{CodeLens, CodeLensParams};
use crate::server::{send_log, ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{self, NormalizedUrl};
impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
@ -12,7 +12,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: CodeLensParams,
) -> ELSResult<Option<Vec<CodeLens>>> {
send_log("code lens requested")?;
self.send_log("code lens requested")?;
let uri = NormalizedUrl::new(params.text_document.uri);
// TODO: parallelize
let result = [

View file

@ -8,7 +8,7 @@ use erg_compiler::hir::Expr;
use lsp_types::{Command, ExecuteCommandParams, Location, Url};
use crate::_log;
use crate::server::{ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{self, NormalizedUrl};
impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
@ -16,11 +16,11 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: ExecuteCommandParams,
) -> ELSResult<Option<Value>> {
_log!("command requested: {}", params.command);
_log!(self, "command requested: {}", params.command);
#[allow(clippy::match_single_binding)]
match &params.command[..] {
other => {
_log!("unknown command {other}: {params:?}");
_log!(self, "unknown command {other}: {params:?}");
Ok(None)
}
}

View file

@ -31,7 +31,7 @@ use lsp_types::{
MarkupContent, MarkupKind, Position, Range, TextEdit,
};
use crate::server::{send_log, ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{self, NormalizedUrl};
fn comp_item_kind(vi: &VarInfo) -> CompletionItemKind {
@ -355,7 +355,7 @@ impl CompletionCache {
let clone = cache.clone();
spawn_new_thread(
move || {
crate::_log!("load_modules");
// crate::_log!("load_modules");
let major_mods = [
"argparse",
"array",
@ -490,7 +490,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: CompletionParams,
) -> ELSResult<Option<CompletionResponse>> {
send_log(format!("completion requested: {params:?}"))?;
self.send_log(format!("completion requested: {params:?}"))?;
let uri = NormalizedUrl::new(params.text_document_position.text_document.uri);
let path = util::uri_to_path(&uri);
let pos = params.text_document_position.position;
@ -514,7 +514,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
Some("(") => CompletionKind::LParen,
_ => CompletionKind::Local,
};
send_log(format!("CompletionKind: {comp_kind:?}"))?;
self.send_log(format!("CompletionKind: {comp_kind:?}"))?;
let mut result: Vec<CompletionItem> = vec![];
let mut already_appeared = Set::new();
let contexts = if comp_kind.should_be_local() {
@ -629,7 +629,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
}
result.extend(self.neighbor_completion(&uri, arg_pt, &mut already_appeared));
}
send_log(format!("completion items: {}", result.len()))?;
self.send_log(format!("completion items: {}", result.len()))?;
Ok(Some(CompletionResponse::Array(result)))
}
@ -637,7 +637,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
mut item: CompletionItem,
) -> ELSResult<CompletionItem> {
send_log(format!("completion resolve requested: {item:?}"))?;
self.send_log(format!("completion resolve requested: {item:?}"))?;
if let Some(data) = &item.data {
let mut contents = vec![];
let Ok(def_loc) = data.as_str().unwrap_or_default().parse::<AbsLocation>() else {

View file

@ -10,7 +10,7 @@ use erg_compiler::varinfo::VarInfo;
use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse, Location, Position, Url};
use crate::server::{send_log, ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{self, NormalizedUrl};
impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
@ -20,12 +20,12 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
token: &Token,
) -> ELSResult<Option<VarInfo>> {
if !token.category_is(TokenCategory::Symbol) {
send_log(format!("not symbol: {token}"))?;
self.send_log(format!("not symbol: {token}"))?;
Ok(None)
} else if let Some(visitor) = self.get_visitor(uri) {
Ok(visitor.get_info(token))
} else {
send_log("not found")?;
self.send_log("not found")?;
Ok(None)
}
}
@ -99,7 +99,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
return Ok(Some(lsp_types::Location::new(def_uri, range)));
}
_ => {
send_log("not found (maybe builtin)")?;
self.send_log("not found (maybe builtin)")?;
return Ok(None);
}
}
@ -112,7 +112,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
Ok(Some(lsp_types::Location::new(def_uri, range)))
}
_ => {
send_log("not found (maybe builtin)")?;
self.send_log("not found (maybe builtin)")?;
Ok(None)
}
}
@ -120,7 +120,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
Ok(None)
}
} else {
send_log("lex error occurred")?;
self.send_log("lex error occurred")?;
Ok(None)
}
}
@ -129,7 +129,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: GotoDefinitionParams,
) -> ELSResult<Option<GotoDefinitionResponse>> {
send_log(format!("definition requested: {params:?}"))?;
self.send_log(format!("definition requested: {params:?}"))?;
let uri = NormalizedUrl::new(params.text_document_position_params.text_document.uri);
let pos = params.text_document_position_params.position;
let result = self.get_definition_location(&uri, pos)?;

View file

@ -23,7 +23,7 @@ use crate::_log;
use crate::channels::WorkerMessage;
use crate::diff::{ASTDiff, HIRDiff};
use crate::server::{
send, send_log, AnalysisResult, DefaultFeatures, ELSResult, Server, ASK_AUTO_SAVE_ID,
AnalysisResult, DefaultFeatures, ELSResult, RedirectableStdout, Server, ASK_AUTO_SAVE_ID,
HEALTH_CHECKER_ID,
};
use crate::util::{self, NormalizedUrl};
@ -39,7 +39,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
uri: NormalizedUrl,
code: S,
) -> ELSResult<()> {
send_log(format!("checking {uri}"))?;
self.send_log(format!("checking {uri}"))?;
let path = util::uri_to_path(&uri);
let mode = if path.to_string_lossy().ends_with(".d.er") {
"declare"
@ -48,14 +48,14 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
};
if let Some((old, new)) = self.analysis_result.get_ast(&uri).zip(self.get_ast(&uri)) {
if ASTDiff::diff(old, &new).is_nop() {
crate::_log!("no changes: {uri}");
crate::_log!(self, "no changes: {uri}");
return Ok(());
}
}
let mut checker = self.get_checker(path.clone());
let artifact = match checker.build(code.into(), mode) {
Ok(artifact) => {
send_log(format!(
self.send_log(format!(
"checking {uri} passed, found warns: {}",
artifact.warns.len()
))?;
@ -63,14 +63,14 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
// clear previous diagnostics
self.send_diagnostics(uri.clone().raw(), vec![])?;
for (uri, diags) in uri_and_diags.into_iter() {
send_log(format!("{uri}, warns: {}", diags.len()))?;
self.send_log(format!("{uri}, warns: {}", diags.len()))?;
self.send_diagnostics(uri, diags)?;
}
artifact.into()
}
Err(artifact) => {
send_log(format!("found errors: {}", artifact.errors.len()))?;
send_log(format!("found warns: {}", artifact.warns.len()))?;
self.send_log(format!("found errors: {}", artifact.errors.len()))?;
self.send_log(format!("found warns: {}", artifact.warns.len()))?;
let diags = artifact
.errors
.clone()
@ -82,7 +82,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
self.send_diagnostics(uri.clone().raw(), vec![])?;
}
for (uri, diags) in uri_and_diags.into_iter() {
send_log(format!("{uri}, errs & warns: {}", diags.len()))?;
self.send_log(format!("{uri}, errs & warns: {}", diags.len()))?;
self.send_diagnostics(uri, diags)?;
}
artifact
@ -108,12 +108,12 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
.insert(uri.clone(), AnalysisResult::new(module, artifact));
}
if let Some(module) = checker.pop_context() {
send_log(format!("{uri}: {}", module.context.name))?;
self.send_log(format!("{uri}: {}", module.context.name))?;
self.modules.insert(uri.clone(), module);
}
let dependents = self.dependents_of(&uri);
for dep in dependents {
// _log!("dep: {dep}");
// _log!(self, "dep: {dep}");
let code = self.file_cache.get_entire_code(&dep)?.to_string();
self.check_file(dep, code)?;
}
@ -122,19 +122,19 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
pub(crate) fn quick_check_file(&mut self, uri: NormalizedUrl) -> ELSResult<()> {
let Some(old) = self.analysis_result.get_ast(&uri) else {
crate::_log!("not found");
crate::_log!(self, "not found");
return Ok(());
};
let Some(new) = self.get_ast(&uri) else {
crate::_log!("not found");
crate::_log!(self, "not found");
return Ok(());
};
let ast_diff = ASTDiff::diff(old, &new);
crate::_log!("diff: {ast_diff}");
crate::_log!(self, "diff: {ast_diff}");
if let Some(mut lowerer) = self.steal_lowerer(&uri) {
let hir = self.analysis_result.get_mut_hir(&uri);
if let Some((hir_diff, hir)) = HIRDiff::new(ast_diff, &mut lowerer).zip(hir) {
crate::_log!("hir_diff: {hir_diff}");
crate::_log!(self, "hir_diff: {hir_diff}");
hir_diff.update(hir);
}
self.restore_lowerer(uri, lowerer);
@ -154,7 +154,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
.unwrap_or(err.input.path().to_path_buf()),
);
let Ok(err_uri) = res_uri else {
crate::_log!("failed to get uri: {}", err.input.path().display());
crate::_log!(self, "failed to get uri: {}", err.input.path().display());
continue;
};
let mut message = remove_style(&err.core.main_message);
@ -214,13 +214,13 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
.map(|doc| doc.publish_diagnostics.is_some())
.unwrap_or(false)
{
send(&json!({
self.send_stdout(&json!({
"jsonrpc": "2.0",
"method": "textDocument/publishDiagnostics",
"params": params,
}))?;
} else {
send_log("the client does not support diagnostics")?;
self.send_log("the client does not support diagnostics")?;
}
Ok(())
}
@ -242,7 +242,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
== Some("afterDelay")
})
{
_log!("Auto saving is enabled");
_log!(_self, "Auto saving is enabled");
break;
}
for uri in _self.file_cache.entries() {
@ -272,20 +272,25 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
pub fn start_client_health_checker(&self, receiver: Receiver<WorkerMessage<()>>) {
const INTERVAL: Duration = Duration::from_secs(5);
const TIMEOUT: Duration = Duration::from_secs(10);
if self.stdout_redirect.is_some() {
return;
}
let _self = self.clone();
// let mut self_ = self.clone();
// FIXME: close this thread when the server is restarted
spawn_new_thread(
move || {
loop {
// send_log("checking client health").unwrap();
// self.send_log("checking client health").unwrap();
let params = ConfigurationParams { items: vec![] };
send(&json!({
"jsonrpc": "2.0",
"id": HEALTH_CHECKER_ID,
"method": "workspace/configuration",
"params": params,
}))
.unwrap();
_self
.send_stdout(&json!({
"jsonrpc": "2.0",
"id": HEALTH_CHECKER_ID,
"method": "workspace/configuration",
"params": params,
}))
.unwrap();
sleep(INTERVAL);
}
},
@ -299,12 +304,12 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
break;
}
Ok(_) => {
// send_log("client health check passed").unwrap();
// self.send_log("client health check passed").unwrap();
}
Err(_) => {
lsp_log!("Client health check timed out");
// lsp_log!("Restart the server");
// _log!("Restart the server");
// lsp_log!(self, "Restart the server");
// _log!(self, "Restart the server");
// send_error_info("Something went wrong, ELS has been restarted").unwrap();
// self_.restart();
panic!("Client health check timed out");

View file

@ -95,8 +95,8 @@ impl HIRDiff {
ASTDiff::Addition(idx, expr) => {
let expr = lowerer
.lower_chunk(expr, None)
.map_err(|err| {
crate::_log!("err: {err}");
.map_err(|_err| {
// crate::_log!(self, "err: {err}");
})
.ok()?;
Some(Self::Addition(idx, expr))
@ -112,8 +112,8 @@ impl HIRDiff {
}
let expr = lowerer
.lower_chunk(expr, None)
.map_err(|err| {
crate::_log!("err: {err}");
.map_err(|_err| {
// crate::_log!(self, "err: {err}");
})
.ok()?;
Some(Self::Modification(idx, expr))

View file

@ -1,5 +1,6 @@
use std::fs::File;
use std::io::Read;
use std::sync::mpsc::Sender;
use lsp_types::{
DidChangeTextDocumentParams, FileOperationFilter, FileOperationPattern,
@ -8,6 +9,7 @@ use lsp_types::{
TextDocumentSyncKind, TextDocumentSyncOptions, Url, WorkspaceFileOperationsServerCapabilities,
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
};
use serde_json::Value;
use erg_common::dict::Dict;
use erg_common::shared::Shared;
@ -16,7 +18,7 @@ use erg_compiler::erg_parser::lex::Lexer;
use erg_compiler::erg_parser::token::{Token, TokenCategory, TokenStream};
use crate::_log;
use crate::server::ELSResult;
use crate::server::{ELSResult, RedirectableStdout};
use crate::util::{self, NormalizedUrl};
fn _get_code_from_uri(uri: &Url) -> ELSResult<String> {
@ -48,12 +50,20 @@ impl FileCacheEntry {
/// This struct can save changes in real-time & incrementally.
#[derive(Debug, Clone, Default)]
pub struct FileCache {
stdout_redirect: Option<Sender<Value>>,
pub files: Shared<Dict<NormalizedUrl, FileCacheEntry>>,
}
impl RedirectableStdout for FileCache {
fn sender(&self) -> Option<&Sender<Value>> {
self.stdout_redirect.as_ref()
}
}
impl FileCache {
pub fn new() -> Self {
pub fn new(stdout_redirect: Option<Sender<Value>>) -> Self {
Self {
stdout_redirect,
files: Shared::new(Dict::new()),
}
}
@ -230,7 +240,7 @@ impl FileCache {
let entry = ent.get(uri);
if let Some(entry) = entry {
if ver.map_or(false, |ver| ver <= entry.ver) {
// crate::_log!("171: double update detected: {ver:?}, {}, code:\n{}", entry.ver, entry.code);
// crate::_log!(self, "171: double update detected: {ver:?}, {}, code:\n{}", entry.ver, entry.code);
return;
}
}
@ -275,7 +285,13 @@ impl FileCache {
return;
};
if entry.ver >= params.text_document.version {
// crate::_log!("212: double update detected {}, {}, code:\n{}", entry.ver, params.text_document.version, entry.code);
crate::_log!(
self,
"212: double update detected {}, {}, code:\n{}",
entry.ver,
params.text_document.version,
entry.code
);
return;
}
let mut code = entry.code.clone();
@ -301,15 +317,15 @@ impl FileCache {
pub fn rename_files(&mut self, params: &RenameFilesParams) -> ELSResult<()> {
for file in &params.files {
let Ok(old_uri) = NormalizedUrl::parse(&file.old_uri) else {
_log!("failed to parse old uri: {}", file.old_uri);
_log!(self, "failed to parse old uri: {}", file.old_uri);
continue;
};
let Ok(new_uri) = NormalizedUrl::parse(&file.new_uri) else {
_log!("failed to parse new uri: {}", file.new_uri);
_log!(self, "failed to parse new uri: {}", file.new_uri);
continue;
};
let Some(entry) = self.files.borrow_mut().remove(&old_uri) else {
_log!("failed to find old uri: {}", file.old_uri);
_log!(self, "failed to find old uri: {}", file.old_uri);
continue;
};
self.files.borrow_mut().insert(new_uri, entry);

View file

@ -7,7 +7,7 @@ use erg_compiler::erg_parser::parse::Parsable;
use lsp_types::{FoldingRange, FoldingRangeKind, FoldingRangeParams};
use crate::_log;
use crate::server::{ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::NormalizedUrl;
fn imports_range(start: &Location, end: &Location) -> Option<FoldingRange> {
@ -25,7 +25,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: FoldingRangeParams,
) -> ELSResult<Option<Vec<FoldingRange>>> {
_log!("folding range requested: {params:?}");
_log!(self, "folding range requested: {params:?}");
let uri = NormalizedUrl::new(params.text_document.uri);
let mut res = vec![];
res.extend(self.fold_imports(&uri));

View file

@ -8,7 +8,7 @@ use erg_compiler::varinfo::{AbsLocation, VarInfo};
use lsp_types::{Hover, HoverContents, HoverParams, MarkedString, Url};
use crate::server::{send_log, ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{self, NormalizedUrl};
const PROG_LANG: &str = if PYTHON_MODE { "python" } else { "erg" };
@ -80,7 +80,7 @@ macro_rules! next {
impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
pub(crate) fn handle_hover(&mut self, params: HoverParams) -> ELSResult<Option<Hover>> {
send_log(format!("hover requested : {params:?}"))?;
self.send_log(format!("hover requested : {params:?}"))?;
let uri = NormalizedUrl::new(params.text_document_position_params.text_document.uri);
let pos = params.text_document_position_params.position;
let mut contents = vec![];
@ -176,7 +176,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
}
}
} else {
send_log("lex error")?;
self.send_log("lex error")?;
}
Ok(Some(Hover {
contents: HoverContents::Array(sort_hovers(contents)),

View file

@ -3,7 +3,7 @@ use erg_compiler::erg_parser::parse::Parsable;
use lsp_types::request::{GotoImplementationParams, GotoImplementationResponse};
use crate::_log;
use crate::server::{ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{loc_to_pos, NormalizedUrl};
impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
@ -11,7 +11,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: GotoImplementationParams,
) -> ELSResult<Option<GotoImplementationResponse>> {
_log!("implementation requested: {params:?}");
_log!(self, "implementation requested: {params:?}");
let uri = NormalizedUrl::new(params.text_document_position_params.text_document.uri);
let pos = params.text_document_position_params.position;
let Some(symbol) = self.file_cache.get_symbol(&uri, pos) else {

View file

@ -19,7 +19,7 @@ use lsp_types::{
};
use crate::_log;
use crate::server::{send, send_log, ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::abs_loc_to_lsp_loc;
use crate::util::{self, loc_to_range, NormalizedUrl};
@ -294,7 +294,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: InlayHintParams,
) -> ELSResult<Option<Vec<InlayHint>>> {
send_log(format!("inlay hint request: {params:?}"))?;
self.send_log(format!("inlay hint request: {params:?}"))?;
let uri = NormalizedUrl::new(params.text_document.uri);
let mut result = vec![];
let gen = InlayHintGenerator {
@ -316,7 +316,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
mut hint: InlayHint,
) -> ELSResult<InlayHint> {
send_log(format!("inlay hint resolve request: {hint:?}"))?;
self.send_log(format!("inlay hint resolve request: {hint:?}"))?;
if let Some(data) = &hint.data {
let Ok(uri) = data.as_str().unwrap().parse::<NormalizedUrl>() else {
return Ok(hint);

View file

@ -26,6 +26,6 @@ use erg_common::config::ErgConfig;
fn main() {
let cfg = ErgConfig::default();
let mut server = server::ErgLanguageServer::new(cfg);
let mut server = server::ErgLanguageServer::new(cfg, None);
server.run().unwrap();
}

View file

@ -1,82 +1,6 @@
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::{Number, Value};
use serde::Serialize;
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorMessage {
jsonrpc: String,
id: Option<Number>,
error: Value,
}
impl ErrorMessage {
#[allow(dead_code)]
pub fn new<N: Into<Number>>(id: Option<N>, error: Value) -> Self {
Self {
jsonrpc: "2.0".into(),
id: id.map(|i| i.into()),
error,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LogMessage {
jsonrpc: String,
method: String,
params: Value,
}
impl LogMessage {
pub fn new<S: Into<String>>(message: S) -> Self {
Self {
jsonrpc: "2.0".into(),
method: "window/logMessage".into(),
params: json! {
{
"type": 3,
"message": message.into(),
}
},
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ShowMessage {
jsonrpc: String,
method: String,
params: Value,
}
impl ShowMessage {
#[allow(unused)]
pub fn info<S: Into<String>>(message: S) -> Self {
Self {
jsonrpc: "2.0".into(),
method: "window/showMessage".into(),
params: json! {
{
"type": 3,
"message": message.into(),
}
},
}
}
pub fn error<S: Into<String>>(message: S) -> Self {
Self {
jsonrpc: "2.0".into(),
method: "window/showMessage".into(),
params: json! {
{
"type": 1,
"message": message.into(),
}
},
}
}
}
pub use molc::messages::ErrorMessage;
#[derive(Serialize)]
pub struct LSPResult<R: Serialize> {

View file

@ -5,7 +5,7 @@ use erg_compiler::varinfo::AbsLocation;
use lsp_types::{Location, Position, ReferenceParams, Url};
use crate::_log;
use crate::server::{ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{self, NormalizedUrl};
impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
@ -13,7 +13,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: ReferenceParams,
) -> ELSResult<Option<Vec<Location>>> {
_log!("references: {params:?}");
_log!(self, "references: {params:?}");
let uri = NormalizedUrl::new(params.text_document_position.text_document.uri);
let pos = params.text_document_position.position;
let result = self.show_refs_inner(&uri, pos);

View file

@ -22,17 +22,17 @@ use lsp_types::{
WorkspaceEdit,
};
use crate::server::{send, send_error_info, send_log, ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{self, NormalizedUrl};
impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
pub(crate) fn rename(&mut self, msg: &Value) -> ELSResult<()> {
let params = RenameParams::deserialize(&msg["params"])?;
send_log(format!("rename request: {params:?}"))?;
self.send_log(format!("rename request: {params:?}"))?;
let uri = NormalizedUrl::new(params.text_document_position.text_document.uri);
let pos = params.text_document_position.position;
if let Some(tok) = self.file_cache.get_symbol(&uri, pos) {
// send_log(format!("token: {tok}"))?;
// self.send_log(format!("token: {tok}"))?;
if let Some(vi) = self
.get_visitor(&uri)
.and_then(|visitor| visitor.get_info(&tok))
@ -65,14 +65,14 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
_ => format!("this {kind} cannot be renamed"),
};
let edit = WorkspaceEdit::new(changes);
send(
self.send_stdout(
&json!({ "jsonrpc": "2.0", "id": msg["id"].as_i64().unwrap(), "result": edit }),
)?;
return send_error_info(error_reason);
return self.send_error_info(error_reason);
}
Self::commit_change(&mut changes, &vi.def_loc, params.new_name.clone());
if let Some(value) = self.get_index().and_then(|ind| ind.get_refs(&vi.def_loc)) {
// send_log(format!("referrers: {referrers:?}"))?;
// self.send_log(format!("referrers: {referrers:?}"))?;
for referrer in value.referrers.iter() {
Self::commit_change(&mut changes, referrer, params.new_name.clone());
}
@ -83,11 +83,11 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
}
let timestamps = self.get_timestamps(changes.keys());
let edit = WorkspaceEdit::new(changes);
send(
self.send_stdout(
&json!({ "jsonrpc": "2.0", "id": msg["id"].as_i64().unwrap(), "result": edit }),
)?;
for _ in 0..20 {
send_log("waiting for file to be modified...")?;
self.send_log("waiting for file to be modified...")?;
if self.all_changed(&timestamps) {
break;
}
@ -102,7 +102,9 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
return Ok(());
}
}
send(&json!({ "jsonrpc": "2.0", "id": msg["id"].as_i64().unwrap(), "result": Value::Null }))
self.send_stdout(
&json!({ "jsonrpc": "2.0", "id": msg["id"].as_i64().unwrap(), "result": Value::Null }),
)
}
fn commit_change(
@ -292,7 +294,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: RenameFilesParams,
) -> ELSResult<Option<WorkspaceEdit>> {
send_log("workspace/willRenameFiles request")?;
self.send_log("workspace/willRenameFiles request")?;
let mut edits = HashMap::new();
let mut renames = vec![];
for file in &params.files {

View file

@ -15,7 +15,7 @@ use lsp_types::{
SemanticToken, SemanticTokenType, SemanticTokens, SemanticTokensParams, SemanticTokensResult,
};
use crate::server::{send_log, ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{self, NormalizedUrl};
#[derive(Debug)]
@ -288,7 +288,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: SemanticTokensParams,
) -> ELSResult<Option<SemanticTokensResult>> {
send_log(format!("full semantic tokens request: {params:?}"))?;
self.send_log(format!("full semantic tokens request: {params:?}"))?;
let uri = NormalizedUrl::new(params.text_document.uri);
let path = util::uri_to_path(&uri);
let src = self.file_cache.get_entire_code(&uri)?;

View file

@ -1,6 +1,6 @@
use std::any::type_name;
use std::io;
use std::io::{stdin, stdout, BufRead, Read, Write};
use std::io::{stdin, BufRead, Read};
use std::ops::Not;
use std::path::{Path, PathBuf};
use std::str::FromStr;
@ -26,6 +26,9 @@ use erg_compiler::lower::ASTLowerer;
use erg_compiler::module::{SharedCompilerResource, SharedModuleGraph, SharedModuleIndex};
use erg_compiler::ty::HasType;
pub use molc::RedirectableStdout;
use molc::{DummyClient, LangServer};
use lsp_types::request::{
CallHierarchyIncomingCalls, CallHierarchyOutgoingCalls, CallHierarchyPrepare,
CodeActionRequest, CodeActionResolveRequest, CodeLensRequest, Completion,
@ -53,7 +56,7 @@ use crate::channels::{SendChannels, Sendable, WorkerMessage};
use crate::completion::CompletionCache;
use crate::file_cache::FileCache;
use crate::hir_visitor::{ExprKind, HIRVisitor};
use crate::message::{ErrorMessage, LSPResult, LogMessage, ShowMessage};
use crate::message::{ErrorMessage, LSPResult};
use crate::util::{self, loc_to_pos, NormalizedUrl};
pub const HEALTH_CHECKER_ID: i64 = 10000;
@ -131,64 +134,12 @@ impl From<&str> for OptionalFeatures {
#[macro_export]
macro_rules! _log {
($($arg:tt)*) => {
($self:ident, $($arg:tt)*) => {
let s = format!($($arg)*);
$crate::server::send_log(format!("{}@{}: {s}", file!(), line!())).unwrap();
$self.send_log(format!("{}@{}: {s}", file!(), line!())).unwrap();
};
}
fn send_stdout<T: ?Sized + Serialize>(message: &T) -> ELSResult<()> {
let msg = serde_json::to_string(message)?;
let mut stdout = stdout().lock();
write!(stdout, "Content-Length: {}\r\n\r\n{}", msg.len(), msg)?;
stdout.flush()?;
Ok(())
}
fn read_line() -> io::Result<String> {
let mut line = String::new();
stdin().lock().read_line(&mut line)?;
Ok(line)
}
fn read_exact(len: usize) -> io::Result<Vec<u8>> {
let mut buf = vec![0; len];
stdin().lock().read_exact(&mut buf)?;
Ok(buf)
}
pub(crate) fn send<T: ?Sized + Serialize>(message: &T) -> ELSResult<()> {
send_stdout(message)
}
pub(crate) fn send_log<S: Into<String>>(msg: S) -> ELSResult<()> {
if cfg!(debug_assertions) || cfg!(feature = "debug") {
send(&LogMessage::new(msg))
} else {
Ok(())
}
}
#[allow(unused)]
pub(crate) fn send_info<S: Into<String>>(msg: S) -> ELSResult<()> {
send(&ShowMessage::info(msg))
}
pub(crate) fn send_error_info<S: Into<String>>(msg: S) -> ELSResult<()> {
send(&ShowMessage::error(msg))
}
pub(crate) fn send_error<S: Into<String>>(id: Option<i64>, code: i64, msg: S) -> ELSResult<()> {
send(&ErrorMessage::new(
id,
json!({ "code": code, "message": msg.into() }),
))
}
pub(crate) fn send_invalid_req_error() -> ELSResult<()> {
send_error(None, -32601, "received an invalid request")
}
#[derive(Debug)]
pub struct AnalysisResult {
pub ast: Module,
@ -328,10 +279,31 @@ pub struct Server<Checker: BuildRunnable = HIRBuilder, Parser: Parsable = Simple
pub(crate) modules: ModuleCache,
pub(crate) analysis_result: AnalysisResultCache,
pub(crate) channels: Option<SendChannels>,
pub(crate) stdout_redirect: Option<mpsc::Sender<Value>>,
pub(crate) _parser: std::marker::PhantomData<fn() -> Parser>,
pub(crate) _checker: std::marker::PhantomData<fn() -> Checker>,
}
impl<C: BuildRunnable, P: Parsable> RedirectableStdout for Server<C, P> {
fn sender(&self) -> Option<&mpsc::Sender<Value>> {
self.stdout_redirect.as_ref()
}
}
impl LangServer for Server {
fn dispatch(&mut self, msg: impl Into<Value>) -> Result<(), Box<dyn std::error::Error>> {
self.dispatch(msg.into())
}
}
impl Server {
#[allow(unused)]
pub fn bind_dummy_client() -> DummyClient<Server> {
let (sender, receiver) = std::sync::mpsc::channel();
DummyClient::new(Server::new(ErgConfig::default(), Some(sender)), receiver)
}
}
impl<C: BuildRunnable, P: Parsable> Clone for Server<C, P> {
fn clone(&self) -> Self {
Self {
@ -347,6 +319,7 @@ impl<C: BuildRunnable, P: Parsable> Clone for Server<C, P> {
modules: self.modules.clone(),
analysis_result: self.analysis_result.clone(),
channels: self.channels.clone(),
stdout_redirect: self.stdout_redirect.clone(),
_parser: std::marker::PhantomData,
_checker: std::marker::PhantomData,
}
@ -354,7 +327,7 @@ impl<C: BuildRunnable, P: Parsable> Clone for Server<C, P> {
}
impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
pub fn new(cfg: ErgConfig) -> Self {
pub fn new(cfg: ErgConfig, stdout_redirect: Option<mpsc::Sender<Value>>) -> Self {
Self {
comp_cache: CompletionCache::new(cfg.copy()),
cfg,
@ -364,10 +337,11 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
client_answers: Shared::new(Dict::new()),
disabled_features: vec![],
opt_features: vec![],
file_cache: FileCache::new(),
file_cache: FileCache::new(stdout_redirect.clone()),
modules: ModuleCache::new(),
analysis_result: AnalysisResultCache::new(),
channels: None,
stdout_redirect,
_parser: std::marker::PhantomData,
_checker: std::marker::PhantomData,
}
@ -377,7 +351,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
loop {
let msg = self.read_message()?;
if let Err(err) = self.dispatch(msg) {
send_error_info(format!("err: {err:?}"))?;
self.send_error_info(format!("err: {err:?}"))?;
}
}
// Ok(())
@ -391,12 +365,24 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
}
}
fn read_line(&self) -> io::Result<String> {
let mut line = String::new();
stdin().lock().read_line(&mut line)?;
Ok(line)
}
fn read_exact(&self, len: usize) -> io::Result<Vec<u8>> {
let mut buf = vec![0; len];
stdin().lock().read_exact(&mut buf)?;
Ok(buf)
}
#[allow(clippy::field_reassign_with_default)]
fn init(&mut self, msg: &Value, id: i64) -> ELSResult<()> {
send_log("initializing ELS")?;
self.send_log("initializing ELS")?;
if msg.get("params").is_some() && msg["params"].get("capabilities").is_some() {
self.init_params = InitializeParams::deserialize(&msg["params"])?;
// send_log(format!("set client capabilities: {:?}", self.client_capas))?;
// self.send_log(format!("set client capabilities: {:?}", self.client_capas))?;
}
let mut args = self.cfg.runtime_args.iter();
while let Some(&arg) = args.next() {
@ -413,7 +399,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
let mut result = InitializeResult::default();
result.capabilities = self.init_capabilities();
self.init_services();
send(&json!({
self.send_stdout(&json!({
"jsonrpc": "2.0",
"id": id,
"result": result,
@ -520,7 +506,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
section: Some("files.autoSave".to_string()),
}],
};
send(&json!({
self.send_stdout(&json!({
"jsonrpc": "2.0",
"id": ASK_AUTO_SAVE_ID,
"method": "workspace/configuration",
@ -606,13 +592,13 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
}
fn exit(&self) -> ELSResult<()> {
send_log("exiting ELS")?;
self.send_log("exiting ELS")?;
std::process::exit(0);
}
fn shutdown(&self, id: i64) -> ELSResult<()> {
send_log("shutting down ELS")?;
send(&json!({
self.send_log("shutting down ELS")?;
self.send_stdout(&json!({
"jsonrpc": "2.0",
"id": id,
"result": json!(null),
@ -634,7 +620,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
// Read in the "Content-Length: xx" part.
let mut size: Option<usize> = None;
loop {
let buffer = read_line()?;
let buffer = self.read_line()?;
// End of input.
if buffer.is_empty() {
@ -689,7 +675,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
}
};
let content = read_exact(size)?;
let content = self.read_exact(size)?;
let s = String::from_utf8(content)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
@ -704,7 +690,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
(Some(id), Some(method)) => self.handle_request(&msg, id, method),
(Some(id), None) => self.handle_response(id, &msg),
(None, Some(notification)) => self.handle_notification(&msg, notification),
_ => send_invalid_req_error(),
_ => self.send_invalid_req_error(),
}
}
@ -735,10 +721,10 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
match msg {
WorkerMessage::Request(id, params) => match handler(&mut _self, params) {
Ok(result) => {
let _ = send(&LSPResult::new(id, result));
let _ = _self.send_stdout(&LSPResult::new(id, result));
}
Err(err) => {
let _ = send(&ErrorMessage::new(
let _ = _self.send_stdout(&ErrorMessage::new(
Some(id),
format!("err from {}: {err}", type_name::<R>()).into(),
));
@ -787,7 +773,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
}
CallHierarchyPrepare::METHOD => self.parse_send::<CallHierarchyPrepare>(id, msg),
FoldingRangeRequest::METHOD => self.parse_send::<FoldingRangeRequest>(id, msg),
other => send_error(Some(id), -32600, format!("{other} is not supported")),
other => self.send_error(Some(id), -32600, format!("{other} is not supported")),
}
}
@ -795,13 +781,13 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
match method {
"initialized" => {
self.ask_auto_save()?;
send_log("successfully bound")
self.send_log("successfully bound")
}
"exit" => self.exit(),
"textDocument/didOpen" => {
let params = DidOpenTextDocumentParams::deserialize(msg["params"].clone())?;
let uri = NormalizedUrl::new(params.text_document.uri);
send_log(format!("{method}: {uri}"))?;
self.send_log(format!("{method}: {uri}"))?;
let code = params.text_document.text;
let ver = params.text_document.version;
self.file_cache.update(&uri, code.clone(), Some(ver));
@ -810,7 +796,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
"textDocument/didSave" => {
let uri =
NormalizedUrl::parse(msg["params"]["textDocument"]["uri"].as_str().unwrap())?;
send_log(format!("{method}: {uri}"))?;
self.send_log(format!("{method}: {uri}"))?;
let code = self.file_cache.get_entire_code(&uri)?;
self.clear_cache(&uri);
self.check_file(uri, code)
@ -831,7 +817,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
self.file_cache.incremental_update(params);
Ok(())
}
_ => send_log(format!("received notification: {method}")),
_ => self.send_log(format!("received notification: {method}")),
}
}
@ -845,7 +831,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
.send(WorkerMessage::Request(0, ()))?;
}
_ => {
_log!("msg: {msg}");
_log!(self, "msg: {msg}");
if msg.get("error").is_none() {
self.client_answers.borrow_mut().insert(id, msg.clone());
}
@ -947,7 +933,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
.file_cache
.get_token_relatively(uri, attr_marker_pos, -2);
if let Some(token) = maybe_token {
// send_log(format!("token: {token}"))?;
// self.send_log(format!("token: {token}"))?;
let mut ctxs = vec![];
if let Some(visitor) = self.get_visitor(uri) {
if let Some(expr) =
@ -965,12 +951,12 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
ctxs.extend(singular_ctxs);
}
} else {
_log!("expr not found: {token}");
_log!(self, "expr not found: {token}");
}
}
Ok(ctxs)
} else {
send_log("token not found")?;
self.send_log("token not found")?;
Ok(vec![])
}
}

View file

@ -11,7 +11,7 @@ use lsp_types::{
};
use crate::hir_visitor::GetExprKind;
use crate::server::{send_log, ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{loc_to_pos, pos_to_loc, NormalizedUrl};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -52,7 +52,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: SignatureHelpParams,
) -> ELSResult<Option<SignatureHelp>> {
send_log(format!("signature help requested: {params:?}"))?;
self.send_log(format!("signature help requested: {params:?}"))?;
let uri = NormalizedUrl::new(params.text_document_position_params.text_document.uri);
let pos = params.text_document_position_params.position;
if params.context.as_ref().map(|ctx| &ctx.trigger_kind)
@ -83,7 +83,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
offset: isize,
) -> Option<(Token, Expr)> {
let token = self.file_cache.get_token_relatively(uri, pos, offset)?;
crate::_log!("token: {token}");
crate::_log!(self, "token: {token}");
if let Some(visitor) = self.get_visitor(uri) {
if let Some(expr) = visitor.get_min_expr(loc_to_pos(token.loc())?) {
return Some((token, expr.clone()));
@ -140,7 +140,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
ctx: &SignatureHelpContext,
) -> Option<SignatureHelp> {
if let Some(token) = self.file_cache.get_token(uri, pos) {
crate::_log!("token: {token}");
crate::_log!(self, "token: {token}");
if let Some(call) = self.get_min::<Call>(uri, pos) {
if call.ln_begin() > token.ln_begin() || call.ln_end() < token.ln_end() {
return None;
@ -148,10 +148,10 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
let nth = self.nth(uri, &call, pos) as u32;
return self.make_sig_help(call.obj.as_ref(), nth);
} else {
crate::_log!("failed to get the call");
crate::_log!(self, "failed to get the call");
}
} else {
crate::_log!("failed to get the token");
crate::_log!(self, "failed to get the token");
}
ctx.active_signature_help.clone()
}
@ -160,7 +160,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
if let Some((_token, Expr::Accessor(acc))) = self.get_min_expr(uri, pos, -2) {
return self.make_sig_help(&acc, 0);
} else {
crate::_log!("lex error occurred");
crate::_log!(self, "lex error occurred");
}
None
}
@ -171,7 +171,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
let help = self.make_sig_help(call.obj.as_ref(), nth);
return help;
} else {
crate::_log!("failed to get continuous help");
crate::_log!(self, "failed to get continuous help");
}
None
}

View file

@ -12,7 +12,7 @@ use lsp_types::{
};
use crate::_log;
use crate::server::{ELSResult, Server};
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{abs_loc_to_lsp_loc, loc_to_range, NormalizedUrl};
pub(crate) fn symbol_kind(vi: &VarInfo) -> SymbolKind {
@ -35,7 +35,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: WorkspaceSymbolParams,
) -> ELSResult<Option<Vec<SymbolInformation>>> {
_log!("workspace symbol requested: {params:?}");
_log!(self, "workspace symbol requested: {params:?}");
let mut res = vec![];
for module in self.modules.values() {
for (name, vi) in module.context.local_dir() {
@ -74,7 +74,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
&mut self,
params: DocumentSymbolParams,
) -> ELSResult<Option<DocumentSymbolResponse>> {
_log!("document symbol requested: {params:?}");
_log!(self, "document symbol requested: {params:?}");
let uri = NormalizedUrl::new(params.text_document.uri);
if let Some(result) = self.analysis_result.get(&uri) {
if let Some(hir) = &result.artifact.object {

View file

@ -1,428 +1,38 @@
use std::fs::File;
use std::io::Read;
use std::path::Path;
use lsp_types::{
CompletionContext, CompletionParams, CompletionResponse, CompletionTriggerKind,
ConfigurationParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
DocumentSymbolParams, DocumentSymbolResponse, FoldingRange, FoldingRangeKind,
FoldingRangeParams, GotoDefinitionParams, Hover, HoverContents, HoverParams, Location,
MarkedString, Position, Range, ReferenceContext, ReferenceParams, RenameParams, SignatureHelp,
SignatureHelpContext, SignatureHelpParams, SignatureHelpTriggerKind,
TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem,
TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier, WorkspaceEdit,
CompletionResponse, DocumentSymbolResponse, FoldingRange, FoldingRangeKind,
GotoDefinitionResponse, HoverContents, MarkedString,
};
use serde::de::Deserialize;
use serde_json::{json, Value};
use els::{NormalizedUrl, Server, TRIGGER_CHARS};
use erg_common::config::ErgConfig;
use erg_common::spawn::safe_yield;
const FILE_A: &str = "tests/a.er";
const FILE_B: &str = "tests/b.er";
const FILE_IMPORTS: &str = "tests/imports.er";
fn add_char(line: u32, character: u32, text: &str) -> TextDocumentContentChangeEvent {
TextDocumentContentChangeEvent {
range: Some(Range {
start: Position { line, character },
end: Position { line, character },
}),
range_length: None,
text: text.to_string(),
}
}
fn abs_pos(uri: Url, line: u32, col: u32) -> TextDocumentPositionParams {
TextDocumentPositionParams {
text_document: TextDocumentIdentifier::new(uri),
position: Position {
line,
character: col,
},
}
}
fn single_range(line: u32, from: u32, to: u32) -> Range {
Range {
start: Position {
line,
character: from,
},
end: Position {
line,
character: to,
},
}
}
fn parse_msgs(_input: &str) -> Vec<Value> {
let mut input = _input;
let mut msgs = Vec::new();
loop {
if input.starts_with("Content-Length: ") {
let idx = "Content-Length: ".len();
input = &input[idx..];
} else {
break;
}
let dights = input.find("\r\n").unwrap();
let len = input[..dights].parse::<usize>().unwrap();
let idx = dights + "\r\n\r\n".len();
input = &input[idx..];
let msg = &input
.get(..len)
.unwrap_or_else(|| panic!("len: {len}, input: `{input}` -> _input: `{_input}`"));
input = &input[len..];
msgs.push(serde_json::from_str(msg).unwrap());
}
msgs
}
pub struct DummyClient {
stdout_buffer: gag::BufferRedirect,
ver: i32,
server: Server,
}
impl Default for DummyClient {
fn default() -> Self {
Self::new()
}
}
impl DummyClient {
pub fn new() -> Self {
let stdout_buffer = loop {
// wait until the other thread is finished
match gag::BufferRedirect::stdout() {
Ok(stdout_buffer) => break stdout_buffer,
Err(_) => safe_yield(),
}
};
DummyClient {
stdout_buffer,
ver: 0,
server: Server::new(ErgConfig::default()),
}
}
/// the server periodically outputs health check messages
fn wait_outputs(&mut self, mut size: usize) -> Result<String, Box<dyn std::error::Error>> {
let mut buf = String::new();
loop {
self.stdout_buffer.read_to_string(&mut buf)?;
if buf.is_empty() {
safe_yield();
} else {
size -= 1;
if size == 0 {
break;
}
}
}
Ok(buf)
}
fn wait_for<R>(&mut self) -> Result<R, Box<dyn std::error::Error>>
where
R: Deserialize<'static>,
{
loop {
let mut buf = String::new();
self.stdout_buffer.read_to_string(&mut buf)?;
for msg in parse_msgs(&buf) {
if msg.get("method").is_some_and(|_| msg.get("id").is_some()) {
self.handle_server_request(&msg);
}
if let Some(result) = msg
.get("result")
.cloned()
.and_then(|res| R::deserialize(res).ok())
{
return Ok(result);
}
}
safe_yield();
}
}
fn handle_server_request(&mut self, msg: &Value) {
if let Ok(_params) = ConfigurationParams::deserialize(&msg["params"]) {
let msg = json!({
"jsonrpc": "2.0",
"id": msg["id"].as_i64().unwrap(),
"result": null,
});
self.server.dispatch(msg).unwrap();
}
}
fn request_initialize(&mut self) -> Result<String, Box<dyn std::error::Error>> {
let msg = json!({
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
});
self.server.dispatch(msg)?;
let buf = self.wait_outputs(2)?;
// eprintln!("`{}`", buf);
Ok(buf)
}
fn notify_open(&mut self, file: &str) -> Result<String, Box<dyn std::error::Error>> {
let uri = Url::from_file_path(Path::new(file).canonicalize().unwrap()).unwrap();
let mut text = String::new();
File::open(file).unwrap().read_to_string(&mut text)?;
let params = DidOpenTextDocumentParams {
text_document: TextDocumentItem::new(uri, "erg".to_string(), self.ver, text),
};
self.ver += 1;
let msg = json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": params,
});
self.server.dispatch(msg)?;
let buf = self.wait_outputs(1)?;
// eprintln!("open: `{}`", buf);
Ok(buf)
}
fn notify_change(
&mut self,
uri: Url,
change: TextDocumentContentChangeEvent,
) -> Result<String, Box<dyn std::error::Error>> {
let params = DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier::new(uri.clone(), self.ver),
content_changes: vec![change],
};
self.ver += 1;
let msg = json!({
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": params,
});
self.server.dispatch(msg)?;
let buf = self.wait_outputs(1)?;
// eprintln!("{}: `{}`", line!(), buf);
Ok(buf)
}
fn request_completion(
&mut self,
uri: Url,
line: u32,
col: u32,
character: &str,
) -> Result<CompletionResponse, Box<dyn std::error::Error>> {
let text_document_position = abs_pos(uri, line, col);
let trigger_kind = if TRIGGER_CHARS.contains(&character) {
CompletionTriggerKind::TRIGGER_CHARACTER
} else {
CompletionTriggerKind::INVOKED
};
let trigger_character = TRIGGER_CHARS
.contains(&character)
.then_some(character.to_string());
let context = Some(CompletionContext {
trigger_kind,
trigger_character,
});
let params = CompletionParams {
text_document_position,
context,
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/completion",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<CompletionResponse>()
}
fn request_rename(
&mut self,
uri: Url,
line: u32,
col: u32,
new_name: &str,
) -> Result<WorkspaceEdit, Box<dyn std::error::Error>> {
let text_document_position = abs_pos(uri, line, col);
let params = RenameParams {
text_document_position,
new_name: new_name.to_string(),
work_done_progress_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/rename",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<WorkspaceEdit>()
}
fn request_signature_help(
&mut self,
uri: Url,
line: u32,
col: u32,
character: &str,
) -> Result<SignatureHelp, Box<dyn std::error::Error>> {
let text_document_position_params = abs_pos(uri, line, col);
let context = SignatureHelpContext {
trigger_kind: SignatureHelpTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some(character.to_string()),
is_retrigger: false,
active_signature_help: None,
};
let params = SignatureHelpParams {
text_document_position_params,
context: Some(context),
work_done_progress_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/signatureHelp",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<SignatureHelp>()
}
fn request_hover(
&mut self,
uri: Url,
line: u32,
col: u32,
) -> Result<Hover, Box<dyn std::error::Error>> {
let params = HoverParams {
text_document_position_params: abs_pos(uri, line, col),
work_done_progress_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/hover",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Hover>()
}
fn request_references(
&mut self,
uri: Url,
line: u32,
col: u32,
) -> Result<Vec<Location>, Box<dyn std::error::Error>> {
let context = ReferenceContext {
include_declaration: false,
};
let params = ReferenceParams {
text_document_position: abs_pos(uri, line, col),
context,
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/references",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Vec<Location>>()
}
fn request_goto_definition(
&mut self,
uri: Url,
line: u32,
col: u32,
) -> Result<Location, Box<dyn std::error::Error>> {
let params = GotoDefinitionParams {
text_document_position_params: abs_pos(uri, line, col),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/definition",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Location>()
}
fn request_folding_range(
&mut self,
uri: Url,
) -> Result<Option<Vec<FoldingRange>>, Box<dyn std::error::Error>> {
let params = FoldingRangeParams {
text_document: TextDocumentIdentifier::new(uri),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/foldingRange",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Option<Vec<FoldingRange>>>()
}
fn request_document_symbols(
&mut self,
uri: Url,
) -> Result<Option<DocumentSymbolResponse>, Box<dyn std::error::Error>> {
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier::new(uri),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/documentSymbol",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Option<DocumentSymbolResponse>>()
}
}
use els::{NormalizedUrl, Server};
use molc::{add_char, oneline_range};
#[test]
fn test_open() -> Result<(), Box<dyn std::error::Error>> {
let mut client = DummyClient::new();
let mut client = Server::bind_dummy_client();
client.request_initialize()?;
let result = client.notify_open(FILE_A)?;
assert!(result.contains("tests/a.er passed, found warns: 0"));
client.notify_open(FILE_A)?;
client.wait_messages(3)?;
assert!(client.responses.iter().any(|val| val
.to_string()
.contains("tests/a.er passed, found warns: 0")));
Ok(())
}
#[test]
fn test_completion() -> Result<(), Box<dyn std::error::Error>> {
let mut client = DummyClient::new();
let mut client = Server::bind_dummy_client();
client.request_initialize()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
client.notify_change(uri.clone().raw(), add_char(2, 0, "x"))?;
client.notify_change(uri.clone().raw(), add_char(2, 1, "."))?;
let resp = client.request_completion(uri.raw(), 2, 2, ".")?;
if let CompletionResponse::Array(items) = resp {
if let Some(CompletionResponse::Array(items)) = resp {
assert!(items.len() >= 40);
assert!(items.iter().any(|item| item.label == "abs"));
Ok(())
@ -433,13 +43,13 @@ fn test_completion() -> Result<(), Box<dyn std::error::Error>> {
#[test]
fn test_neighbor_completion() -> Result<(), Box<dyn std::error::Error>> {
let mut client = DummyClient::new();
let mut client = Server::bind_dummy_client();
client.request_initialize()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
client.notify_open(FILE_B)?;
let resp = client.request_completion(uri.raw(), 2, 0, "n")?;
if let CompletionResponse::Array(items) = resp {
if let Some(CompletionResponse::Array(items)) = resp {
assert!(items.len() >= 40);
assert!(items
.iter()
@ -452,11 +62,13 @@ fn test_neighbor_completion() -> Result<(), Box<dyn std::error::Error>> {
#[test]
fn test_rename() -> Result<(), Box<dyn std::error::Error>> {
let mut client = DummyClient::new();
let mut client = Server::bind_dummy_client();
client.request_initialize()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
let edit = client.request_rename(uri.clone().raw(), 1, 5, "y")?;
let edit = client
.request_rename(uri.clone().raw(), 1, 5, "y")?
.unwrap();
assert!(edit
.changes
.is_some_and(|changes| changes.values().next().unwrap().len() == 2));
@ -465,13 +77,15 @@ fn test_rename() -> Result<(), Box<dyn std::error::Error>> {
#[test]
fn test_signature_help() -> Result<(), Box<dyn std::error::Error>> {
let mut client = DummyClient::new();
let mut client = Server::bind_dummy_client();
client.request_initialize()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
client.notify_change(uri.clone().raw(), add_char(2, 0, "assert"))?;
client.notify_change(uri.clone().raw(), add_char(2, 6, "("))?;
let help = client.request_signature_help(uri.raw(), 2, 7, "(")?;
let help = client
.request_signature_help(uri.raw(), 2, 7, "(")?
.unwrap();
assert_eq!(help.signatures.len(), 1);
let sig = &help.signatures[0];
assert_eq!(sig.label, "::assert: (test: Bool, msg := Str) -> NoneType");
@ -481,11 +95,11 @@ fn test_signature_help() -> Result<(), Box<dyn std::error::Error>> {
#[test]
fn test_hover() -> Result<(), Box<dyn std::error::Error>> {
let mut client = DummyClient::new();
let mut client = Server::bind_dummy_client();
client.request_initialize()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
let hover = client.request_hover(uri.raw(), 1, 4)?;
let hover = client.request_hover(uri.raw(), 1, 4)?.unwrap();
let HoverContents::Array(contents) = hover.contents else {
todo!()
};
@ -506,30 +120,34 @@ fn test_hover() -> Result<(), Box<dyn std::error::Error>> {
#[test]
fn test_references() -> Result<(), Box<dyn std::error::Error>> {
let mut client = DummyClient::new();
let mut client = Server::bind_dummy_client();
client.request_initialize()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
let locations = client.request_references(uri.raw(), 1, 4)?;
let locations = client.request_references(uri.raw(), 1, 4)?.unwrap();
assert_eq!(locations.len(), 1);
assert_eq!(&locations[0].range, &single_range(1, 4, 5));
assert_eq!(&locations[0].range, &oneline_range(1, 4, 5));
Ok(())
}
#[test]
fn test_goto_definition() -> Result<(), Box<dyn std::error::Error>> {
let mut client = DummyClient::new();
let mut client = Server::bind_dummy_client();
client.request_initialize()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
let location = client.request_goto_definition(uri.raw(), 1, 4)?;
assert_eq!(&location.range, &single_range(0, 0, 1));
let Some(GotoDefinitionResponse::Scalar(location)) =
client.request_goto_definition(uri.raw(), 1, 4)?
else {
todo!()
};
assert_eq!(&location.range, &oneline_range(0, 0, 1));
Ok(())
}
#[test]
fn test_folding_range() -> Result<(), Box<dyn std::error::Error>> {
let mut client = DummyClient::new();
let mut client = Server::bind_dummy_client();
client.request_initialize()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_IMPORTS).canonicalize()?)?;
client.notify_open(FILE_IMPORTS)?;
@ -550,7 +168,7 @@ fn test_folding_range() -> Result<(), Box<dyn std::error::Error>> {
#[test]
fn test_document_symbol() -> Result<(), Box<dyn std::error::Error>> {
let mut client = DummyClient::new();
let mut client = Server::bind_dummy_client();
client.request_initialize()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;

19
crates/molc/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "molc"
version = "0.1.0"
description = "A mock language client for testing language servers"
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
homepage.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85"
lsp-types = { version = "0.93.2", features = ["proposed"] }
[path]
lib = "src/lib.rs"

69
crates/molc/README.md Normal file
View file

@ -0,0 +1,69 @@
# `molc`
`molc` is a mock language client for testing language servers.
## Usage
You can see specific examples of molc use in [ELS](https://github.com/erg-lang/erg/tree/main/crates/els).
```rust
use lsp_types::{Url, Value};
use molc::{DummyClient, LangServer, RedirectableStdout};
use molc::oneline_range;
pub struct Server {
stdout_redirect: Option<std::sync::mpsc::Sender<Value>>,
...
}
impl LangServer for Server {
fn dispatch(&mut self, msg: impl Into<Value>) -> Result<(), Box<dyn std::error::Error>> {
self.dispatch(msg)
}
}
impl RedirectableStdout for Server {
fn sender(&self) -> Option<&std::sync::mpsc::Sender<Value>> {
self.stdout_redirect.as_ref()
}
}
impl Server {
fn bind_dummy_client() -> DummyClient<Self> {
// The server should send responses to this channel at least during testing.
let (sender, receiver) = std::sync::mpsc::channel();
DummyClient::new(
Server::new(Some(sender)),
receiver,
)
}
fn init(&mut self, msg: &Value, id: i64) -> ELSResult<()> {
self.send_log("initializing the language server")?;
let result = InitializeResult {
...
};
self.init_services();
self.send_stdout(&json!({
"jsonrpc": "2.0",
"id": id,
"result": result,
}))
}
...
}
#[test]
fn test_references() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_dummy_client();
client.request_initialize()?;
let uri = Url::from_file_path(Path::new(FILE_A).canonicalize()?).unwrap();
client.notify_open(FILE_A)?;
let locations = client.request_references(uri, 1, 4)?.unwrap();
assert_eq!(locations.len(), 1);
assert_eq!(&locations[0].range, &oneline_range(1, 4, 5));
Ok(())
}
```

466
crates/molc/src/lib.rs Normal file
View file

@ -0,0 +1,466 @@
pub mod messages;
use std::fs::File;
use std::io::{stdout, Read, Write};
use std::path::Path;
use std::{collections::HashMap, sync::mpsc::Sender};
use lsp_types::{
CompletionContext, CompletionParams, CompletionResponse, CompletionTriggerKind,
DidChangeTextDocumentParams, DidOpenTextDocumentParams, DocumentSymbolParams,
DocumentSymbolResponse, FoldingRange, FoldingRangeParams, GotoDefinitionParams,
GotoDefinitionResponse, Hover, HoverParams, InitializeResult, Location, Position, Range,
ReferenceContext, ReferenceParams, RenameParams, ServerCapabilities, SignatureHelp,
SignatureHelpContext, SignatureHelpParams, SignatureHelpTriggerKind,
TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem,
TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier, WorkspaceEdit,
};
use serde::de::Deserialize;
use serde::Serialize;
use serde_json::{json, Value};
use crate::messages::{ErrorMessage, LogMessage, ShowMessage};
fn safe_yield() {
std::thread::yield_now();
std::thread::sleep(std::time::Duration::from_millis(10));
}
pub fn add_char(line: u32, character: u32, text: &str) -> TextDocumentContentChangeEvent {
TextDocumentContentChangeEvent {
range: Some(Range {
start: Position { line, character },
end: Position { line, character },
}),
range_length: None,
text: text.to_string(),
}
}
pub fn abs_pos(uri: Url, line: u32, col: u32) -> TextDocumentPositionParams {
TextDocumentPositionParams {
text_document: TextDocumentIdentifier::new(uri),
position: Position {
line,
character: col,
},
}
}
pub fn oneline_range(line: u32, from: u32, to: u32) -> Range {
Range {
start: Position {
line,
character: from,
},
end: Position {
line,
character: to,
},
}
}
pub fn parse_msgs(_input: &str) -> Vec<Value> {
let mut input = _input;
let mut msgs = Vec::new();
loop {
if input.starts_with("Content-Length: ") {
let idx = "Content-Length: ".len();
input = &input[idx..];
} else {
break;
}
let dights = input.find("\r\n").unwrap();
let len = input[..dights].parse::<usize>().unwrap();
let idx = dights + "\r\n\r\n".len();
input = &input[idx..];
let msg = &input
.get(..len)
.unwrap_or_else(|| panic!("len: {len}, input: `{input}` -> _input: `{_input}`"));
input = &input[len..];
msgs.push(serde_json::from_str(msg).unwrap());
}
msgs
}
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
pub trait RedirectableStdout {
fn sender(&self) -> Option<&Sender<Value>>;
fn send_stdout<T: ?Sized + Serialize>(&self, message: &T) -> Result<()> {
if let Some(sender) = self.sender() {
sender.send(serde_json::to_value(message)?)?;
} else {
let msg = serde_json::to_string(message)?;
let mut stdout = stdout().lock();
write!(stdout, "Content-Length: {}\r\n\r\n{}", msg.len(), msg)?;
stdout.flush()?;
}
Ok(())
}
fn send_log<S: Into<String>>(&self, msg: S) -> Result<()> {
if cfg!(debug_assertions) || cfg!(feature = "debug") {
self.send_stdout(&LogMessage::new(msg))
} else {
Ok(())
}
}
#[allow(unused)]
fn send_info<S: Into<String>>(&self, msg: S) -> Result<()> {
self.send_stdout(&ShowMessage::info(msg))
}
fn send_error_info<S: Into<String>>(&self, msg: S) -> Result<()> {
self.send_stdout(&ShowMessage::error(msg))
}
fn send_error<S: Into<String>>(&self, id: Option<i64>, code: i64, msg: S) -> Result<()> {
self.send_stdout(&ErrorMessage::new(
id,
json!({ "code": code, "message": msg.into() }),
))
}
fn send_invalid_req_error(&self) -> Result<()> {
self.send_error(None, -32601, "received an invalid request")
}
}
pub trait LangServer {
fn dispatch(&mut self, msg: impl Into<Value>) -> Result<()>;
}
pub struct DummyClient<LS: LangServer> {
server: LS,
receiver: std::sync::mpsc::Receiver<Value>,
server_capas: Option<ServerCapabilities>,
pub responses: Vec<Value>,
#[allow(clippy::complexity)]
request_handlers: HashMap<String, Box<dyn Fn(&Value, &mut LS) -> Result<()>>>,
ver: i32,
req_id: i64,
}
impl<LS: LangServer> DummyClient<LS> {
/// The server should send responses to the channel at least during testing.
pub fn new(server: LS, receiver: std::sync::mpsc::Receiver<Value>) -> Self {
DummyClient {
receiver,
responses: Vec::new(),
ver: 0,
req_id: 0,
server_capas: None,
request_handlers: HashMap::new(),
server,
}
}
pub fn add_request_handler(
&mut self,
method_name: impl Into<String>,
handler: impl Fn(&Value, &mut LS) -> Result<()> + 'static,
) {
self.request_handlers
.insert(method_name.into(), Box::new(handler));
}
pub fn remove_request_handler(&mut self, method_name: &str) {
self.request_handlers.remove(method_name);
}
/// Waits for `n` messages to be received.
/// When a request is received, the registered handler will be executed.
pub fn wait_messages(&mut self, n: usize) -> Result<()> {
for _ in 0..n {
if let Ok(msg) = self.receiver.recv() {
if msg.get("method").is_some_and(|_| msg.get("id").is_some()) {
self.handle_server_request(&msg);
}
self.responses.push(msg);
}
}
Ok(())
}
/// Waits for a response to the request, where its `id` is expected to be that of `req_id`,
/// and `req_id` will be incremented if the response is successfully received.
/// When a request is received, the registered handler will be executed.
fn wait_for<R>(&mut self) -> Result<R>
where
R: Deserialize<'static>,
{
loop {
if let Ok(msg) = self.receiver.recv() {
if msg.get("method").is_some_and(|_| msg.get("id").is_some()) {
self.handle_server_request(&msg);
}
self.responses.push(msg);
let msg = self.responses.last().unwrap();
if msg.get("id").is_some_and(|val| val == self.req_id) {
if let Some(result) = msg
.get("result")
.cloned()
.and_then(|res| R::deserialize(res).ok())
{
self.req_id += 1;
return Ok(result);
}
}
}
safe_yield();
}
}
fn handle_server_request(&mut self, msg: &Value) {
if let Some(method) = msg.get("method").and_then(|val| val.as_str()) {
if let Some(handler) = self.request_handlers.get(method) {
if let Err(err) = handler(msg, &mut self.server) {
eprintln!("error: {:?}", err);
}
}
}
}
/// This will set the server capabilities
pub fn request_initialize(&mut self) -> Result<InitializeResult> {
let msg = json!({
"jsonrpc": "2.0",
"id": self.req_id,
"method": "initialize",
});
self.server.dispatch(msg)?;
let res = self.wait_for::<InitializeResult>()?;
self.server_capas = Some(res.capabilities.clone());
Ok(res)
}
pub fn notify_open(&mut self, file: &str) -> Result<()> {
let uri = Url::from_file_path(Path::new(file).canonicalize().unwrap()).unwrap();
let mut text = String::new();
File::open(file).unwrap().read_to_string(&mut text)?;
let params = DidOpenTextDocumentParams {
text_document: TextDocumentItem::new(uri, "erg".to_string(), self.ver, text),
};
self.ver += 1;
let msg = json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": params,
});
self.server.dispatch(msg)?;
Ok(())
}
pub fn notify_change(
&mut self,
uri: Url,
change: TextDocumentContentChangeEvent,
) -> Result<()> {
let params = DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier::new(uri.clone(), self.ver),
content_changes: vec![change],
};
self.ver += 1;
let msg = json!({
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": params,
});
self.server.dispatch(msg)?;
Ok(())
}
fn is_trigger_char(&self, character: &str) -> bool {
self.server_capas.as_ref().is_some_and(|cap| {
cap.completion_provider.as_ref().is_some_and(|comp| {
comp.trigger_characters
.as_ref()
.is_some_and(|chars| chars.iter().any(|c| c == character))
})
})
}
pub fn request_completion(
&mut self,
uri: Url,
line: u32,
col: u32,
character: &str,
) -> Result<Option<CompletionResponse>> {
let text_document_position = abs_pos(uri, line, col);
let trigger_kind = if self.is_trigger_char(character) {
CompletionTriggerKind::TRIGGER_CHARACTER
} else {
CompletionTriggerKind::INVOKED
};
let trigger_character = self
.is_trigger_char(character)
.then_some(character.to_string());
let context = Some(CompletionContext {
trigger_kind,
trigger_character,
});
let params = CompletionParams {
text_document_position,
context,
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": self.req_id,
"method": "textDocument/completion",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Option<CompletionResponse>>()
}
pub fn request_rename(
&mut self,
uri: Url,
line: u32,
col: u32,
new_name: &str,
) -> Result<Option<WorkspaceEdit>> {
let text_document_position = abs_pos(uri, line, col);
let params = RenameParams {
text_document_position,
new_name: new_name.to_string(),
work_done_progress_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": self.req_id,
"method": "textDocument/rename",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Option<WorkspaceEdit>>()
}
pub fn request_signature_help(
&mut self,
uri: Url,
line: u32,
col: u32,
character: &str,
) -> Result<Option<SignatureHelp>> {
let text_document_position_params = abs_pos(uri, line, col);
let context = SignatureHelpContext {
trigger_kind: SignatureHelpTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some(character.to_string()),
is_retrigger: false,
active_signature_help: None,
};
let params = SignatureHelpParams {
text_document_position_params,
context: Some(context),
work_done_progress_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": self.req_id,
"method": "textDocument/signatureHelp",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Option<SignatureHelp>>()
}
pub fn request_hover(&mut self, uri: Url, line: u32, col: u32) -> Result<Option<Hover>> {
let params = HoverParams {
text_document_position_params: abs_pos(uri, line, col),
work_done_progress_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": self.req_id,
"method": "textDocument/hover",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Option<Hover>>()
}
pub fn request_references(
&mut self,
uri: Url,
line: u32,
col: u32,
) -> Result<Option<Vec<Location>>> {
let context = ReferenceContext {
include_declaration: false,
};
let params = ReferenceParams {
text_document_position: abs_pos(uri, line, col),
context,
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": self.req_id,
"method": "textDocument/references",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Option<Vec<Location>>>()
}
pub fn request_goto_definition(
&mut self,
uri: Url,
line: u32,
col: u32,
) -> Result<Option<GotoDefinitionResponse>> {
let params = GotoDefinitionParams {
text_document_position_params: abs_pos(uri, line, col),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": self.req_id,
"method": "textDocument/definition",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Option<GotoDefinitionResponse>>()
}
pub fn request_folding_range(&mut self, uri: Url) -> Result<Option<Vec<FoldingRange>>> {
let params = FoldingRangeParams {
text_document: TextDocumentIdentifier::new(uri),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": self.req_id,
"method": "textDocument/foldingRange",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Option<Vec<FoldingRange>>>()
}
pub fn request_document_symbols(&mut self, uri: Url) -> Result<Option<DocumentSymbolResponse>> {
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier::new(uri),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let msg = json!({
"jsonrpc": "2.0",
"id": self.req_id,
"method": "textDocument/documentSymbol",
"params": params,
});
self.server.dispatch(msg)?;
self.wait_for::<Option<DocumentSymbolResponse>>()
}
}

View file

@ -0,0 +1,79 @@
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::{Number, Value};
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorMessage {
jsonrpc: String,
id: Option<Number>,
error: Value,
}
impl ErrorMessage {
#[allow(dead_code)]
pub fn new<N: Into<Number>>(id: Option<N>, error: Value) -> Self {
Self {
jsonrpc: "2.0".into(),
id: id.map(|i| i.into()),
error,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LogMessage {
jsonrpc: String,
method: String,
params: Value,
}
impl LogMessage {
pub fn new<S: Into<String>>(message: S) -> Self {
Self {
jsonrpc: "2.0".into(),
method: "window/logMessage".into(),
params: json! {
{
"type": 3,
"message": message.into(),
}
},
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ShowMessage {
jsonrpc: String,
method: String,
params: Value,
}
impl ShowMessage {
#[allow(unused)]
pub fn info<S: Into<String>>(message: S) -> Self {
Self {
jsonrpc: "2.0".into(),
method: "window/showMessage".into(),
params: json! {
{
"type": 3,
"message": message.into(),
}
},
}
}
pub fn error<S: Into<String>>(message: S) -> Self {
Self {
jsonrpc: "2.0".into(),
method: "window/showMessage".into(),
params: json! {
{
"type": 1,
"message": message.into(),
}
},
}
}
}