hoshi-lang dev
Yet another programming language
Loading...
Searching...
No Matches
completion.cpp
Go to the documentation of this file.
1//
2// Completion Provider implementation
3//
4
5#include "completion.h"
6#include <algorithm>
7#include <compiler/ir/IR.h>
8#include <filesystem>
9#include <set>
10#include <share/def.hpp>
11
12namespace lsp {
13
14namespace fs = std::filesystem;
15
16static std::string documentFilePath(const Document *doc) {
17 if (!doc) return "";
18 std::string path = doc->uri;
19 if (path.rfind("file://", 0) == 0) path = path.substr(7);
20 return path;
21}
22
23static std::vector<fs::path> moduleSearchRoots(const Document *doc) {
24 std::vector<fs::path> roots;
25 const std::string documentPath = documentFilePath(doc);
26 if (!documentPath.empty()) {
27 const fs::path parent = fs::path(documentPath).parent_path();
28 roots.push_back(parent);
29 roots.push_back(parent / ".tsuki_modules");
30 }
31 if (doc && doc->projectIndex) {
32 for (const auto &searchPath : doc->projectIndex->getSearchPaths()) {
33 if (!searchPath.empty()) roots.emplace_back(searchPath);
34 }
35 }
36 return roots;
37}
38
39// Look up a function's return type from the visitor's IR module.
40static std::string getReturnTypeFromIR(Document *doc, const std::string &funcName) {
41 if (!doc || !doc->irModule) return "";
42 yoi::wstr wName = yoi::string2wstring(funcName);
43 // Check function overloads first
44 if (doc->irModule->functionOverloadIndexies.contains(wName)) {
45 auto &overloads = doc->irModule->functionOverloadIndexies[wName];
46 if (!overloads.empty()) {
47 auto idx = overloads[0];
48 auto &funcDef = *doc->irModule->functionTable[idx];
49 // funcDef has returnType info; parse from the name string
50 std::string detail = yoi::wstring2string(funcDef.name);
51 size_t colon = detail.rfind(':');
52 if (colon != std::string::npos) {
53 std::string retType = detail.substr(colon + 1);
54 while (!retType.empty() && std::isspace(retType.front())) retType.erase(0, 1);
55 size_t tmpl = retType.find('<');
56 if (tmpl != std::string::npos) retType = retType.substr(0, tmpl);
57 size_t dot = retType.rfind('.');
58 if (dot != std::string::npos) retType = retType.substr(dot + 1);
59 return retType;
60 }
61 }
62 }
63 return "";
64}
65
67 CompletionList result;
68 result.isIncomplete = false;
69
70 if (!doc) return result;
71
72 int charPos = static_cast<int>(pos.character);
73 int lineNum = static_cast<int>(pos.line);
74
75 // Find the line text
76 std::string lineText;
77 {
78 int lineIdx = 0;
79 size_t start = 0;
80 std::string text = yoi::wstring2string(doc->text);
81 for (size_t i = 0; i < text.size(); i++) {
82 if (text[i] == '\n') {
83 if (lineIdx == lineNum) {
84 lineText = text.substr(start, i - start);
85 break;
86 }
87 lineIdx++;
88 start = i + 1;
89 }
90 }
91 if (lineText.empty() && lineIdx == lineNum) {
92 lineText = text.substr(start);
93 }
94 }
95
96 // Get the prefix (word before cursor)
97 std::string prefix;
98 if (charPos > 0 && charPos <= static_cast<int>(lineText.size())) {
99 std::string before = lineText.substr(0, charPos);
100 size_t wordStart = before.size();
101 while (wordStart > 0 && (std::isalnum(before[wordStart - 1]) || before[wordStart - 1] == '_'))
102 wordStart--;
103 prefix = before.substr(wordStart);
104 }
105
106 // Detect dot-access: cursor after a dot or after an identifier preceded by a dot
107 bool afterDot = false;
108 std::string dotPrefix; // The identifier before the dot (module alias / struct name)
109 if (charPos > 0 && charPos <= static_cast<int>(lineText.size())) {
110 std::string before = lineText.substr(0, charPos);
111 while (!before.empty() && std::isspace(before.back()))
112 before.pop_back();
113 // Check if the character immediately before the cursor is a dot,
114 // or if there's a dot before the current word
115 if (!before.empty()) {
116 // Find the last dot position
117 size_t lastDot = before.rfind('.');
118 if (lastDot != std::string::npos) {
119 // Check if cursor is right after the dot (no word yet)
120 if (lastDot == before.size() - 1) {
121 afterDot = true;
122 // Extract the identifier before the dot
123 std::string beforeDot = before.substr(0, lastDot);
124 // Get the last identifier before the dot
125 size_t idEnd = beforeDot.size();
126 size_t idStart = idEnd;
127 while (idStart > 0 && (std::isalnum(beforeDot[idStart - 1]) || beforeDot[idStart - 1] == '_'))
128 idStart--;
129 if (idStart < idEnd) {
130 dotPrefix = beforeDot.substr(idStart, idEnd - idStart);
131 }
132 } else {
133 // There's a word after the dot — check if it's part of the current identifier
134 std::string afterDotStr = before.substr(lastDot + 1);
135 // If prefix starts at or after lastDot+1, we're doing member completion
136 if (prefix.rfind(afterDotStr, 0) == 0 || afterDotStr.rfind(prefix, 0) == 0) {
137 afterDot = true;
138 prefix = afterDotStr; // Use the part after dot as prefix
139 std::string beforeDot = before.substr(0, lastDot);
140 size_t idEnd = beforeDot.size();
141 size_t idStart = idEnd;
142 while (idStart > 0 && (std::isalnum(beforeDot[idStart - 1]) || beforeDot[idStart - 1] == '_'))
143 idStart--;
144 if (idStart < idEnd) {
145 dotPrefix = beforeDot.substr(idStart, idEnd - idStart);
146 }
147 }
148 }
149 }
150 }
151 }
152
153 // Gather completions
154 const std::string beforeCursor = charPos >= 0 && charPos <= static_cast<int>(lineText.size())
155 ? lineText.substr(0, charPos) : lineText;
156 const size_t firstNonSpace = beforeCursor.find_first_not_of(" \t");
157 const std::string useText = firstNonSpace == std::string::npos ? "" : beforeCursor.substr(firstNonSpace);
158 if (useText.rfind("use", 0) == 0 &&
159 (useText.size() == 3 || std::isspace(static_cast<unsigned char>(useText[3])))) {
160 const std::string useTail = useText.substr(3);
161 const size_t quote = useTail.find('"');
162 if (quote != std::string::npos && useTail.find('"', quote + 1) == std::string::npos) {
163 result.items = usePathCompletions(doc, useTail.substr(quote + 1));
164 } else if (quote == std::string::npos) {
165 result.items = useModuleCompletions(doc, prefix);
166 }
167 return result;
168 } else if (!doc->parseSucceeded && afterDot && !dotPrefix.empty()) {
169 // Parse failed but we have a dot-prefix — try to resolve the module
170 // from the raw text. This handles incomplete code like "str." where
171 // the parser throws on the trailing dot.
172 result.items = resolveModuleCompletionsFromText(doc, dotPrefix, prefix);
173 // Fall back to keywords if module resolution failed
174 if (result.items.empty()) {
175 result.items = keywordCompletions(prefix);
176 }
177 return result;
178 } else if (!doc->parseSucceeded) {
179 // Parse failed — return keywords as a fallback
180 result.items = keywordCompletions(prefix);
181 return result;
182 } else if (afterDot && !dotPrefix.empty()) {
183 // Member access — try to match dotPrefix to a module alias or struct
184 result.items = memberCompletions(doc, dotPrefix, prefix, lineNum);
185 // If member completions returned nothing, fall back to global completions
186 if (result.items.empty()) {
187 result.items = symbolCompletions(doc, prefix, lineNum);
188 auto crossItems = crossModuleCompletions(doc, prefix);
189 result.items.insert(result.items.end(), crossItems.begin(), crossItems.end());
190 auto kwCompletions = keywordCompletions(prefix);
191 result.items.insert(result.items.end(), kwCompletions.begin(), kwCompletions.end());
192 }
193 } else if (afterDot) {
194 // Dot without a clear prefix (e.g., at start of line, or after a number)
195 // Fall through to global completions instead of returning empty
196 result.items = symbolCompletions(doc, prefix, lineNum);
197 auto crossItems = crossModuleCompletions(doc, prefix);
198 result.items.insert(result.items.end(), crossItems.begin(), crossItems.end());
199 auto kwCompletions = keywordCompletions(prefix);
200 result.items.insert(result.items.end(), kwCompletions.begin(), kwCompletions.end());
201 } else {
202 // Symbol + keyword completions (global scope)
203 result.items = symbolCompletions(doc, prefix, lineNum);
204 auto crossItems = crossModuleCompletions(doc, prefix);
205 result.items.insert(result.items.end(), crossItems.begin(), crossItems.end());
206 auto kwCompletions = keywordCompletions(prefix);
207 result.items.insert(result.items.end(), kwCompletions.begin(), kwCompletions.end());
208 }
209
210 // Sort alphabetically
211 std::sort(result.items.begin(), result.items.end(),
212 [](const CompletionItem &a, const CompletionItem &b) {
213 return a.sortText.value_or(a.label) < b.sortText.value_or(b.label);
214 });
215
216 return result;
217}
218
219std::vector<CompletionItem> CompletionProvider::memberCompletions(Document *doc, const std::string &parent, const std::string &prefix, int cursorLine) {
220 std::vector<CompletionItem> result;
221 yoi::wstr wParent = yoi::string2wstring(parent);
222
223 // Find all symbols matching parent name
224 std::vector<const Symbol *> matching;
225 for (auto &sym : doc->symbols) {
226 if (cursorLine >= 0 &&
227 !isSymbolVisibleAt(doc->symbols, sym, static_cast<yoi::indexT>(cursorLine))) {
228 continue;
229 }
230 if (sym.name == wParent) matching.push_back(&sym);
231 }
232 // If multiple matches, pick the one closest to cursorLine (innermost scope)
233 const Symbol *bestMatch = nullptr;
234 if (!matching.empty()) {
235 bestMatch = matching[0];
236 if (cursorLine >= 0 && matching.size() > 1) {
237 for (auto *m : matching) {
238 if (static_cast<int>(m->line) <= cursorLine &&
239 static_cast<int>(m->line) > static_cast<int>(bestMatch->line)) {
240 bestMatch = m;
241 }
242 }
243 }
244 }
245
246 if (bestMatch) {
247 const auto &sym = *bestMatch;
248 if (sym.name == wParent) {
249 // Case 1: struct/interface — offer its children
250 if (!sym.children.empty()) {
251 for (auto &child : sym.children) {
252 std::string name = yoi::wstring2string(child.name);
253 if (name.rfind(prefix, 0) != 0) continue;
255 item.label = name;
256 item.insertText = name;
257 result.push_back(item);
258 }
259 return result; // Found struct/interface with children
260 }
261 // Case 2: typed variable — look up its type and offer methods
262 if (sym.kind == HoshiSymbolKind::Variable && !sym.typeInfo.empty()) {
263 result = completionsForType(doc, sym.typeInfo, prefix);
264 if (!result.empty()) return result;
265 }
266 // If it's a ModuleAlias or unresolved, fall through to cross-module search
267 }
268 }
269
270 // Check cross-module symbols: find a ModuleAlias with name == parent
271 for (auto &sym : doc->symbols) {
272 if (sym.kind == HoshiSymbolKind::ModuleAlias && sym.name == wParent) {
273 // This is a module alias — offer all symbols from the resolved module
274 for (auto &cross : doc->crossModuleSymbols) {
275 std::string name = yoi::wstring2string(cross.name);
276 if (name.rfind(prefix, 0) != 0) continue;
278 item.label = name;
279 item.insertText = name;
280 item.documentation = std::string("From module ") + yoi::wstring2string(sym.importPath);
281 result.push_back(item);
282 }
283 return result;
284 }
285 }
286
287 // Check cross-module symbols: find a struct/interface by name and offer children
288 for (auto &cross : doc->crossModuleSymbols) {
289 if (cross.name == wParent && !cross.children.empty() &&
290 (cross.kind == HoshiSymbolKind::Struct || cross.kind == HoshiSymbolKind::Interface)) {
291 for (auto &child : cross.children) {
292 std::string name = yoi::wstring2string(child.name);
293 if (name.rfind(prefix, 0) != 0) continue;
295 item.label = name;
296 item.insertText = name;
297 item.documentation = std::string("From ") + yoi::wstring2string(cross.sourceFile);
298 result.push_back(item);
299 }
300 if (!result.empty()) return result;
301 }
302 }
303
304 // Also check cross-module symbols directly by parentName (for methods/fields)
305 for (auto &cross : doc->crossModuleSymbols) {
306 if (cross.parentName == wParent) {
307 std::string name = yoi::wstring2string(cross.name);
308 if (name.rfind(prefix, 0) != 0) continue;
310 item.label = name;
311 item.insertText = name;
312 result.push_back(item);
313 }
314 }
315
316 return result;
317}
318
319std::vector<CompletionItem> CompletionProvider::crossModuleCompletions(Document *doc, const std::string &prefix) {
320 std::vector<CompletionItem> result;
321
322 if (!doc) return result;
323
324 for (auto &sym : doc->crossModuleSymbols) {
325 if (sym.isLocal) continue;
326 std::string name = yoi::wstring2string(sym.name);
327 if (name.rfind(prefix, 0) != 0) continue;
328
330 item.label = name;
331 item.insertText = name;
332 item.sortText = name;
333 if (!sym.sourceFile.empty()) {
334 item.documentation = std::string("From ") + yoi::wstring2string(sym.sourceFile);
335 }
336 result.push_back(item);
337 }
338
339 return result;
340}
341
343 CompletionItem item;
344 item.label = yoi::wstring2string(sym.name);
348
349 switch (sym.kind) {
366 default: item.kind = CompletionItemKind::Text; break;
367 }
368
369 if (!sym.typeInfo.empty())
370 item.documentation = "Type: " + yoi::wstring2string(sym.typeInfo);
371
372 return item;
373}
374
375std::vector<CompletionItem> CompletionProvider::keywordCompletions(const std::string &prefix) {
376 static const std::vector<std::pair<const char *, const char *>> keywords = {
377 {"func", "Function definition"},
378 {"struct", "Struct definition"},
379 {"interface", "Interface definition"},
380 {"impl", "Implementation block"},
381 {"let", "Variable declaration"},
382 {"if", "If statement"},
383 {"elif", "Else-if branch"},
384 {"else", "Else branch"},
385 {"while", "While loop"},
386 {"for", "For loop"},
387 {"forEach", "For-each loop"},
388 {"return", "Return statement"},
389 {"break", "Break statement"},
390 {"continue", "Continue statement"},
391 {"import", "Import declaration"},
392 {"export", "Export declaration"},
393 {"use", "Use (module alias) statement"},
394 {"alias", "Type alias"},
395 {"enum", "Enumeration definition"},
396 {"datastruct", "Data struct definition"},
397 {"new", "New expression"},
398 {"try", "Try block"},
399 {"catch", "Catch block"},
400 {"finally", "Finally block"},
401 {"throw", "Throw statement"},
402 {"type_id", "Type ID expression"},
403 {"dyn_cast", "Dynamic cast expression"},
404 {"yield", "Yield statement"},
405 {"concept", "Concept definition"},
406 {"constructor", "Constructor"},
407 {"finalizer", "Finalizer"},
408 {"true", "Boolean true literal"},
409 {"false", "Boolean false literal"},
410 {"null", "Null literal"},
411 {"as", "Type cast / export alias"},
412 {"from", "Import source specifier"},
413 {"in", "Membership check"},
414 {"no_ffi", "Function attribute: no FFI"},
415 {"static", "Function attribute: static"},
416 {"intrinsic", "Function attribute: intrinsic"},
417 {"generator", "Function attribute: generator"},
418 {"always_inline", "Function attribute: always inline"},
419 {"datafield", "Struct field modifier"},
420 {"callable", "Callable interface"},
421 {"weak", "Weak modifier"},
422 {"satisfy", "Concept satisfy clause"},
423 };
424
425 std::vector<CompletionItem> result;
426 for (auto &[kw, desc] : keywords) {
427 if (prefix.empty() || std::string(kw).rfind(prefix, 0) == 0) {
428 CompletionItem item;
429 item.label = kw;
431 item.detail = desc;
432 item.insertText = kw;
433 item.sortText = kw;
434 result.push_back(item);
435 }
436 }
437 return result;
438}
439
441 Document *doc, const std::string &prefix) {
442 std::vector<CompletionItem> result;
443 std::set<std::string> moduleNames = {"builtin"};
444
445 for (const auto &root : moduleSearchRoots(doc)) {
446 std::error_code ec;
447 if (!fs::is_directory(root, ec)) continue;
448 for (const auto &entry : fs::directory_iterator(root, ec)) {
449 if (ec) break;
450 const fs::path path = entry.path();
451 if (entry.is_regular_file(ec) && path.extension() == ".hoshi") {
452 moduleNames.insert(path.stem().string());
453 } else if (entry.is_directory(ec) && fs::is_regular_file(path / "index.hoshi", ec)) {
454 moduleNames.insert(path.filename().string());
455 }
456 }
457 }
458
459 for (const auto &name : moduleNames) {
460 if (!prefix.empty() && name.rfind(prefix, 0) != 0) continue;
461 CompletionItem item;
462 item.label = name;
464 item.detail = "use " + name + " \"" + name + "\"";
465 item.insertText = name + " \"" + name + "\"";
466 item.sortText = name;
467 result.push_back(std::move(item));
468 }
469 return result;
470}
471
472std::vector<CompletionItem> CompletionProvider::usePathCompletions(
473 Document *doc, const std::string &pathPrefix) {
474 std::vector<CompletionItem> result;
475 const size_t slash = pathPrefix.find_last_of("/\\");
476 const std::string directoryPart = slash == std::string::npos ? "" : pathPrefix.substr(0, slash + 1);
477 const std::string entryPrefix = slash == std::string::npos ? pathPrefix : pathPrefix.substr(slash + 1);
478 std::set<std::string> entries;
479
480 for (const auto &root : moduleSearchRoots(doc)) {
481 const fs::path directory = root / fs::path(directoryPart);
482 std::error_code ec;
483 if (!fs::is_directory(directory, ec)) continue;
484 for (const auto &entry : fs::directory_iterator(directory, ec)) {
485 if (ec) break;
486 const std::string name = entry.path().filename().string();
487 if (name.empty() || name[0] == '.' ||
488 (!entryPrefix.empty() && name.rfind(entryPrefix, 0) != 0)) {
489 continue;
490 }
491 if (entry.is_directory(ec)) {
492 entries.insert(name + "/");
493 } else if (entry.is_regular_file(ec) && entry.path().extension() == ".hoshi") {
494 entries.insert(name);
495 }
496 }
497 }
498
499 for (const auto &entry : entries) {
500 CompletionItem item;
501 item.label = directoryPart + entry;
502 item.kind = entry.back() == '/' ? CompletionItemKind::Folder : CompletionItemKind::File;
503 item.detail = entry.back() == '/' ? "directory" : "Hoshi module";
504 item.insertText = entry;
505 item.sortText = entry;
506 result.push_back(std::move(item));
507 }
508 return result;
509}
510
511std::vector<CompletionItem> CompletionProvider::symbolCompletions(Document *doc,
512 const std::string &prefix,
513 int cursorLine) {
514 std::vector<CompletionItem> result;
515
516 if (!doc || !doc->parseSucceeded) return result;
517
518 for (auto &sym : doc->symbols) {
519 if (cursorLine >= 0 &&
520 !isSymbolVisibleAt(doc->symbols, sym, static_cast<yoi::indexT>(cursorLine))) {
521 continue;
522 }
523 std::string name = yoi::wstring2string(sym.name);
524 if (name.rfind(prefix, 0) != 0) continue;
525
527 item.label = name;
528 item.insertText = name;
529 item.sortText = name;
530 result.push_back(item);
531 }
532
533 return result;
534}
535
537 Document *doc, const std::string &parent, const std::string &prefix) {
538 std::vector<CompletionItem> result;
539 if (!doc || !doc->projectIndex) return result;
540
541 // Search the raw text for "use <parent> \"...\"" or "import ... from \"...\"" patterns
542 std::string text = yoi::wstring2string(doc->text);
543 std::string importPath;
544
545 // Pattern 1: use <parent> "<path>"
546 std::string usePattern = "use " + parent + " \"";
547 size_t pos = text.find(usePattern);
548 if (pos != std::string::npos) {
549 size_t pathStart = pos + usePattern.size();
550 size_t pathEnd = text.find('"', pathStart);
551 if (pathEnd != std::string::npos) {
552 importPath = text.substr(pathStart, pathEnd - pathStart);
553 }
554 }
555
556 // Pattern 2: import ... from "<path>" (any import whose path might contain the module)
557 if (importPath.empty()) {
558 std::string importKw = "import ";
559 size_t ipos = 0;
560 while ((ipos = text.find(importKw, ipos)) != std::string::npos) {
561 size_t fromPos = text.find("from \"", ipos);
562 if (fromPos != std::string::npos) {
563 size_t pathStart = fromPos + 6; // strlen("from \"")
564 size_t pathEnd = text.find('"', pathStart);
565 if (pathEnd != std::string::npos) {
566 std::string maybePath = text.substr(pathStart, pathEnd - pathStart);
567 // Check if the module path ends with the parent name
568 if (maybePath == parent ||
569 maybePath.rfind("/" + parent) == maybePath.size() - parent.size() - 1 ||
570 maybePath.rfind("\\" + parent) == maybePath.size() - parent.size() - 1) {
571 importPath = maybePath;
572 break;
573 }
574 }
575 }
576 ipos = fromPos != std::string::npos ? fromPos + 1 : ipos + 1;
577 }
578 }
579
580 if (importPath.empty()) {
581 // Not a module alias — try to find the variable's type from its declaration.
582 // Look for "let <parent> = <Module>.<Type>(...)" or "let <parent>: <Type>"
583 std::string letPattern = "let " + parent + " = ";
584 size_t letPos = text.find(letPattern);
585 if (letPos == std::string::npos) {
586 letPattern = "let " + parent + ": ";
587 letPos = text.find(letPattern);
588 }
589 if (letPos != std::string::npos) {
590 std::string afterLet = text.substr(letPos + letPattern.size());
591 // Extract the type: for "str.Str(" → module="str", type="Str"
592 size_t dotPos = afterLet.find('.');
593 std::string typeName, moduleName;
594 if (dotPos != std::string::npos && dotPos < afterLet.find('(')) {
595 // Module.Type pattern — extract module and type
596 moduleName = afterLet.substr(0, dotPos);
597 size_t typeStart = dotPos + 1;
598 size_t typeEnd = typeStart;
599 while (typeEnd < afterLet.size() && (std::isalnum(afterLet[typeEnd]) || afterLet[typeEnd] == '_'))
600 typeEnd++;
601 typeName = afterLet.substr(typeStart, typeEnd - typeStart);
602 } else {
603 size_t typeEnd = 0;
604 while (typeEnd < afterLet.size() && (std::isalnum(afterLet[typeEnd]) || afterLet[typeEnd] == '_'))
605 typeEnd++;
606 typeName = afterLet.substr(0, typeEnd);
607 }
608 if (!typeName.empty() && doc->projectIndex) {
609 // Index the module and resolve the type
610 if (!moduleName.empty()) {
611 std::string fp = doc->uri;
612 if (fp.rfind("file://", 0) == 0) fp = fp.substr(7);
613 std::string useP = "use " + moduleName + " \"";
614 size_t upos = text.find(useP);
615 if (upos != std::string::npos) {
616 size_t ps = upos + useP.size();
617 size_t pe = text.find('"', ps);
618 if (pe != std::string::npos) {
619 std::string resolvedPath = doc->projectIndex->resolveModule(
620 text.substr(ps, pe - ps), fp);
621 if (!resolvedPath.empty()) {
622 doc->projectIndex->indexAndGet(resolvedPath);
623 }
624 }
625 }
626 }
627
628 // If lowercase (function call), look up the function's return type
629 std::string lookupType = typeName;
630 if (std::islower(typeName[0]) && !moduleName.empty()) {
631 // First try the visitor's IR module for precise return type
632 std::string irRetType = getReturnTypeFromIR(doc, typeName);
633 if (!irRetType.empty()) {
634 lookupType = irRetType;
635 } else {
636 // Fall back to symbol-based lookup
637 yoi::wstr wFunc = yoi::string2wstring(typeName);
638 yoi::vec<Symbol> allSyms;
639 doc->projectIndex->getAllSymbols(allSyms);
640 for (auto &sym : allSyms) {
641 if (sym.name == wFunc && sym.kind == HoshiSymbolKind::Function && !sym.typeInfo.empty()) {
642 std::string retType = yoi::wstring2string(sym.typeInfo);
643 size_t lastDot = retType.rfind('.');
644 if (lastDot != std::string::npos) retType = retType.substr(lastDot + 1);
645 size_t tmpl = retType.find('<');
646 if (tmpl != std::string::npos) retType = retType.substr(0, tmpl);
647 while (!retType.empty() && std::isspace(retType.back())) retType.pop_back();
648 if (!retType.empty()) { lookupType = retType; break; }
649 }
650 }
651 }
652 }
653
654 // Look up the type name for member completion
655 if (std::isupper(lookupType[0])) {
656 auto completions = completionsForTypeNameFromModules(doc, lookupType, prefix);
657 if (!completions.empty()) return completions;
658 }
659 }
660 }
661 return result;
662 }
663
664 // Resolve the module path relative to the document's directory
665 std::string filePath = doc->uri;
666 if (filePath.rfind("file://", 0) == 0) {
667 filePath = filePath.substr(7);
668 }
669 std::string resolvedPath = doc->projectIndex->resolveModule(importPath, filePath);
670 if (resolvedPath.empty()) return result;
671
672 // Get symbols from the resolved module
673 const auto &modSymbols = doc->projectIndex->indexAndGet(resolvedPath);
674
675 // Build completion items
676 for (const auto &sym : modSymbols) {
677 if (sym.isLocal) continue;
678 std::string name = yoi::wstring2string(sym.name);
679 if (!prefix.empty() && name.rfind(prefix, 0) != 0) continue;
680
682 item.label = name;
683 item.insertText = name;
684 item.sortText = name;
685 item.documentation = std::string("From ") + resolvedPath;
686 result.push_back(item);
687 }
688
689 return result;
690}
691
692std::vector<CompletionItem> CompletionProvider::completionsForType(
693 Document *doc, const yoi::wstr &typeName, const std::string &prefix) {
694 std::vector<CompletionItem> result;
695 bool found = false;
696
697 // Search local symbols for a struct/interface with this name
698 for (auto &sym : doc->symbols) {
699 if (sym.name == typeName &&
700 (sym.kind == HoshiSymbolKind::Struct || sym.kind == HoshiSymbolKind::Interface)) {
701 found = true;
702 // Include children (declared inside the struct)
703 for (auto &child : sym.children) {
704 std::string name = yoi::wstring2string(child.name);
705 if (name.rfind(prefix, 0) != 0) continue;
707 item.label = name;
708 item.insertText = name;
709 item.documentation = std::string("From ") + yoi::wstring2string(sym.name);
710 result.push_back(item);
711 }
712 }
713 // Also include symbols whose parentName matches (methods from impl blocks)
714 if (sym.parentName == typeName &&
715 (sym.kind == HoshiSymbolKind::Method || sym.kind == HoshiSymbolKind::Field ||
716 sym.kind == HoshiSymbolKind::Constructor || sym.kind == HoshiSymbolKind::Finalizer)) {
717 std::string name = yoi::wstring2string(sym.name);
718 if (name.rfind(prefix, 0) != 0) continue;
720 item.label = name;
721 item.insertText = name;
722 item.documentation = std::string("From ") + yoi::wstring2string(typeName);
723 result.push_back(item);
724 }
725 }
726 if (found && !result.empty()) return result;
727
728 // Search cross-module symbols
729 yoi::vec<Symbol> allCross;
730 for (auto &cross : doc->crossModuleSymbols) allCross.push_back(cross);
731 // Also get all from ProjectIndex
732 if (doc->projectIndex) {
733 doc->projectIndex->getAllSymbols(allCross);
734 }
735
736 for (auto &cross : allCross) {
737 if (cross.name == typeName &&
738 (cross.kind == HoshiSymbolKind::Struct || cross.kind == HoshiSymbolKind::Interface)) {
739 found = true;
740 for (auto &child : cross.children) {
741 std::string name = yoi::wstring2string(child.name);
742 if (name.rfind(prefix, 0) != 0) continue;
744 item.label = name;
745 item.insertText = name;
746 item.documentation = std::string("From ") + yoi::wstring2string(typeName);
747 result.push_back(item);
748 }
749 }
750 if (cross.parentName == typeName &&
751 (cross.kind == HoshiSymbolKind::Method || cross.kind == HoshiSymbolKind::Field ||
752 cross.kind == HoshiSymbolKind::Constructor || cross.kind == HoshiSymbolKind::Finalizer)) {
753 std::string name = yoi::wstring2string(cross.name);
754 if (name.rfind(prefix, 0) != 0) continue;
756 item.label = name;
757 item.insertText = name;
758 item.documentation = std::string("From ") + yoi::wstring2string(typeName);
759 result.push_back(item);
760 }
761 }
762
763 return result;
764}
765
767 Document *doc, const std::string &typeName, const std::string &prefix) {
768 std::vector<CompletionItem> result;
769 if (!doc || !doc->projectIndex) return result;
770 yoi::wstr wTypeName = yoi::string2wstring(typeName);
771
772 // Always ensure builtin is indexed (contains core types like Result, TypeInfo, etc.)
773 doc->projectIndex->indexAndGet("builtin");
774
775 // Check cross-module symbols already loaded
776 for (auto &cross : doc->crossModuleSymbols) {
777 if (cross.name == wTypeName && !cross.children.empty() &&
778 (cross.kind == HoshiSymbolKind::Struct || cross.kind == HoshiSymbolKind::Interface)) {
779 for (auto &child : cross.children) {
780 std::string name = yoi::wstring2string(child.name);
781 if (name.rfind(prefix, 0) != 0) continue;
783 item.label = name;
784 item.insertText = name;
785 item.documentation = std::string("From ") + typeName;
786 result.push_back(item);
787 }
788 return result;
789 }
790 }
791
792 // Also check all loaded modules in the ProjectIndex
793 yoi::vec<Symbol> allSymbols;
794 doc->projectIndex->getAllSymbols(allSymbols);
795 for (auto &sym : allSymbols) {
796 if (sym.name == wTypeName && !sym.children.empty() &&
797 (sym.kind == HoshiSymbolKind::Struct || sym.kind == HoshiSymbolKind::Interface)) {
798 for (auto &child : sym.children) {
799 std::string name = yoi::wstring2string(child.name);
800 if (name.rfind(prefix, 0) != 0) continue;
802 item.label = name;
803 item.insertText = name;
804 item.documentation = std::string("From ") + typeName;
805 result.push_back(item);
806 }
807 return result;
808 }
809 }
810
811 return result;
812}
813
814} // namespace lsp
std::vector< CompletionItem > completionsForType(Document *doc, const yoi::wstr &typeName, const std::string &prefix)
std::vector< CompletionItem > usePathCompletions(Document *doc, const std::string &pathPrefix)
std::vector< CompletionItem > completionsForTypeNameFromModules(Document *doc, const std::string &typeName, const std::string &prefix)
std::vector< CompletionItem > keywordCompletions(const std::string &prefix)
CompletionList provide(Document *doc, const Position &pos)
std::vector< CompletionItem > resolveModuleCompletionsFromText(Document *doc, const std::string &parent, const std::string &prefix)
std::vector< CompletionItem > symbolCompletions(Document *doc, const std::string &prefix, int cursorLine)
std::vector< CompletionItem > crossModuleCompletions(Document *doc, const std::string &prefix)
CompletionItem symbolToCompletionItem(const Symbol &sym)
std::vector< CompletionItem > memberCompletions(Document *doc, const std::string &parent, const std::string &prefix, int cursorLine=-1)
std::vector< CompletionItem > useModuleCompletions(Document *doc, const std::string &prefix)
const yoi::vec< Symbol > & indexAndGet(const std::string &absolutePath)
Parse and index a module, then return its symbols.
const std::vector< std::string > & getSearchPaths() const
std::string resolveModule(const std::string &importPath, const std::string &relativeToFile)
void getAllSymbols(yoi::vec< Symbol > &out)
Collect all symbols from all indexed modules into flat list.
detail namespace with internal helper functions
Definition json.hpp:263
bool isSymbolVisibleAt(const yoi::vec< Symbol > &symbols, const Symbol &symbol, yoi::indexT line)
static std::string getReturnTypeFromIR(Document *doc, const std::string &funcName)
static std::string documentFilePath(const Document *doc)
static std::vector< fs::path > moduleSearchRoots(const Document *doc)
std::string wstring2string(const std::wstring &v)
Definition def.cpp:230
std::vector< t > vec
Definition def.hpp:56
std::wstring string2wstring(const std::string &v)
Definition def.cpp:224
std::wstring wstr
Definition def.hpp:51
uint64_t indexT
Definition def.hpp:54
std::optional< std::string > sortText
Definition protocol.h:203
std::string label
Definition protocol.h:198
std::optional< std::string > detail
Definition protocol.h:200
std::optional< std::string > insertText
Definition protocol.h:202
CompletionItemKind kind
Definition protocol.h:199
std::optional< std::string > documentation
Definition protocol.h:201
std::vector< CompletionItem > items
Definition protocol.h:216
yoi::vec< Symbol > crossModuleSymbols
Definition document.h:30
std::string uri
Definition document.h:24
yoi::vec< Symbol > symbols
Definition document.h:29
yoi::wstr text
Definition document.h:27
ProjectIndex * projectIndex
Definition document.h:33
std::shared_ptr< yoi::IRModule > irModule
Definition document.h:34
bool parseSucceeded
Definition document.h:32
yoi::wstr detail
yoi::indexT line
yoi::wstr name
HoshiSymbolKind kind
yoi::wstr typeInfo