载入中...
搜索中...
未找到
LanguageServer.cpp
浏览该文件的文档.
3
4#include "common/Runtime.h"
6#include "scripts.h"
7
8#include <Poco/Dynamic/Var.h>
9#include <Poco/JSON/Array.h>
10#include <Poco/JSON/Object.h>
11#include <Poco/JSON/Parser.h>
12#include <Poco/JSON/Stringifier.h>
13
14#include <algorithm>
15#include <cctype>
16#include <cstdint>
17#include <filesystem>
18#include <fstream>
19#include <functional>
20#include <iostream>
21#include <iterator>
22#include <regex>
23#include <sstream>
24#include <unordered_map>
25#include <utility>
26
27namespace eve::dev {
28namespace {
29
30std::string stringify(const Poco::Dynamic::Var& value) {
31 std::ostringstream output;
32 Poco::JSON::Stringifier::stringify(value, output);
33 return output.str();
34}
35
36void writeFrame(std::ostream& output, const Poco::JSON::Object::Ptr& message) {
37 const std::string body = stringify(Poco::Dynamic::Var(message));
38 output << "Content-Length: " << body.size() << "\r\n\r\n" << body << std::flush;
39}
40
41bool readFrame(std::istream& input, std::string& body) {
42 std::string line;
43 size_t length = 0;
44 while (std::getline(input, line)) {
45 if (!line.empty() && line.back() == '\r') line.pop_back();
46 if (line.empty()) break;
47 constexpr std::string_view prefix = "Content-Length:";
48 if (line.rfind(prefix, 0) == 0) {
49 try {
50 length = static_cast<size_t>(std::stoull(line.substr(prefix.size())));
51 } catch (const std::exception&) {
52 return false;
53 }
54 }
55 }
56 if (length == 0 || !input) return false;
57 body.resize(length);
58 input.read(body.data(), static_cast<std::streamsize>(length));
59 return static_cast<size_t>(input.gcount()) == length;
60}
61
62Poco::JSON::Object::Ptr rpcResponse(const Poco::Dynamic::Var& id, const Poco::Dynamic::Var& result) {
63 Poco::JSON::Object::Ptr message = new Poco::JSON::Object();
64 message->set("jsonrpc", "2.0");
65 message->set("id", id);
66 message->set("result", result);
67 return message;
68}
69
70Poco::JSON::Object::Ptr rpcError(const Poco::Dynamic::Var& id, int code, std::string messageText) {
71 Poco::JSON::Object::Ptr error = new Poco::JSON::Object();
72 error->set("code", code);
73 error->set("message", std::move(messageText));
74 Poco::JSON::Object::Ptr message = new Poco::JSON::Object();
75 message->set("jsonrpc", "2.0");
76 message->set("id", id);
77 message->set("error", error);
78 return message;
79}
80
81int clampNonNegative(int value) { return value < 0 ? 0 : value; }
82
83Poco::JSON::Object::Ptr rangeObject(const lsp::Range& value) {
84 Poco::JSON::Object::Ptr start = new Poco::JSON::Object();
85 start->set("line", static_cast<int>(value.start.line));
86 start->set("character", static_cast<int>(value.start.character));
87 Poco::JSON::Object::Ptr end = new Poco::JSON::Object();
88 end->set("line", static_cast<int>(value.end.line));
89 end->set("character", static_cast<int>(value.end.character));
90 Poco::JSON::Object::Ptr result = new Poco::JSON::Object();
91 result->set("start", start);
92 result->set("end", end);
93 return result;
94}
95
96Poco::JSON::Object::Ptr locationObject(const lsp::Location& value) {
97 Poco::JSON::Object::Ptr result = new Poco::JSON::Object();
98 result->set("uri", value.uri);
99 result->set("range", rangeObject(value.range));
100 return result;
101}
102
103lsp::Range parseRange(const Poco::JSON::Object::Ptr& value) {
104 lsp::Range range;
105 if (!value) return range;
106 if (auto start = value->getObject("start")) {
107 range.start.line = static_cast<size_t>(clampNonNegative(start->optValue<int>("line", 0)));
108 range.start.character = static_cast<size_t>(clampNonNegative(start->optValue<int>("character", 0)));
109 }
110 if (auto end = value->getObject("end")) {
111 range.end.line = static_cast<size_t>(clampNonNegative(end->optValue<int>("line", 0)));
112 range.end.character = static_cast<size_t>(clampNonNegative(end->optValue<int>("character", 0)));
113 }
114 return range;
115}
116
117lsp::FormatOptions parseFormatOptions(const Poco::JSON::Object::Ptr& value) {
118 lsp::FormatOptions options;
119 if (!value) return options;
120 options.tabSize = static_cast<unsigned>(clampNonNegative(value->optValue<int>("tabSize", 4)));
121 options.insertSpaces = value->optValue<bool>("insertSpaces", true);
122 if (options.tabSize == 0) options.tabSize = 4;
123 return options;
124}
125
126Poco::JSON::Array::Ptr textEditsArray(const std::vector<lsp::TextEdit>& edits) {
127 Poco::JSON::Array::Ptr items = new Poco::JSON::Array();
128 for (const lsp::TextEdit& edit : edits) {
129 Poco::JSON::Object::Ptr item = new Poco::JSON::Object();
130 item->set("range", rangeObject(edit.location.range));
131 item->set("newText", edit.newText);
132 items->add(item);
133 }
134 return items;
135}
136
137Poco::JSON::Object::Ptr semanticTokensLegend() {
138 Poco::JSON::Array::Ptr types = new Poco::JSON::Array();
139 types->add("namespace");
140 types->add("class");
141 types->add("function");
142 types->add("method");
143 types->add("variable");
144 types->add("parameter");
145 types->add("property");
146 types->add("keyword");
147 Poco::JSON::Array::Ptr modifiers = new Poco::JSON::Array();
148 modifiers->add("declaration");
149 modifiers->add("readonly");
150 modifiers->add("defaultLibrary");
151 Poco::JSON::Object::Ptr legend = new Poco::JSON::Object();
152 legend->set("tokenTypes", types);
153 legend->set("tokenModifiers", modifiers);
154 return legend;
155}
156
157Poco::JSON::Array::Ptr semanticTokensData(const std::vector<lsp::SemanticToken>& tokens) {
158 Poco::JSON::Array::Ptr data = new Poco::JSON::Array();
159 uint32_t prevLine = 0;
160 uint32_t prevChar = 0;
161 for (const lsp::SemanticToken& token : tokens) {
162 const auto line = static_cast<uint32_t>(token.start.line);
163 const auto start = static_cast<uint32_t>(token.start.character);
164 const auto dLine = line - prevLine;
165 const auto dStart = dLine == 0 ? start - prevChar : start;
166 data->add(static_cast<int>(dLine));
167 data->add(static_cast<int>(dStart));
168 data->add(static_cast<int>(token.length));
169 data->add(static_cast<int>(token.type));
170 data->add(static_cast<int>(token.modifiers));
171 prevLine = line;
172 prevChar = start;
173 }
174 return data;
175}
176
177int completionKind(std::string_view kind) {
178 if (kind == "function" || kind == "async") return 3;
179 if (kind == "class") return 7;
180 if (kind == "module" || kind == "slot") return 9;
181 if (kind == "keyword") return 14;
182 if (kind == "const" || kind == "export") return 21;
183 return 6;
184}
185
186int symbolKind(std::string_view kind) {
187 if (kind == "function" || kind == "async") return 12;
188 if (kind == "class") return 5;
189 if (kind == "const") return 14;
190 return 13;
191}
192
193std::string percentDecode(std::string path) {
194 for (size_t cursor = 0; cursor + 2 < path.size();) {
195 if (path[cursor] != '%') {
196 ++cursor;
197 continue;
198 }
199 const auto hex = [](char value) -> int {
200 if (value >= '0' && value <= '9') return value - '0';
201 if (value >= 'a' && value <= 'f') return value - 'a' + 10;
202 if (value >= 'A' && value <= 'F') return value - 'A' + 10;
203 return -1;
204 };
205 const int high = hex(path[cursor + 1]);
206 const int low = hex(path[cursor + 2]);
207 if (high < 0 || low < 0) {
208 ++cursor;
209 continue;
210 }
211 path.replace(cursor, 3, 1, static_cast<char>((high << 4) | low));
212 ++cursor;
213 }
214 return path;
215}
216
217std::string pathFromFileUri(std::string_view uri) {
218 std::string path;
219 if (uri.rfind("file:///", 0) == 0)
220 path = std::string(uri.substr(8));
221 else if (uri.rfind("file://", 0) == 0)
222 path = std::string(uri.substr(7));
223 else
224 return {};
225 path = percentDecode(std::move(path));
226#if defined(_WIN32)
227 if (path.size() >= 3 && path[0] == '/' && std::isalpha(static_cast<unsigned char>(path[1])) != 0 && path[2] == ':')
228 path.erase(path.begin());
229#else
230 if (!path.empty() && path.front() != '/') path.insert(path.begin(), '/');
231#endif
232 return path;
233}
234
235struct CursorWord {
236 std::string word;
237 std::string qualifier;
238 bool memberAccess = false;
239};
240
241CursorWord wordAt(std::string_view source, size_t line, size_t character) {
242 size_t offset = 0;
243 for (size_t current = 0; current < line && offset < source.size(); ++current) {
244 const size_t newline = source.find('\n', offset);
245 offset = newline == std::string_view::npos ? source.size() : newline + 1;
246 }
247 offset = std::min(offset + character, source.size());
248 size_t begin = offset;
249 size_t end = offset;
250 while (begin > 0 && (std::isalnum(static_cast<unsigned char>(source[begin - 1])) != 0 || source[begin - 1] == '_'))
251 --begin;
252 while (end < source.size() && (std::isalnum(static_cast<unsigned char>(source[end])) != 0 || source[end] == '_'))
253 ++end;
254 CursorWord result;
255 result.word = std::string(source.substr(begin, end - begin));
256 size_t scan = begin;
257 if (result.word.empty() && offset > 0 && source[offset - 1] == '.') scan = offset;
258 while (scan > 0 && std::isspace(static_cast<unsigned char>(source[scan - 1])) != 0) --scan;
259 if (scan > 0 && source[scan - 1] == '.') {
260 result.memberAccess = true;
261 size_t qualEnd = scan - 1;
262 while (qualEnd > 0 && std::isspace(static_cast<unsigned char>(source[qualEnd - 1])) != 0) --qualEnd;
263 size_t qualBegin = qualEnd;
264 while (qualBegin > 0 &&
265 (std::isalnum(static_cast<unsigned char>(source[qualBegin - 1])) != 0 || source[qualBegin - 1] == '_'))
266 --qualBegin;
267 result.qualifier = std::string(source.substr(qualBegin, qualEnd - qualBegin));
268 }
269 return result;
270}
271
272int activeParameterAt(std::string_view source, size_t offset) {
273 int commas = 0;
274 size_t depth = 0;
275 for (size_t i = offset; i > 0; --i) {
276 const char value = source[i - 1];
277 if (value == ')') {
278 ++depth;
279 continue;
280 }
281 if (value == '(') {
282 if (depth == 0) return commas;
283 --depth;
284 continue;
285 }
286 if (value == ',' && depth == 0) ++commas;
287 if (value == '\n' && depth == 0) break;
288 }
289 return commas;
290}
291
292std::string calleeBeforeParen(std::string_view source, size_t offset) {
293 size_t i = offset;
294 while (i > 0) {
295 if (source[i - 1] == ')') {
296 size_t depth = 1;
297 --i;
298 while (i > 0 && depth > 0) {
299 --i;
300 if (source[i] == ')') ++depth;
301 else if (source[i] == '(')
302 --depth;
303 }
304 continue;
305 }
306 if (source[i - 1] == '(') {
307 size_t end = i - 1;
308 while (end > 0 && std::isspace(static_cast<unsigned char>(source[end - 1])) != 0) --end;
309 size_t begin = end;
310 while (begin > 0 &&
311 (std::isalnum(static_cast<unsigned char>(source[begin - 1])) != 0 || source[begin - 1] == '_'))
312 --begin;
313 return std::string(source.substr(begin, end - begin));
314 }
315 --i;
316 }
317 return {};
318}
319
320bool identifierStart(char value) { return std::isalpha(static_cast<unsigned char>(value)) != 0 || value == '_'; }
321bool identifierPart(char value) { return std::isalnum(static_cast<unsigned char>(value)) != 0 || value == '_'; }
322
323std::string trimTypeName(std::string_view value) {
324 size_t begin = 0;
325 while (begin < value.size() && std::isspace(static_cast<unsigned char>(value[begin])) != 0) ++begin;
326 size_t end = value.size();
327 while (end > begin && std::isspace(static_cast<unsigned char>(value[end - 1])) != 0) --end;
328 if (end > begin && value[end - 1] == '?') --end;
329 return std::string(value.substr(begin, end - begin));
330}
331
332bool isPrimitiveType(std::string_view type) {
333 return type.empty() || type == "dynamic" || type == "void" || type == "null" || type == "int" || type == "float" ||
334 type == "bool" || type == "string" || type.rfind("Array", 0) == 0 || type.rfind("Table", 0) == 0;
335}
336
337enum class ExprTokKind { Ident, Dot, LParen, RParen };
338
339struct ExprTok {
340 ExprTokKind kind = ExprTokKind::Ident;
341 std::string text;
342};
343
344std::vector<ExprTok> tokenizeExpr(std::string_view source, size_t begin, size_t end) {
345 std::vector<ExprTok> result;
346 for (size_t i = begin; i < end && i < source.size();) {
347 const char value = source[i];
348 const char next = i + 1 < source.size() ? source[i + 1] : 0;
349 if (std::isspace(static_cast<unsigned char>(value)) != 0) {
350 ++i;
351 continue;
352 }
353 if (value == '/' && next == '/') {
354 while (i < end && i < source.size() && source[i] != '\n') ++i;
355 continue;
356 }
357 if (value == '/' && next == '*') {
358 i += 2;
359 while (i + 1 < source.size() && i < end && !(source[i] == '*' && source[i + 1] == '/')) ++i;
360 i = std::min(i + 2, end);
361 continue;
362 }
363 if (value == '"' || value == '\'') {
364 const char quote = value;
365 ++i;
366 while (i < end && i < source.size() && source[i] != quote) {
367 if (source[i] == '\\' && i + 1 < source.size()) i += 2;
368 else
369 ++i;
370 }
371 if (i < end) ++i;
372 continue;
373 }
374 if (identifierStart(value)) {
375 size_t start = i++;
376 while (i < end && i < source.size() && identifierPart(source[i])) ++i;
377 result.push_back({ExprTokKind::Ident, std::string(source.substr(start, i - start))});
378 continue;
379 }
380 if (value == '.') {
381 result.push_back({ExprTokKind::Dot, "."});
382 ++i;
383 continue;
384 }
385 if (value == '(') {
386 result.push_back({ExprTokKind::LParen, "("});
387 ++i;
388 continue;
389 }
390 if (value == ')') {
391 result.push_back({ExprTokKind::RParen, ")"});
392 ++i;
393 continue;
394 }
395 ++i;
396 }
397 return result;
398}
399
400size_t skipCallTokens(const std::vector<ExprTok>& tokens, size_t index) {
401 if (index >= tokens.size() || tokens[index].kind != ExprTokKind::LParen) return index;
402 int depth = 0;
403 while (index < tokens.size()) {
404 if (tokens[index].kind == ExprTokKind::LParen) ++depth;
405 else if (tokens[index].kind == ExprTokKind::RParen) {
406 --depth;
407 ++index;
408 if (depth == 0) return index;
409 continue;
410 }
411 ++index;
412 }
413 return index;
414}
415
416using SlotClassMap = std::unordered_map<std::string, std::string>;
417
418std::string applyRootIdent(std::string_view name, bool called, const SlotClassMap& slots,
419 const script::BindingContractRegistry& bindings,
420 const std::function<std::string(std::string_view)>& localType) {
421 if (name == "eve") return called ? std::string{} : std::string{"eve"};
422 const auto slot = slots.find(std::string(name));
423 if (slot != slots.end()) return slot->second;
424 if (bindings.hasScriptClass(name)) return std::string(name);
425 return localType(name);
426}
427
428std::string applyMember(std::string type, std::string_view name, bool called,
429 const script::BindingContractRegistry& bindings) {
430 if (type == "eve") {
431 return bindings.hasScriptClass(name) ? std::string(name) : std::string{};
432 }
433 if (isPrimitiveType(type)) return {};
434 const script::BindingContract* contract = bindings.findMethod(type, name);
435 if (contract == nullptr) return {};
436 if (!called) return type;
437 const std::string returned = trimTypeName(contract->returnType);
438 if (isPrimitiveType(returned) || !bindings.hasScriptClass(returned)) return {};
439 return returned;
440}
441
442std::string typeOfTokens(const std::vector<ExprTok>& tokens, const SlotClassMap& slots,
443 const script::BindingContractRegistry& bindings,
444 const std::function<std::string(std::string_view)>& localType) {
445 std::string type;
446 for (size_t i = 0; i < tokens.size();) {
447 if (tokens[i].kind == ExprTokKind::Dot) {
448 ++i;
449 continue;
450 }
451 if (tokens[i].kind != ExprTokKind::Ident) {
452 ++i;
453 continue;
454 }
455 const std::string name = tokens[i].text;
456 ++i;
457 const bool called = i < tokens.size() && tokens[i].kind == ExprTokKind::LParen;
458 if (called) i = skipCallTokens(tokens, i);
459 type = type.empty() ? applyRootIdent(name, called, slots, bindings, localType)
460 : applyMember(std::move(type), name, called, bindings);
461 if (type.empty()) return {};
462 }
463 return type;
464}
465
466size_t statementStart(std::string_view source, size_t end) {
467 size_t i = end;
468 while (i > 0) {
469 const char value = source[i - 1];
470 if (value == '\n' || value == ';' || value == '{' || value == '}') break;
471 --i;
472 }
473 return i;
474}
475
476std::string inferAssignedType(std::string_view source, size_t offset, std::string_view name, const SlotClassMap& slots,
477 const script::BindingContractRegistry& bindings) {
478 size_t match = std::string::npos;
479 for (size_t i = 0; i + name.size() <= offset;) {
480 if (!identifierStart(source[i]) || source.compare(i, name.size(), name) != 0) {
481 ++i;
482 continue;
483 }
484 const size_t after = i + name.size();
485 if ((i > 0 && identifierPart(source[i - 1])) || (after < source.size() && identifierPart(source[after]))) {
486 ++i;
487 continue;
488 }
489 size_t cursor = after;
490 while (cursor < offset && std::isspace(static_cast<unsigned char>(source[cursor])) != 0) ++cursor;
491 if (cursor < offset && source[cursor] == ':') {
492 ++cursor;
493 while (cursor < offset && std::isspace(static_cast<unsigned char>(source[cursor])) != 0) ++cursor;
494 size_t typeEnd = cursor;
495 while (typeEnd < offset && (identifierPart(source[typeEnd]) || source[typeEnd] == '?' || source[typeEnd] == '.'))
496 ++typeEnd;
497 const std::string annotated = trimTypeName(source.substr(cursor, typeEnd - cursor));
498 if (bindings.hasScriptClass(annotated)) {
499 match = i;
500 i = typeEnd;
501 continue;
502 }
503 cursor = typeEnd;
504 while (cursor < offset && std::isspace(static_cast<unsigned char>(source[cursor])) != 0) ++cursor;
505 }
506 const bool assign = cursor < offset && (source[cursor] == '=' ||
507 (source[cursor] == '<' && cursor + 1 < offset && source[cursor + 1] == '-'));
508 if (!assign || (source[cursor] == '=' && cursor + 1 < offset && source[cursor + 1] == '=')) {
509 ++i;
510 continue;
511 }
512 match = i;
513 i = cursor + 1;
514 }
515 if (match == std::string::npos) return {};
516 size_t cursor = match + name.size();
517 while (cursor < offset && std::isspace(static_cast<unsigned char>(source[cursor])) != 0) ++cursor;
518 if (cursor < offset && source[cursor] == ':') {
519 ++cursor;
520 while (cursor < offset && std::isspace(static_cast<unsigned char>(source[cursor])) != 0) ++cursor;
521 size_t typeEnd = cursor;
522 while (typeEnd < offset && identifierPart(source[typeEnd])) ++typeEnd;
523 const std::string annotated = trimTypeName(source.substr(cursor, typeEnd - cursor));
524 if (bindings.hasScriptClass(annotated)) return annotated;
525 cursor = typeEnd;
526 while (cursor < offset && std::isspace(static_cast<unsigned char>(source[cursor])) != 0) ++cursor;
527 }
528 if (cursor < offset && source[cursor] == '<' && cursor + 1 < offset && source[cursor + 1] == '-') cursor += 2;
529 else if (cursor < offset && source[cursor] == '=')
530 ++cursor;
531 while (cursor < offset && std::isspace(static_cast<unsigned char>(source[cursor])) != 0) ++cursor;
532 size_t rhsEnd = cursor;
533 int depth = 0;
534 while (rhsEnd < offset) {
535 const char value = source[rhsEnd];
536 if (value == '(' || value == '[' || value == '{') ++depth;
537 else if (value == ')' || value == ']' || value == '}') {
538 if (depth == 0) break;
539 --depth;
540 } else if (depth == 0 && (value == ';' || value == '\n'))
541 break;
542 ++rhsEnd;
543 }
544 const auto noopLocal = [](std::string_view) { return std::string{}; };
545 return typeOfTokens(tokenizeExpr(source, cursor, rhsEnd), slots, bindings, noopLocal);
546}
547
548script::ScriptCompletion completionFromContract(const script::BindingContract& contract) {
549 std::string signature = contract.method + "(";
550 std::string insertion = contract.method + "(";
551 for (size_t i = 0; i < contract.parameters.size(); ++i) {
552 if (i != 0) {
553 signature += ", ";
554 insertion += ", ";
555 }
556 signature += contract.parameters[i].name + ": " + contract.parameters[i].type;
557 insertion += contract.parameters[i].name + ": ";
558 }
559 signature += ") -> " + contract.returnType;
560 insertion += ')';
561 return {contract.method, "function", std::move(signature), std::move(insertion)};
562}
563
564class ProjectDirectoryProvider final : public script::IScriptModuleProvider {
565public:
566 explicit ProjectDirectoryProvider(std::filesystem::path root)
567 : root_(std::filesystem::weakly_canonical(std::move(root))) {}
568
569 script::ScriptModuleStatus resolve(const script::ScriptModuleRequest& request, std::string& canonicalUri,
570 std::string& error) override {
574 }
575
576 script::ScriptModuleStatus load(std::string_view canonicalUri, script::ScriptModuleSource& source,
577 std::string& error) override {
578 constexpr std::string_view prefix = "game:/";
579 if (canonicalUri.rfind(prefix, 0) != 0) return script::ScriptModuleStatus::NotHandled;
580 const auto path = std::filesystem::weakly_canonical(root_ / std::string(canonicalUri.substr(prefix.size())));
581 const auto relative = path.lexically_relative(root_).generic_string();
582 if (relative.empty() || relative.rfind("..", 0) == 0) {
583 error = "module escapes the project root: " + std::string(canonicalUri);
585 }
586 std::ifstream input(path, std::ios::binary);
588 source.canonicalUri = std::string(canonicalUri);
589 source.utf8Source.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
590 source.debugOrigin = path.string();
592 }
593
594private:
595 std::filesystem::path root_;
596};
597
598std::vector<std::string> moduleNames(std::string_view source, const std::regex& pattern) {
599 const std::string owned(source);
600 std::vector<std::string> result;
601 for (std::sregex_iterator it(owned.begin(), owned.end(), pattern), end; it != end; ++it)
602 result.push_back((*it)[1].str());
603 return result;
604}
605
606} // namespace
607
609 explicit Impl(std::string root)
610 : root_(std::filesystem::absolute(std::move(root)).lexically_normal().string()),
611 runtime_(2048, ssq::Libs::ALL) {
612 runtime_.scriptModules().registerProvider(std::make_shared<ProjectDirectoryProvider>(std::filesystem::path(root_)),
613 100);
614 const std::string contract = module_list_content ? module_list_content : "";
615 const size_t split = contract.find("eve_module_contract");
616 static const std::regex slotPattern(R"(\bslot\s*=\s*[\"']([^\"']+)[\"'])");
617 static const std::regex namePattern(R"(\bname\s*=\s*[\"']([^\"']+)[\"'])");
618 static const std::regex slotClassPattern(
619 R"(\{\s*slot\s*=\s*[\"']([^\"']+)[\"']\s*,\s*cls\s*=\s*[\"']([^\"']+)[\"'])");
620 activeModules_ = moduleNames(contract.substr(0, split), slotPattern);
621 knownModules_ = moduleNames(
622 split == std::string::npos ? std::string_view{} : std::string_view(contract).substr(split), namePattern);
623 const std::string owned(contract.substr(0, split));
624 for (std::sregex_iterator it(owned.begin(), owned.end(), slotClassPattern), end; it != end; ++it)
625 slotClass_[(*it)[1].str()] = (*it)[2].str();
626 indexProject();
627 }
628
629 std::string canonicalUri(const std::string& uri) const {
630 if (uri.rfind("game:/", 0) == 0) return uri;
631 const std::string path = pathFromFileUri(uri);
632 if (path.empty()) return uri;
633 std::error_code error;
634 const auto relative = std::filesystem::path(path).lexically_normal().lexically_relative(
635 std::filesystem::path(root_).lexically_normal());
636 if (error || relative.empty() || *relative.begin() == "..") return uri;
637 return "game:/" + relative.generic_string();
638 }
639
640 std::string identity(std::string_view uri) const {
641 const std::string owned(uri);
642 const auto found = canonical_.find(owned);
643 return found == canonical_.end() ? canonicalUri(owned) : found->second;
644 }
645
646 const std::string* sourceFor(std::string_view uri) const {
647 const auto found = documents_.find(std::string(uri));
648 return found == documents_.end() ? nullptr : &found->second;
649 }
650
651 void update(const std::string& uri, std::string source) {
652 if (uri.empty()) return;
653 documents_[uri] = std::move(source);
656 try {
658 } catch (const std::exception&) {
659 }
660 }
661
662 void close(const std::string& uri) {
663 const auto canonical = canonical_.find(uri);
664 if (canonical != canonical_.end()) {
665 const std::filesystem::path path = pathForCanonical(canonical->second);
666 std::ifstream input(path, std::ios::binary);
667 if (input) {
668 std::string source{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
669 index_.update(canonical->second, fileUriFromPath(path.string()), std::move(source));
670 } else {
671 index_.remove(canonical->second);
672 }
673 }
674 documents_.erase(uri);
675 canonical_.erase(uri);
676 }
677
678 std::vector<script::ScriptDiagnostic> diagnostics(std::string_view uri) const {
679 const std::string owned(uri);
680 const std::string ident = identity(owned);
681 auto list = runtime_.scriptCompiler().diagnostics(ident);
682 const auto document = documents_.find(owned);
683 const bool isProjectConfig =
684 ident == "game:/config.nut" || (owned.size() >= 11 && owned.compare(owned.size() - 11, 11, "/config.nut") == 0);
685 if (document != documents_.end() && isProjectConfig) {
686 auto configDiagnostics = script::ScriptCompiler::validateProjectConfig(document->second, ident, knownModules_,
688 list.insert(list.end(), configDiagnostics.begin(), configDiagnostics.end());
689 }
690 return list;
691 }
692
693 std::vector<script::ScriptCompletion> complete(std::string_view uri, lsp::Position position) const {
694 const std::string* source = sourceFor(uri);
695 CursorWord cursor;
696 if (source)
697 cursor = wordAt(*source, position.line, position.character);
698 const std::string ident = identity(uri);
699 std::vector<script::ScriptCompletion> result;
700 if (cursor.memberAccess) {
701 size_t wordBegin = 0;
702 if (source) {
703 size_t offset = 0;
704 for (size_t current = 0; current < position.line && offset < source->size(); ++current) {
705 const size_t newline = source->find('\n', offset);
706 offset = newline == std::string_view::npos ? source->size() : newline + 1;
707 }
708 offset = std::min(offset + position.character, source->size());
709 wordBegin = offset;
710 while (wordBegin > 0 && identifierPart((*source)[wordBegin - 1])) --wordBegin;
711 }
712 size_t dot = wordBegin;
713 if (source) {
714 while (dot > 0 && std::isspace(static_cast<unsigned char>((*source)[dot - 1])) != 0) --dot;
715 if (dot > 0 && (*source)[dot - 1] == '.') --dot;
716 }
717 const auto localType = [&](std::string_view name) -> std::string {
718 if (const auto unit = runtime_.scriptCompiler().metadata(ident)) {
719 for (const script::ScriptSymbolMetadata& symbol : unit->get().symbols) {
720 if (symbol.name != name) continue;
721 const std::string annotated = trimTypeName(symbol.erasedType);
722 if (runtime_.scriptCompiler().bindings().hasScriptClass(annotated)) return annotated;
723 }
724 }
725 return source ? inferAssignedType(*source, dot, name, slotClass_, runtime_.scriptCompiler().bindings())
726 : std::string{};
727 };
728 const std::string receiver =
729 source ? typeOfTokens(tokenizeExpr(*source, statementStart(*source, dot), dot), slotClass_,
730 runtime_.scriptCompiler().bindings(), localType)
731 : std::string{};
732 const std::string expectedClass = !receiver.empty()
733 ? receiver
734 : (slotClass_.count(cursor.qualifier) ? slotClass_.at(cursor.qualifier)
735 : cursor.qualifier);
736 if (expectedClass == "eve") {
737 std::unordered_map<std::string, bool> seen;
738 for (const script::BindingContract& contract : runtime_.scriptCompiler().bindings().snapshot()) {
739 if (contract.scriptClass.empty() || contract.scriptClass.rfind(cursor.word, 0) != 0) continue;
740 if (!seen.emplace(contract.scriptClass, true).second) continue;
741 result.push_back({contract.scriptClass, "class", "eve." + contract.scriptClass, contract.scriptClass});
742 }
743 return result;
744 }
745 for (const script::BindingContract& contract : runtime_.scriptCompiler().bindings().snapshot()) {
746 if (contract.scriptClass != expectedClass) continue;
747 if (contract.method.rfind(cursor.word, 0) != 0) continue;
748 result.push_back(completionFromContract(contract));
749 }
750 return result;
751 }
752
753 result = runtime_.scriptCompiler().completions(ident, cursor.word);
754 static constexpr const char* keywords[] = {"import", "export", "persist", "match", "async", "await",
755 "local", "function", "class", "const", "foreach", "return",
756 "if", "else", "while", "for", "switch", "case",
757 "break", "continue", "try", "catch", "throw", "null",
758 "true", "false", "this", "base", "typeof", "instanceof",
759 "resume", "yield", "clone", "delete"};
760 for (const char* keyword : keywords) {
761 if (std::string_view(keyword).rfind(cursor.word, 0) != 0) continue;
762 result.push_back({keyword, "keyword", "EveScript keyword", keyword});
763 }
764 for (const std::string& slot : activeModules_) {
765 if (slot.rfind(cursor.word, 0) != 0) continue;
766 const auto cls = slotClass_.find(slot);
767 result.push_back({slot, "slot", cls == slotClass_.end() ? "module slot" : cls->second, slot});
768 }
769 std::sort(result.begin(), result.end(), [](const script::ScriptCompletion& a, const script::ScriptCompletion& b) {
770 if (a.label != b.label) return a.label < b.label;
771 return a.kind < b.kind;
772 });
773 result.erase(std::unique(result.begin(), result.end(),
775 return a.label == b.label && a.kind == b.kind;
776 }),
777 result.end());
778 return result;
779 }
780
781 std::optional<script::ScriptHover> hover(std::string_view uri, lsp::Position position) const {
782 const std::string* source = sourceFor(uri);
783 if (!source) return std::nullopt;
784 const CursorWord cursor = wordAt(*source, position.line, position.character);
785 if (cursor.word.empty()) return std::nullopt;
786 const std::string ident = identity(uri);
787 if (auto value = runtime_.scriptCompiler().hover(ident, cursor.word)) return value;
788 const auto slot = slotClass_.find(cursor.word);
789 if (slot != slotClass_.end()) {
791 hover.symbol = cursor.word;
792 hover.markdown = "`" + cursor.word + "` — `" + slot->second + "` module slot";
793 return hover;
794 }
795 if (const auto location = index_.definition(ident, position)) {
797 hover.symbol = cursor.word;
798 hover.markdown = "`" + cursor.word + "` defined in `" + location->uri + "`";
799 return hover;
800 }
801 return std::nullopt;
802 }
803
804 std::optional<LanguageSignatureHelp> signatureHelp(std::string_view uri, lsp::Position position) const {
805 const std::string* source = sourceFor(uri);
806 if (!source) return std::nullopt;
807 size_t offset = 0;
808 for (size_t current = 0; current < position.line && offset < source->size(); ++current) {
809 const size_t newline = source->find('\n', offset);
810 offset = newline == std::string_view::npos ? source->size() : newline + 1;
811 }
812 offset = std::min(offset + position.character, source->size());
813 const std::string callee = calleeBeforeParen(*source, offset);
814 if (callee.empty()) return std::nullopt;
816 if (!contract) return std::nullopt;
818 help.activeParameter = activeParameterAt(*source, offset);
819 help.documentation = contract->documentationId;
820 help.label = contract->scriptClass + "." + contract->method + "(";
821 for (size_t i = 0; i < contract->parameters.size(); ++i) {
822 if (i != 0) help.label += ", ";
823 help.label += contract->parameters[i].name + ": " + contract->parameters[i].type;
824 help.parameters.push_back({contract->parameters[i].name + ": " + contract->parameters[i].type, {}});
825 }
826 help.label += ") -> " + contract->returnType;
827 if (help.activeParameter >= static_cast<int>(help.parameters.size()))
828 help.activeParameter = help.parameters.empty() ? 0 : static_cast<int>(help.parameters.size()) - 1;
829 return help;
830 }
831
832 std::filesystem::path pathForCanonical(std::string_view canonical) const {
833 constexpr std::string_view prefix = "game:/";
834 return canonical.rfind(prefix, 0) == 0
835 ? std::filesystem::path(root_) / std::string(canonical.substr(prefix.size()))
836 : std::filesystem::path{};
837 }
838
840 std::error_code error;
841 for (std::filesystem::recursive_directory_iterator it(root_, error), end; it != end && !error;
842 it.increment(error)) {
843 if (it->is_directory(error)) {
844 const std::string name = it->path().filename().string();
845 if (name == ".git" || name == "build" || name == "third-party") it.disable_recursion_pending();
846 continue;
847 }
848 if (!it->is_regular_file(error) || it->path().extension() != ".nut") continue;
849 std::ifstream input(it->path(), std::ios::binary);
850 if (!input) continue;
851 std::string source{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
852 const std::string uri = fileUriFromPath(it->path().string());
853 const auto relative = std::filesystem::relative(it->path(), std::filesystem::path(root_), error);
854 if (error || relative.empty() || relative.generic_string().rfind("..", 0) == 0) continue;
855 index_.update("game:/" + relative.generic_string(), uri, std::move(source));
856 }
857 }
858
859 std::string root_;
862 std::unordered_map<std::string, std::string> documents_;
863 std::unordered_map<std::string, std::string> canonical_;
864 std::vector<std::string> knownModules_;
865 std::vector<std::string> activeModules_;
866 std::unordered_map<std::string, std::string> slotClass_;
867 bool shutdown_ = false;
868};
869
870LanguageServer::LanguageServer(std::string projectRoot) : impl_(std::make_unique<Impl>(std::move(projectRoot))) {}
872
873const std::string& LanguageServer::projectRoot() const noexcept { return impl_->root_; }
874
875std::string LanguageServer::fileUriFromPath(std::string_view path) {
876 std::string generic = std::filesystem::absolute(std::filesystem::path(path)).lexically_normal().generic_string();
877 std::string encoded;
878 encoded.reserve(generic.size());
879 for (const char value : generic) {
880 if (value == ' ')
881 encoded += "%20";
882 else if (value == '#')
883 encoded += "%23";
884 else if (value == '%')
885 encoded += "%25";
886 else
887 encoded += value;
888 }
889 if (!encoded.empty() && encoded.front() != '/') encoded.insert(encoded.begin(), '/');
890 return "file://" + encoded;
891}
892
893void LanguageServer::openDocument(std::string uri, std::string text) { impl_->update(uri, std::move(text)); }
894void LanguageServer::changeDocument(std::string uri, std::string text) { impl_->update(uri, std::move(text)); }
895void LanguageServer::changeDocument(std::string uri, lsp::Range range, std::string text) {
896 auto found = impl_->documents_.find(uri);
897 if (found == impl_->documents_.end()) {
898 impl_->update(uri, std::move(text));
899 return;
900 }
902 impl_->update(uri, found->second);
903}
904void LanguageServer::closeDocument(std::string uri) { impl_->close(uri); }
905
906std::vector<script::ScriptDiagnostic> LanguageServer::diagnosticsFor(std::string_view uri) const {
907 return impl_->diagnostics(uri);
908}
909
910std::vector<script::ScriptCompletion> LanguageServer::complete(std::string_view uri, lsp::Position position) const {
911 return impl_->complete(uri, position);
912}
913
914std::optional<script::ScriptHover> LanguageServer::hover(std::string_view uri, lsp::Position position) const {
915 return impl_->hover(uri, position);
916}
917
918std::optional<lsp::Location> LanguageServer::definition(std::string_view uri, lsp::Position position) const {
919 return impl_->index_.definition(impl_->identity(uri), position);
920}
921
922std::vector<lsp::Location> LanguageServer::references(std::string_view uri, lsp::Position position,
923 bool includeDeclaration) const {
924 return impl_->index_.references(impl_->identity(uri), position, includeDeclaration);
925}
926
927std::optional<std::vector<lsp::TextEdit>> LanguageServer::rename(std::string_view uri, lsp::Position position,
928 std::string_view newName) const {
929 return impl_->index_.rename(impl_->identity(uri), position, newName);
930}
931
932std::vector<script::ScriptSymbolMetadata> LanguageServer::documentSymbols(std::string_view uri) const {
933 const auto metadata = impl_->runtime_.scriptCompiler().metadata(impl_->identity(uri));
934 return metadata ? metadata->get().symbols : std::vector<script::ScriptSymbolMetadata>{};
935}
936
937std::optional<LanguageSignatureHelp> LanguageServer::signatureHelp(std::string_view uri, lsp::Position position) const {
938 return impl_->signatureHelp(uri, position);
939}
940
941std::vector<lsp::TextEdit> LanguageServer::formatDocument(std::string_view uri, lsp::FormatOptions options,
942 const lsp::Range* range) const {
943 const std::string* source = impl_->sourceFor(uri);
944 if (!source) return {};
945 const std::string formatted = lsp::formatEveScript(*source, options);
946 lsp::TextEdit edit;
947 edit.location.uri = std::string(uri);
948 if (range != nullptr && lsp::lastLineIndex(formatted) == lsp::lastLineIndex(*source)) {
949 size_t startLine = range->start.line;
950 size_t endLine = range->end.line;
951 if (range->end.character == 0 && endLine > startLine) --endLine;
952 const std::string originalSpan = lsp::sliceLines(*source, startLine, endLine);
953 const std::string formattedSpan = lsp::sliceLines(formatted, startLine, endLine);
954 if (formattedSpan == originalSpan) return {};
955 edit.location.range = lsp::coveringLines(*source, startLine, endLine);
956 edit.newText = formattedSpan;
957 return {std::move(edit)};
958 }
959 if (formatted == *source) return {};
961 edit.newText = formatted;
962 return {std::move(edit)};
963}
964
965std::vector<lsp::FoldingRange> LanguageServer::foldingRanges(std::string_view uri) const {
966 const std::string* source = impl_->sourceFor(uri);
967 return source ? lsp::foldingRanges(*source) : std::vector<lsp::FoldingRange>{};
968}
969
970std::vector<lsp::SemanticToken> LanguageServer::semanticTokens(std::string_view uri) const {
971 std::vector<lsp::SemanticToken> tokens = impl_->index_.semanticTokens(impl_->identity(uri));
972 const auto& bindings = impl_->runtime_.scriptCompiler().bindings();
973 const auto metadata = impl_->runtime_.scriptCompiler().metadata(impl_->identity(uri));
974 std::unordered_map<std::string, uint32_t> extra;
975 if (metadata) {
976 for (const script::ScriptSymbolMetadata& symbol : metadata->get().symbols) {
977 if (symbol.kind == "class") extra[symbol.name] = lsp::SemanticTypes::Class;
978 else if (symbol.kind == "function" || symbol.kind == "async")
979 extra[symbol.name] = lsp::SemanticTypes::Function;
980 }
981 }
983 if (token.type == lsp::SemanticTypes::Keyword) continue;
985 if (bindings.hasScriptClass(token.name)) token.type = lsp::SemanticTypes::Class;
986 continue;
987 }
988 if (impl_->slotClass_.count(token.name) != 0) {
991 continue;
992 }
993 if (bindings.hasScriptClass(token.name)) {
995 continue;
996 }
997 const auto found = extra.find(token.name);
998 if (found != extra.end() && token.type == lsp::SemanticTypes::Variable) token.type = found->second;
999 }
1000 return tokens;
1001}
1002
1003LspDispatch LanguageServer::handleMessage(std::string_view jsonBody, std::ostream& output) {
1004 Poco::JSON::Parser parser;
1005 Poco::JSON::Object::Ptr request;
1006 try {
1007 request = parser.parse(std::string(jsonBody)).extract<Poco::JSON::Object::Ptr>();
1008 } catch (const std::exception& error) {
1009 writeFrame(output, rpcError(Poco::Dynamic::Var(), -32700, error.what()));
1010 return LspDispatch::Continue;
1011 }
1012 if (!request) return LspDispatch::Continue;
1013 const std::string method = request->optValue<std::string>("method", "");
1014 const auto params = request->getObject("params");
1015 const bool hasId = request->has("id");
1016 const auto id = hasId ? request->get("id") : Poco::Dynamic::Var();
1017 const auto documentUri = [&]() -> std::string {
1018 if (!params) return {};
1019 auto document = params->getObject("textDocument");
1020 return document ? document->optValue<std::string>("uri", "") : std::string{};
1021 };
1022 const auto requestPosition = [&]() -> lsp::Position {
1023 auto position = params ? params->getObject("position") : nullptr;
1024 return position ? lsp::Position{static_cast<size_t>(clampNonNegative(position->optValue<int>("line", 0))),
1025 static_cast<size_t>(clampNonNegative(position->optValue<int>("character", 0)))}
1026 : lsp::Position{};
1027 };
1028
1029 const auto publishDiagnostics = [&](const std::string& uri) {
1030 Poco::JSON::Array::Ptr items = new Poco::JSON::Array();
1031 for (const script::ScriptDiagnostic& diagnostic : diagnosticsFor(uri)) {
1032 const int line = clampNonNegative(static_cast<int>(diagnostic.position.line) - 1);
1033 const int col = clampNonNegative(static_cast<int>(diagnostic.position.column) - 1);
1034 lsp::Range range{{static_cast<size_t>(line), static_cast<size_t>(col)},
1035 {static_cast<size_t>(line), static_cast<size_t>(col + 1)}};
1036 Poco::JSON::Object::Ptr item = new Poco::JSON::Object();
1037 item->set("range", rangeObject(range));
1038 item->set("severity", diagnostic.severity == script::ScriptDiagnosticSeverity::Error ? 1 : 2);
1039 item->set("code", diagnostic.code);
1040 item->set("source", "evescript");
1041 item->set("message", diagnostic.message);
1042 items->add(item);
1043 }
1044 Poco::JSON::Object::Ptr diagParams = new Poco::JSON::Object();
1045 diagParams->set("uri", uri);
1046 diagParams->set("diagnostics", items);
1047 Poco::JSON::Object::Ptr notification = new Poco::JSON::Object();
1048 notification->set("jsonrpc", "2.0");
1049 notification->set("method", "textDocument/publishDiagnostics");
1050 notification->set("params", diagParams);
1051 writeFrame(output, notification);
1052 };
1053
1054 if (method == "initialize") {
1055 Poco::JSON::Object::Ptr sync = new Poco::JSON::Object();
1056 sync->set("openClose", true);
1057 sync->set("change", 2);
1058 Poco::JSON::Object::Ptr completion = new Poco::JSON::Object();
1059 completion->set("resolveProvider", false);
1060 Poco::JSON::Array::Ptr triggers = new Poco::JSON::Array();
1061 triggers->add(".");
1062 triggers->add(":");
1063 completion->set("triggerCharacters", triggers);
1064 Poco::JSON::Object::Ptr signature = new Poco::JSON::Object();
1065 Poco::JSON::Array::Ptr sigTriggers = new Poco::JSON::Array();
1066 sigTriggers->add("(");
1067 sigTriggers->add(",");
1068 signature->set("triggerCharacters", sigTriggers);
1069 Poco::JSON::Object::Ptr capabilities = new Poco::JSON::Object();
1070 capabilities->set("textDocumentSync", sync);
1071 capabilities->set("completionProvider", completion);
1072 capabilities->set("hoverProvider", true);
1073 capabilities->set("definitionProvider", true);
1074 capabilities->set("referencesProvider", true);
1075 capabilities->set("renameProvider", true);
1076 capabilities->set("documentSymbolProvider", true);
1077 capabilities->set("signatureHelpProvider", signature);
1078 capabilities->set("documentFormattingProvider", true);
1079 capabilities->set("documentRangeFormattingProvider", true);
1080 capabilities->set("foldingRangeProvider", true);
1081 Poco::JSON::Object::Ptr semantic = new Poco::JSON::Object();
1082 semantic->set("legend", semanticTokensLegend());
1083 semantic->set("full", true);
1084 semantic->set("range", false);
1085 capabilities->set("semanticTokensProvider", semantic);
1086 Poco::JSON::Object::Ptr diagnosticProvider = new Poco::JSON::Object();
1087 diagnosticProvider->set("interFileDependencies", true);
1088 diagnosticProvider->set("workspaceDiagnostics", false);
1089 capabilities->set("diagnosticProvider", diagnosticProvider);
1090 Poco::JSON::Object::Ptr result = new Poco::JSON::Object();
1091 result->set("capabilities", capabilities);
1092 Poco::JSON::Object::Ptr serverInfo = new Poco::JSON::Object();
1093 serverInfo->set("name", "evescript");
1094 serverInfo->set("version", "1");
1095 result->set("serverInfo", serverInfo);
1096 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var(result)));
1097 } else if (method == "initialized" || method == "textDocument/didSave" || method.rfind("$/", 0) == 0) {
1098 return LspDispatch::Continue;
1099 } else if (method == "shutdown") {
1100 impl_->shutdown_ = true;
1101 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var()));
1102 } else if (method == "exit") {
1103 return LspDispatch::Exit;
1104 } else if (method == "textDocument/didOpen") {
1105 auto document = params ? params->getObject("textDocument") : nullptr;
1106 if (document) {
1107 const std::string uri = document->optValue<std::string>("uri", "");
1108 openDocument(uri, document->optValue<std::string>("text", ""));
1109 publishDiagnostics(uri);
1110 }
1111 } else if (method == "textDocument/didChange") {
1112 const std::string uri = documentUri();
1113 auto changes = params ? params->getArray("contentChanges") : nullptr;
1114 if (changes) {
1115 for (unsigned int index = 0; index < changes->size(); ++index) {
1116 auto change = changes->getObject(index);
1117 if (!change) continue;
1118 const std::string text = change->optValue<std::string>("text", "");
1119 if (change->has("range"))
1120 changeDocument(uri, parseRange(change->getObject("range")), text);
1121 else
1123 }
1124 if (changes->size() != 0) publishDiagnostics(uri);
1125 }
1126 } else if (method == "textDocument/didClose") {
1127 const std::string uri = documentUri();
1129 Poco::JSON::Object::Ptr diagParams = new Poco::JSON::Object();
1130 diagParams->set("uri", uri);
1131 diagParams->set("diagnostics", Poco::JSON::Array::Ptr(new Poco::JSON::Array()));
1132 Poco::JSON::Object::Ptr notification = new Poco::JSON::Object();
1133 notification->set("jsonrpc", "2.0");
1134 notification->set("method", "textDocument/publishDiagnostics");
1135 notification->set("params", diagParams);
1136 writeFrame(output, notification);
1137 } else if (method == "textDocument/completion") {
1138 Poco::JSON::Array::Ptr items = new Poco::JSON::Array();
1139 for (const script::ScriptCompletion& completion : complete(documentUri(), requestPosition())) {
1140 Poco::JSON::Object::Ptr item = new Poco::JSON::Object();
1141 item->set("label", completion.label);
1142 item->set("detail", completion.detail);
1143 item->set("insertText", completion.insertText);
1144 item->set("kind", completionKind(completion.kind));
1145 items->add(item);
1146 }
1147 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var(items)));
1148 } else if (method == "textDocument/hover") {
1149 const auto value = hover(documentUri(), requestPosition());
1150 if (!value) {
1151 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var()));
1152 } else {
1153 Poco::JSON::Object::Ptr contents = new Poco::JSON::Object();
1154 contents->set("kind", "markdown");
1155 contents->set("value", value->markdown);
1156 Poco::JSON::Object::Ptr result = new Poco::JSON::Object();
1157 result->set("contents", contents);
1158 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var(result)));
1159 }
1160 } else if (method == "textDocument/definition") {
1161 const auto value = definition(documentUri(), requestPosition());
1162 writeFrame(output, rpcResponse(id, value ? Poco::Dynamic::Var(locationObject(*value)) : Poco::Dynamic::Var()));
1163 } else if (method == "textDocument/references") {
1164 bool includeDeclaration = false;
1165 if (auto context = params ? params->getObject("context") : nullptr)
1166 includeDeclaration = context->optValue<bool>("includeDeclaration", false);
1167 Poco::JSON::Array::Ptr items = new Poco::JSON::Array();
1168 for (const lsp::Location& location : references(documentUri(), requestPosition(), includeDeclaration))
1169 items->add(locationObject(location));
1170 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var(items)));
1171 } else if (method == "textDocument/rename") {
1172 const std::string newName = params ? params->optValue<std::string>("newName", "") : std::string{};
1173 const auto edits = rename(documentUri(), requestPosition(), newName);
1174 if (!edits) {
1175 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var()));
1176 } else {
1177 Poco::JSON::Object::Ptr changes = new Poco::JSON::Object();
1178 std::unordered_map<std::string, Poco::JSON::Array::Ptr> byUri;
1179 for (const lsp::TextEdit& edit : *edits) {
1180 auto& list = byUri[edit.location.uri];
1181 if (!list) list = new Poco::JSON::Array();
1182 Poco::JSON::Object::Ptr item = new Poco::JSON::Object();
1183 item->set("range", rangeObject(edit.location.range));
1184 item->set("newText", edit.newText);
1185 list->add(item);
1186 }
1187 for (const auto& [uri, list] : byUri) changes->set(uri, list);
1188 Poco::JSON::Object::Ptr result = new Poco::JSON::Object();
1189 result->set("changes", changes);
1190 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var(result)));
1191 }
1192 } else if (method == "textDocument/documentSymbol") {
1193 Poco::JSON::Array::Ptr items = new Poco::JSON::Array();
1194 for (const script::ScriptSymbolMetadata& symbol : documentSymbols(documentUri())) {
1195 const int line = clampNonNegative(static_cast<int>(symbol.position.line) - 1);
1196 const int col = clampNonNegative(static_cast<int>(symbol.position.column) - 1);
1197 lsp::Range range{{static_cast<size_t>(line), static_cast<size_t>(col)},
1198 {static_cast<size_t>(line), static_cast<size_t>(col + symbol.name.size())}};
1199 Poco::JSON::Object::Ptr item = new Poco::JSON::Object();
1200 item->set("name", symbol.name);
1201 item->set("detail", symbol.erasedType);
1202 item->set("kind", symbolKind(symbol.kind));
1203 item->set("range", rangeObject(range));
1204 item->set("selectionRange", rangeObject(range));
1205 items->add(item);
1206 }
1207 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var(items)));
1208 } else if (method == "textDocument/signatureHelp") {
1209 const auto value = signatureHelp(documentUri(), requestPosition());
1210 if (!value) {
1211 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var()));
1212 } else {
1213 Poco::JSON::Array::Ptr parameters = new Poco::JSON::Array();
1214 for (const LanguageSignatureParameter& parameter : value->parameters) {
1215 Poco::JSON::Object::Ptr item = new Poco::JSON::Object();
1216 item->set("label", parameter.label);
1217 parameters->add(item);
1218 }
1219 Poco::JSON::Object::Ptr signature = new Poco::JSON::Object();
1220 signature->set("label", value->label);
1221 signature->set("documentation", value->documentation);
1222 signature->set("parameters", parameters);
1223 Poco::JSON::Array::Ptr signatures = new Poco::JSON::Array();
1224 signatures->add(signature);
1225 Poco::JSON::Object::Ptr result = new Poco::JSON::Object();
1226 result->set("signatures", signatures);
1227 result->set("activeSignature", 0);
1228 result->set("activeParameter", value->activeParameter);
1229 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var(result)));
1230 }
1231 } else if (method == "textDocument/formatting") {
1232 const auto options = parseFormatOptions(params ? params->getObject("options") : nullptr);
1233 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var(textEditsArray(formatDocument(documentUri(), options)))));
1234 } else if (method == "textDocument/rangeFormatting") {
1235 const auto options = parseFormatOptions(params ? params->getObject("options") : nullptr);
1236 lsp::Range range = parseRange(params ? params->getObject("range") : nullptr);
1237 writeFrame(output,
1238 rpcResponse(id, Poco::Dynamic::Var(textEditsArray(formatDocument(documentUri(), options, &range)))));
1239 } else if (method == "textDocument/foldingRange") {
1240 Poco::JSON::Array::Ptr items = new Poco::JSON::Array();
1241 for (const lsp::FoldingRange& fold : foldingRanges(documentUri())) {
1242 Poco::JSON::Object::Ptr item = new Poco::JSON::Object();
1243 item->set("startLine", static_cast<int>(fold.startLine));
1244 item->set("endLine", static_cast<int>(fold.endLine));
1245 if (!fold.kind.empty()) item->set("kind", fold.kind);
1246 items->add(item);
1247 }
1248 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var(items)));
1249 } else if (method == "textDocument/semanticTokens/full") {
1250 Poco::JSON::Object::Ptr result = new Poco::JSON::Object();
1251 result->set("data", semanticTokensData(semanticTokens(documentUri())));
1252 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var(result)));
1253 } else if (method == "textDocument/diagnostic") {
1254 Poco::JSON::Array::Ptr items = new Poco::JSON::Array();
1255 const std::string uri = documentUri();
1256 for (const script::ScriptDiagnostic& diagnostic : diagnosticsFor(uri)) {
1257 const int line = clampNonNegative(static_cast<int>(diagnostic.position.line) - 1);
1258 const int col = clampNonNegative(static_cast<int>(diagnostic.position.column) - 1);
1259 lsp::Range range{{static_cast<size_t>(line), static_cast<size_t>(col)},
1260 {static_cast<size_t>(line), static_cast<size_t>(col + 1)}};
1261 Poco::JSON::Object::Ptr item = new Poco::JSON::Object();
1262 item->set("range", rangeObject(range));
1263 item->set("severity", diagnostic.severity == script::ScriptDiagnosticSeverity::Error ? 1 : 2);
1264 item->set("code", diagnostic.code);
1265 item->set("source", "evescript");
1266 item->set("message", diagnostic.message);
1267 items->add(item);
1268 }
1269 Poco::JSON::Object::Ptr result = new Poco::JSON::Object();
1270 result->set("kind", "full");
1271 result->set("items", items);
1272 writeFrame(output, rpcResponse(id, Poco::Dynamic::Var(result)));
1273 } else if (hasId) {
1274 writeFrame(output, rpcError(id, -32601, "unsupported LSP method: " + method));
1275 }
1276 return LspDispatch::Continue;
1277}
1278
1279int LanguageServer::runStdio(std::istream& input, std::ostream& output) {
1280 std::string body;
1281 while (readFrame(input, body)) {
1282 if (handleMessage(body, output) == LspDispatch::Exit) return impl_->shutdown_ ? 0 : 1;
1283 }
1284 return impl_->shutdown_ ? 0 : 1;
1285}
1286
1287} // namespace eve::dev
double value
Duration start
int root
Definition AnimSmr.cpp:119
Vec3 relative
Definition AnimSmr.cpp:164
std::string output
ActiveSource owned
float length
Definition CaveMesh.cpp:94
bool split
Definition CaveMesh.cpp:123
HSQOBJECT cls
Definition ECS.cpp:21
std::uint16_t method
EvpackChunkInput input
Definition Evpack.cpp:170
std::string pattern
std::string message
DiagnosticCode code
const GltfImportRequest & request
std::unordered_map< const Graphics *, std::shared_ptr< Lifetime > > tokens
std::string text
TokenKind kind
size_t offset
Range range
char quote
std::array< float, 3 > position
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< std::shared_ptr< DeviceBytes > > bindings
Definition OnnxGpgpu.cpp:38
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
std::string uri
float begin
uint64_t token
bool found
double current
std::string string
LocalPageCacheEntry slots[ShadowConfig::kLocalSlots]
const SquirrelValueOptions & options
TacticalUnit * unit
std::size_t cursor
std::string body
uint32_t index
const UnitySourceAsset & source
std::uint32_t depth
const VegetationPresetContext & context
uint32_t semantic
Definition WfcSimple.cpp:15
EVENGINE_API_FOUNDATION public API.
Definition Runtime.h:189
ScriptId compileSource(std::string source, std::string sourceName="buffer")
Compiles script source without running it.
Definition Runtime.cpp:501
script::ScriptModuleResolver & scriptModules() noexcept
Script module provider registry and dependency resolver for this VM.
Definition Runtime.cpp:482
script::ScriptCompiler & scriptCompiler() noexcept
Unified EveScript compiler, metadata store, and binding contracts.
Definition Runtime.cpp:486
LanguageServer(std::string projectRoot)
const std::string & projectRoot() const noexcept
Absolute project root used to resolve game:/ URIs.
std::vector< script::ScriptSymbolMetadata > documentSymbols(std::string_view uri) const
std::optional< LanguageSignatureHelp > signatureHelp(std::string_view uri, lsp::Position position) const
void openDocument(std::string uri, std::string text)
static std::string fileUriFromPath(std::string_view path)
Converts an on-disk path to the file:// URI the server expects.
std::vector< script::ScriptDiagnostic > diagnosticsFor(std::string_view uri) const
std::optional< lsp::Location > definition(std::string_view uri, lsp::Position position) const
std::vector< lsp::FoldingRange > foldingRanges(std::string_view uri) const
Foldable brace, bracket, and block-comment ranges in an open document.
std::vector< script::ScriptCompletion > complete(std::string_view uri, lsp::Position position) const
std::optional< script::ScriptHover > hover(std::string_view uri, lsp::Position position) const
std::optional< std::vector< lsp::TextEdit > > rename(std::string_view uri, lsp::Position position, std::string_view newName) const
std::vector< lsp::Location > references(std::string_view uri, lsp::Position position, bool includeDeclaration) const
std::vector< lsp::SemanticToken > semanticTokens(std::string_view uri) const
Semantic highlight tokens for an open document (class / function / variable / …).
int runStdio(std::istream &input, std::ostream &output)
Stdio LSP host. Returns 0 after a clean shutdown.
std::vector< lsp::TextEdit > formatDocument(std::string_view uri, lsp::FormatOptions options={}, const lsp::Range *range=nullptr) const
Formats an open document (brace/bracket indent, trailing whitespace).
LspDispatch handleMessage(std::string_view jsonBody, std::ostream &output)
Dispatches one LSP JSON-RPC body and appends Content-Length frames.
void changeDocument(std::string uri, std::string text)
Replaces the whole document (full sync).
void closeDocument(std::string uri)
Cross-file semantic index for EveScript source modules.
void remove(std::string_view canonicalUri)
Removes one source unit from the index.
std::optional< Location > definition(std::string_view canonicalUri, Position position) const
Finds the declaration targeted by the identifier at a position.
void update(std::string canonicalUri, std::string clientUri, std::string source)
Adds or replaces one canonical source unit.
bool hasScriptClass(std::string_view scriptClass) const noexcept
True when generated contracts include this script class.
std::vector< BindingContract > snapshot() const
Returns a stable snapshot sorted by contract key.
const BindingContract * findMethod(std::string_view method) const noexcept
Finds a uniquely named method.
std::vector< ScriptDiagnostic > diagnostics(std::string_view canonicalUri) const
Returns the latest diagnostics for one source unit.
BindingContractRegistry & bindings() noexcept
Native binding contracts used by semantic checks and tools.
eve::OptionalRef< const ScriptMetadata > metadata(std::string_view canonicalUri) const noexcept
Borrows metadata for the most recent successful compilation.
static std::vector< ScriptDiagnostic > validateProjectConfig(std::string_view source, std::string_view canonicalUri, const std::vector< std::string > &knownModules, const std::vector< std::string > &activeModules)
Statically checks literal config module lists without executing config.nut.
std::optional< ScriptHover > hover(std::string_view canonicalUri, std::string_view symbol) const
Returns hover information for a script or native symbol.
std::vector< ScriptCompletion > completions(std::string_view canonicalUri, std::string_view prefix={}) const
Returns completion candidates from script metadata and native contracts.
ProviderId registerProvider(std::shared_ptr< IScriptModuleProvider > provider, int priority=0)
Registers a provider. Higher priorities are tried first.
static bool canonicalize(const ScriptModuleRequest &request, std::string &output, std::string &error)
Normalizes a logical module URI and rejects root traversal.
std::string qualifier
std::string word
bool memberAccess
std::vector< ParamSpec > params
constexpr uint32_t DefaultLibrary
constexpr uint32_t Variable
constexpr uint32_t Method
constexpr uint32_t Namespace
constexpr uint32_t Keyword
constexpr uint32_t Function
constexpr uint32_t Property
size_t lastLineIndex(std::string_view source) noexcept
Last 0-based line index (empty source is line 0).
std::string sliceLines(std::string_view source, size_t startLine, size_t endLineInclusive)
Source bytes for coveringLines of the same line span.
bool applyIncremental(std::string &source, const Range *range, std::string_view text)
Applies one LSP content change.
std::string formatEveScript(std::string_view source, const FormatOptions &options)
Indents EveScript by braces/brackets and trims trailing whitespace.
Range coveringLines(std::string_view source, size_t startLine, size_t endLineInclusive) noexcept
Half-open LSP range covering startLine through endLine inclusive. Ends at the start of the following ...
Range documentRange(std::string_view source) noexcept
Full-document range covering every character in source.
std::vector< FoldingRange > foldingRanges(std::string_view source)
Brace, bracket, and block-comment folds that span more than one line.
LspDispatch
Whether the stdio/JSON-RPC host should keep reading after one dispatch.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
Result< std::string > stringify(const eve::Value &value)
Serialize the canonical owning Value as deterministic compact JSON.
Definition Json.cpp:563
ScriptModuleStatus
Result of asking a provider to resolve or load a script module.
const char * module_list_content
EVEngine ECS 集成层:底层实现使用 sunxfancy/ECS.hpp https://github.com/sunxfancy/ECS.hpp
Definition ECS.h:14
std::unordered_map< std::string, std::string > canonical_
std::unordered_map< std::string, std::string > slotClass_
std::filesystem::path pathForCanonical(std::string_view canonical) const
std::unordered_map< std::string, std::string > documents_
std::string canonicalUri(const std::string &uri) const
const std::string * sourceFor(std::string_view uri) const
std::vector< script::ScriptDiagnostic > diagnostics(std::string_view uri) const
void update(const std::string &uri, std::string source)
std::optional< LanguageSignatureHelp > signatureHelp(std::string_view uri, lsp::Position position) const
std::string identity(std::string_view uri) const
std::vector< std::string > activeModules_
std::optional< script::ScriptHover > hover(std::string_view uri, lsp::Position position) const
std::vector< script::ScriptCompletion > complete(std::string_view uri, lsp::Position position) const
void close(const std::string &uri)
std::vector< std::string > knownModules_
Binding-contract signature shown while typing a call.
std::vector< LanguageSignatureParameter > parameters
One parameter in an LSP signature-help result.
One foldable region (0-based inclusive end line).
Editor formatting preferences from LSP FormattingOptions.
Location public API.
Position public API.
Range public API.
One identifier classified for editor semantic highlighting.
TextEdit public API.
Complete script-facing contract for a native function or method.
std::vector< BindingParameterContract > parameters
std::string std::string scriptClass
Tool-neutral completion item suitable for LSP, MCP, and editor adapters.
Stable structured diagnostic shared by Runtime, LSP, MCP, and editor tools.
Tool-neutral hover result for one script or binding symbol.
Erased source-level symbol retained for tooling.