diff options
| -rw-r--r-- | source/compiler-core/slang-language-server-protocol.cpp | 38 | ||||
| -rw-r--r-- | source/compiler-core/slang-language-server-protocol.h | 83 | ||||
| -rw-r--r-- | source/core/slang-io.h | 6 | ||||
| -rw-r--r-- | source/core/slang-string.cpp | 15 | ||||
| -rw-r--r-- | source/core/slang-string.h | 1 | ||||
| -rw-r--r-- | source/slang/slang-language-server-completion.cpp | 297 | ||||
| -rw-r--r-- | source/slang/slang-language-server-completion.h | 14 | ||||
| -rw-r--r-- | source/slang/slang-language-server.cpp | 127 | ||||
| -rw-r--r-- | source/slang/slang-language-server.h | 5 | ||||
| -rw-r--r-- | source/slang/slang-workspace-version.cpp | 3 |
10 files changed, 528 insertions, 61 deletions
diff --git a/source/compiler-core/slang-language-server-protocol.cpp b/source/compiler-core/slang-language-server-protocol.cpp index d2950b164..628baf08d 100644 --- a/source/compiler-core/slang-language-server-protocol.cpp +++ b/source/compiler-core/slang-language-server-protocol.cpp @@ -150,6 +150,17 @@ static const StructRttiInfo _makeRangeRtti() } const StructRttiInfo Range::g_rttiInfo = _makeRangeRtti(); +static const StructRttiInfo _makeTextEditRtti() +{ + TextEdit obj; + StructRttiBuilder builder(&obj, "LanguageServerProtocol::TextEdit", nullptr); + builder.addField("range", &obj.range); + builder.addField("newText", &obj.newText); + builder.ignoreUnknownFields(); + return builder.make(); +} +const StructRttiInfo TextEdit::g_rttiInfo = _makeTextEditRtti(); + static const StructRttiInfo _makeDidOpenTextDocumentRtti() { DidOpenTextDocumentParams obj; @@ -460,6 +471,22 @@ static const StructRttiInfo _makeCompletionItemRtti() } const StructRttiInfo CompletionItem::g_rttiInfo = _makeCompletionItemRtti(); +static const StructRttiInfo _makeTextEditCompletionItemRtti() +{ + TextEditCompletionItem obj; + StructRttiBuilder builder(&obj, "LanguageServerProtocol::TextEditCompletionItem", nullptr); + builder.addField("label", &obj.label, StructRttiInfo::Flag::Optional); + builder.addField("detail", &obj.detail, StructRttiInfo::Flag::Optional); + builder.addField("kind", &obj.kind, StructRttiInfo::Flag::Optional); + builder.addField("documentation", &obj.documentation, StructRttiInfo::Flag::Optional); + builder.addField("data", &obj.data, StructRttiInfo::Flag::Optional); + builder.addField("textEdit", &obj.textEdit, StructRttiInfo::Flag::Optional); + builder.addField("commitCharacters", &obj.commitCharacters, StructRttiInfo::Flag::Optional); + builder.ignoreUnknownFields(); + return builder.make(); +} +const StructRttiInfo TextEditCompletionItem::g_rttiInfo = _makeTextEditCompletionItemRtti(); + static const StructRttiInfo _makeSemanticTokensParamsRtti() { SemanticTokensParams obj; @@ -641,17 +668,6 @@ static const StructRttiInfo _makeDocumentSymbolRtti() } const StructRttiInfo DocumentSymbol::g_rttiInfo = _makeDocumentSymbolRtti(); -static const StructRttiInfo _makeTextEditRtti() -{ - TextEdit obj; - StructRttiBuilder builder(&obj, "LanguageServerProtocol::TextEdit", nullptr); - builder.addField("range", &obj.range); - builder.addField("newText", &obj.newText); - builder.ignoreUnknownFields(); - return builder.make(); -} -const StructRttiInfo TextEdit::g_rttiInfo = _makeTextEditRtti(); - static const StructRttiInfo _makeInlayHintParamsRtti() { InlayHintParams obj; diff --git a/source/compiler-core/slang-language-server-protocol.h b/source/compiler-core/slang-language-server-protocol.h index fbc79acca..316c15c04 100644 --- a/source/compiler-core/slang-language-server-protocol.h +++ b/source/compiler-core/slang-language-server-protocol.h @@ -176,6 +176,24 @@ struct Range static const StructRttiInfo g_rttiInfo; }; +struct TextEdit +{ + /** + * The range of the text document to be manipulated. To insert + * text into a document create a range where start === end. + */ + Range range; + + /** + * The string to be inserted. For delete operations use an + * empty string. + */ + String newText; + + static const StructRttiInfo g_rttiInfo; + +}; + struct DidOpenTextDocumentParams { TextDocumentItem textDocument; @@ -605,6 +623,53 @@ struct CompletionItem static const StructRttiInfo g_rttiInfo; }; +struct TextEditCompletionItem +{ + /** + * The label of this completion item. + * + * The label property is also by default the text that + * is inserted when selecting this completion. + * + * If label details are provided the label itself should + * be an unqualified name of the completion item. + */ + String label; + + /** + * The kind of this completion item. Based of the kind + * an icon is chosen by the editor. The standardized set + * of available values is defined in `CompletionItemKind`. + */ + CompletionItemKind kind = CompletionItemKind(0); + + /** + * A human-readable string with additional information + * about this item, like type or symbol information. + */ + String detail; + + /** + * A human-readable string that represents a doc-comment. + */ + MarkupContent documentation; + + TextEdit textEdit; + + /** + * An optional set of characters that when pressed while this completion is + * active will accept it first and then type that character. *Note* that all + * commit characters should have `length=1` and that superfluous characters + * will be ignored. + */ + List<String> commitCharacters; + + // Additional data. + String data; + + static const StructRttiInfo g_rttiInfo; +}; + struct SemanticTokensParams : WorkDoneProgressParams { TextDocumentIdentifier textDocument; @@ -921,24 +986,6 @@ struct InlayHintParams static const UnownedStringSlice methodName; }; -struct TextEdit -{ - /** - * The range of the text document to be manipulated. To insert - * text into a document create a range where start === end. - */ - Range range; - - /** - * The string to be inserted. For delete operations use an - * empty string. - */ - String newText; - - static const StructRttiInfo g_rttiInfo; - -}; - typedef int InlayHintKind; const int kInlayHintKindType = 1; const int kInlayHintKindParameter = 2; diff --git a/source/core/slang-io.h b/source/core/slang-io.h index 43812aaf9..4de246289 100644 --- a/source/core/slang-io.h +++ b/source/core/slang-io.h @@ -63,6 +63,12 @@ namespace Slang static const char kPathDelimiter = '/'; +#if SLANG_WINDOWS_FAMILY + static const char kOSCanonicalPathDelimiter = '\\'; +#else + static const char kOSCanonicalPathDelimiter = '/'; +#endif + /// Finds all all the items in the specified directory, that matches the pattern. /// /// @param directoryPath The directory to do the search in. If the directory is not found, SLANG_E_NOT_FOUND is returned diff --git a/source/core/slang-string.cpp b/source/core/slang-string.cpp index 03de07ee1..c02ba048d 100644 --- a/source/core/slang-string.cpp +++ b/source/core/slang-string.cpp @@ -134,7 +134,7 @@ namespace Slang if (otherSize > thisSize) return false; - return UnownedStringSlice(begin(), begin() + otherSize) == other; + return head(otherSize) == other; } bool UnownedStringSlice::startsWith(char const* str) const @@ -142,6 +142,17 @@ namespace Slang return startsWith(UnownedTerminatedStringSlice(str)); } + bool UnownedStringSlice::startsWithCaseInsensitive(UnownedStringSlice const& other) const + { + UInt thisSize = getLength(); + UInt otherSize = other.getLength(); + + if (otherSize > thisSize) + return false; + + return head(otherSize).caseInsensitiveEquals(other); + } + bool UnownedStringSlice::endsWith(UnownedStringSlice const& other) const { @@ -474,6 +485,8 @@ namespace Slang { auto oldLength = getLength(); auto textLength = textEnd - textBegin; + if (textLength <= 0) + return; auto newLength = oldLength + textLength; diff --git a/source/core/slang-string.h b/source/core/slang-string.h index 59d441b76..69ca7e2c3 100644 --- a/source/core/slang-string.h +++ b/source/core/slang-string.h @@ -154,6 +154,7 @@ namespace Slang /// True if contents is a single char of c SLANG_FORCE_INLINE bool isChar(char c) const { return getLength() == 1 && m_begin[0] == c; } + bool startsWithCaseInsensitive(UnownedStringSlice const& other) const; bool startsWith(UnownedStringSlice const& other) const; bool startsWith(char const* str) const; diff --git a/source/slang/slang-language-server-completion.cpp b/source/slang/slang-language-server-completion.cpp index 5eee1dba7..adb30df6e 100644 --- a/source/slang/slang-language-server-completion.cpp +++ b/source/slang/slang-language-server-completion.cpp @@ -8,6 +8,9 @@ #include "slang-check-impl.h" #include "slang-syntax.h" +#include "../core/slang-char-util.h" +#include <chrono> + namespace Slang { @@ -94,6 +97,300 @@ SlangResult CompletionContext::tryCompleteAttributes() return SLANG_OK; } +List<LanguageServerProtocol::TextEditCompletionItem> CompletionContext::gatherFileAndModuleCompletionItems( + const String& prefixPath, + bool translateModuleName, + bool isImportString, + Index lineIndex, + Index fileNameEnd, + Index sectionStart, + Index sectionEnd, + char closingChar) +{ + struct FileEnumerationContext + { + List<LanguageServerProtocol::TextEditCompletionItem> items; + HashSet<String> itemSet; + CompletionContext* completionContext; + String path; + String workspaceRoot; + bool translateModuleName; + bool isImportString; + } context; + context.completionContext = this; + context.translateModuleName = translateModuleName; + context.isImportString = isImportString; + if (version->workspace->rootDirectories.getCount()) + context.workspaceRoot = version->workspace->rootDirectories[0]; + if (context.workspaceRoot.getLength() && + context.workspaceRoot[context.workspaceRoot.getLength() - 1] != + Path::kOSCanonicalPathDelimiter) + { + context.workspaceRoot = context.workspaceRoot + String(Path::kOSCanonicalPathDelimiter); + } + + auto addCandidate = [&](const String& path) + { + context.path = path; + if (path.getUnownedSlice().endsWithCaseInsensitive(prefixPath.getUnownedSlice())) + { + OSFileSystem::getExtSingleton()->enumeratePathContents( + path.getBuffer(), + [](SlangPathType pathType, const char* name, void* userData) + { + FileEnumerationContext* context = (FileEnumerationContext*)userData; + LanguageServerProtocol::TextEditCompletionItem item; + if (pathType == SLANG_PATH_TYPE_DIRECTORY) + { + item.label = name; + item.kind = LanguageServerProtocol::kCompletionItemKindFolder; + if (item.label.indexOf('.') != -1) + return; + } + else + { + auto nameSlice = UnownedStringSlice(name); + if (context->isImportString || context->translateModuleName) + { + if (!nameSlice.endsWithCaseInsensitive(".slang")) + return; + } + StringBuilder nameSB; + auto fileName = UnownedStringSlice(name); + if (context->translateModuleName || context->isImportString) + fileName = fileName.head(nameSlice.getLength() - 6); + for (auto ch : fileName) + { + if (context->translateModuleName) + { + switch (ch) + { + case '-': + nameSB.appendChar('_'); + break; + case '.': + // Ignore any file items that contains a "." + return; + default: + nameSB.appendChar(ch); + break; + } + } + else + { + nameSB.appendChar(ch); + } + } + item.label = nameSB.ProduceString(); + item.kind = LanguageServerProtocol::kCompletionItemKindFile; + } + if (item.label.getLength()) + { + auto key = String(item.kind) + item.label; + if (context->itemSet.Add(key)) + { + item.detail = Path::combine(context->path, String(name)); + Path::getCanonical(item.detail, item.detail); + + if (item.detail.getUnownedSlice().startsWithCaseInsensitive(context->workspaceRoot.getUnownedSlice())) + { + item.detail = item.detail.getUnownedSlice().tail(context->workspaceRoot.getLength()); + } + context->items.add(item); + } + } + }, + &context); + } + }; + + // A big workspace may take a long time to enumerate, thus we limit the amount + // of time allowed to scan the file directory. + + auto startTime = std::chrono::high_resolution_clock::now(); + bool isIncomplete = false; + + for (auto& searchPath : this->version->workspace->additionalSearchPaths) + { + auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - startTime).count(); + if (elapsedTime > 200) + { + isIncomplete = true; + break; + } + addCandidate(searchPath); + } + if (this->version->workspace->searchInWorkspace) + { + for (auto& searchPath : this->version->workspace->workspaceSearchPaths) + { + auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - startTime).count(); + if (elapsedTime > 200) + { + isIncomplete = true; + break; + } + addCandidate(searchPath); + } + } + for (auto& item : context.items) + { + item.textEdit.range.start.line = (int)lineIndex; + item.textEdit.range.end.line = (int)lineIndex; + if (!translateModuleName && item.kind == LanguageServerProtocol::kCompletionItemKindFile) + { + item.textEdit.range.start.character = (int)sectionStart; + item.textEdit.range.end.character = (int)fileNameEnd; + item.textEdit.newText = item.label; + if (closingChar) + item.textEdit.newText.appendChar(closingChar); + } + else + { + item.textEdit.newText = item.label; + item.textEdit.range.start.character = (int)sectionStart; + item.textEdit.range.end.character = (int)sectionEnd; + } + } + + if (commitCharacterBehavior != CommitCharacterBehavior::Disabled && !isIncomplete) + { + for (auto& item : context.items) + { + for (auto ch : getCommitChars()) + item.commitCharacters.add(ch); + } + } + return context.items; +} + +SlangResult CompletionContext::tryCompleteImport() +{ + static auto importStr = UnownedStringSlice("import "); + auto lineContent = doc->getLine(line); + Index pos = lineContent.indexOf(importStr); + if (pos == -1) + return SLANG_FAIL; + auto lineBeforeImportKeyword = lineContent.head(pos).trim(); + if (lineBeforeImportKeyword.getLength() != 0 && lineBeforeImportKeyword != "__exported") + return SLANG_FAIL; + + pos += importStr.getLength(); + while (pos < lineContent.getLength() && pos < col - 1 && CharUtil::isWhitespace(lineContent[pos])) + pos++; + if (pos < lineContent.getLength() && lineContent[pos] == '"') + { + return tryCompleteRawFileName(lineContent, pos, true); + } + + StringBuilder prefixSB; + Index lastPos = col - 2; + if (lastPos < 0) + return SLANG_FAIL; + while (lastPos >= pos && lineContent[lastPos] != '.') + { + if (lineContent[lastPos] == ';') + return SLANG_FAIL; + lastPos--; + } + UnownedStringSlice prefixSlice; + if (lastPos > pos) + prefixSlice = lineContent.subString(pos, lastPos - pos); + Index sectionEnd = col - 1; + while (sectionEnd < lineContent.getLength() && (lineContent[sectionEnd] != '.' && lineContent[sectionEnd] != ';')) + sectionEnd++; + Index fileNameEnd = sectionEnd; + while (fileNameEnd < lineContent.getLength() && lineContent[fileNameEnd] != ';') + fileNameEnd++; + for (auto ch : prefixSlice) + { + if (ch == '.') + prefixSB.appendChar(Path::kOSCanonicalPathDelimiter); + else if (ch == '_') + prefixSB.appendChar('-'); + else + prefixSB.appendChar(ch); + } + auto prefix = prefixSB.ProduceString(); + auto items = gatherFileAndModuleCompletionItems( + prefix, true, false, line - 1, fileNameEnd, lastPos + 1, sectionEnd, 0); + server->m_connection->sendResult(&items, responseId); + return SLANG_OK; +} + +SlangResult CompletionContext::tryCompleteRawFileName(UnownedStringSlice lineContent, Index pos, bool isImportString) +{ + while (pos < lineContent.getLength() && (lineContent[pos] != '\"' && lineContent[pos] != '<')) + pos++; + char closingChar = '"'; + if (pos < lineContent.getLength() && lineContent[pos] == '<') + closingChar = '>'; + pos++; + StringBuilder prefixSB; + Index lastPos = col - 2; + if (lastPos < 0) + return SLANG_FAIL; + while (lastPos >= pos && (lineContent[lastPos] != '/' && lineContent[lastPos] != '\\')) + { + if (lineContent[lastPos] == '\"' || lineContent[lastPos] == '>') + return SLANG_FAIL; + lastPos--; + } + Index sectionEnd = col - 1; + if (sectionEnd < 0) + return SLANG_FAIL; + while (sectionEnd < lineContent.getLength() && + (lineContent[sectionEnd] != '\"' && lineContent[sectionEnd] != '>' && + lineContent[sectionEnd] != '/' && lineContent[sectionEnd] != '\\')) + { + sectionEnd++; + } + Index fileNameEnd = sectionEnd; + while (fileNameEnd < lineContent.getLength() && lineContent[fileNameEnd] != ';') + fileNameEnd++; + UnownedStringSlice prefixSlice; + if (lastPos > pos) + prefixSlice = lineContent.subString(pos, lastPos - pos); + for (auto ch : prefixSlice) + { + if (ch == '/' || ch == '\\') + prefixSB.appendChar(Path::kOSCanonicalPathDelimiter); + else + prefixSB.appendChar(ch); + } + auto prefix = prefixSB.ProduceString(); + auto items = gatherFileAndModuleCompletionItems( + prefix, + false, + isImportString, + line - 1, + fileNameEnd, + lastPos + 1, + sectionEnd, + closingChar); + server->m_connection->sendResult(&items, responseId); + return SLANG_OK; +} + +SlangResult CompletionContext::tryCompleteInclude() +{ + auto lineContent = doc->getLine(line); + if (!lineContent.startsWith("#")) + return SLANG_FAIL; + + static auto includeStr = UnownedStringSlice("include "); + Index pos = lineContent.indexOf(includeStr); + if (pos == -1) + return SLANG_FAIL; + for (Index i = 1; i < pos; i++) + { + if (!CharUtil::isWhitespace(lineContent[i])) + return SLANG_FAIL; + } + pos += includeStr.getLength(); + return tryCompleteRawFileName(lineContent, pos, false); +} + SlangResult CompletionContext::tryCompleteMemberAndSymbol() { List<LanguageServerProtocol::CompletionItem> items = collectMembersAndSymbols(); diff --git a/source/slang/slang-language-server-completion.h b/source/slang/slang-language-server-completion.h index 8c0c782ee..45082e0d3 100644 --- a/source/slang/slang-language-server-completion.h +++ b/source/slang/slang-language-server-completion.h @@ -31,10 +31,24 @@ struct CompletionContext SlangResult tryCompleteMemberAndSymbol(); SlangResult tryCompleteHLSLSemantic(); SlangResult tryCompleteAttributes(); + SlangResult tryCompleteImport(); + SlangResult tryCompleteInclude(); + SlangResult tryCompleteRawFileName(UnownedStringSlice lineContent, Index fileNameStartPos, bool isImportString); + + List<LanguageServerProtocol::CompletionItem> collectMembersAndSymbols(); List<LanguageServerProtocol::CompletionItem> createSwizzleCandidates( Type* baseType, IntegerLiteralValue elementCount[2]); List<LanguageServerProtocol::CompletionItem> collectAttributes(); + List<LanguageServerProtocol::TextEditCompletionItem> gatherFileAndModuleCompletionItems( + const String& prefixPath, + bool translateModuleName, + bool isImportString, + Index lineIndex, + Index fileNameEnd, + Index sectionStart, + Index sectionEnd, + char closingChar); }; } // namespace Slang diff --git a/source/slang/slang-language-server.cpp b/source/slang/slang-language-server.cpp index 7dc2f6dcc..673b91593 100644 --- a/source/slang/slang-language-server.cpp +++ b/source/slang/slang-language-server.cpp @@ -123,6 +123,8 @@ SlangResult LanguageServer::parseNextMessage() result.capabilities.completionProvider.triggerCharacters.add("."); result.capabilities.completionProvider.triggerCharacters.add(":"); result.capabilities.completionProvider.triggerCharacters.add("["); + result.capabilities.completionProvider.triggerCharacters.add("\""); + result.capabilities.completionProvider.triggerCharacters.add("/"); result.capabilities.completionProvider.resolveProvider = true; result.capabilities.completionProvider.workDoneToken = ""; result.capabilities.semanticTokensProvider.full = true; @@ -347,6 +349,26 @@ void appendDefinitionLocation(StringBuilder& sb, Workspace* workspace, const Hum sb << "Defined in " << pathSlice << "(" << loc.line << ")\n"; } +HumaneSourceLoc getModuleLoc(SourceManager* manager, ModuleDecl* moduleDecl) +{ + if (moduleDecl) + { + if (moduleDecl->members.getCount() && + moduleDecl->members[0]) + { + auto loc = moduleDecl->members[0]->loc; + if (loc.isValid()) + { + auto location = manager->getHumaneLoc(loc, SourceLocType::Actual); + location.line = 1; + location.column = 1; + return location; + } + } + } + return HumaneSourceLoc(); +} + SlangResult LanguageServer::hover( const LanguageServerProtocol::HoverParams& args, const JSONValue& responseId) { @@ -429,6 +451,27 @@ SlangResult LanguageServer::hover( LookupResultItem& item = overloadedExpr->lookupResult2.item; fillDeclRefHoverInfo(item.declRef); } + else if (auto importDecl = as<ImportDecl>(leafNode)) + { + auto moduleLoc = getModuleLoc(version->linkage->getSourceManager(), importDecl->importedModuleDecl); + if (moduleLoc.pathInfo.hasFoundPath()) + { + String path = moduleLoc.pathInfo.foundPath; + Path::getCanonical(path, path); + sb << path; + auto humaneLoc = version->linkage->getSourceManager()->getHumaneLoc( + importDecl->startLoc, SourceLocType::Actual); + Index utf16Line, utf16Col; + doc->oneBasedUTF8LocToZeroBasedUTF16Loc(humaneLoc.line, humaneLoc.column, utf16Line, utf16Col); + hover.range.start.line = (int)utf16Line; + hover.range.start.character = (int)utf16Col; + humaneLoc = version->linkage->getSourceManager()->getHumaneLoc( + importDecl->endLoc, SourceLocType::Actual); + doc->oneBasedUTF8LocToZeroBasedUTF16Loc(humaneLoc.line, humaneLoc.column, utf16Line, utf16Col); + hover.range.end.line = (int)utf16Line; + hover.range.end.character = (int)utf16Col; + } + } else if (auto decl = as<Decl>(leafNode)) { fillDeclRefHoverInfo(DeclRef<Decl>(decl, nullptr)); @@ -529,18 +572,10 @@ SlangResult LanguageServer::gotoDefinition( } else if (auto importDecl = as<ImportDecl>(leafNode)) { - if (importDecl->importedModuleDecl) + auto location = getModuleLoc(version->linkage->getSourceManager(), importDecl->importedModuleDecl); + if (location.pathInfo.hasFoundPath()) { - if (importDecl->importedModuleDecl->members.getCount() && - importDecl->importedModuleDecl->members[0]) - { - auto loc = importDecl->importedModuleDecl->members[0]->loc; - if (loc.isValid()) - { - auto location = version->linkage->getSourceManager()->getHumaneLoc(loc, SourceLocType::Actual); - locations.add(LocationResult{location, 0}); - } - } + locations.add(LocationResult{ location, 0 }); } } if (locations.getCount() == 0) @@ -630,38 +665,58 @@ SlangResult LanguageServer::completion( return SLANG_OK; } - // Insert a completion request token at cursor position. - auto originalText = doc->getText(); - StringBuilder newText; - newText << originalText.getUnownedSlice().head(cursorOffset + 1) << "#?" - << originalText.getUnownedSlice().tail(cursorOffset + 1); - doc->setText(newText.ProduceString()); - auto restoreDocText = makeDeferred([&]() { doc->setText(originalText); }); - // Always create a new workspace version for the completion request since we // will use a modified source. auto version = m_workspace->createVersionForCompletion(); auto moduleName = getMangledNameFromNameString(canonicalPath.getUnownedSlice()); version->linkage->contentAssistInfo.cursorLine = utf8Line; version->linkage->contentAssistInfo.cursorCol = utf8Col; - Module* parsedModule = version->getOrLoadModule(canonicalPath); - if (!parsedModule) - { - m_connection->sendResult(NullResponse::get(), responseId); - return SLANG_OK; - } - Slang::CompletionContext context; context.server = this; context.cursorOffset = cursorOffset; context.version = version; context.doc = doc.Ptr(); - context.parsedModule = parsedModule; context.responseId = responseId; context.canonicalPath = canonicalPath.getUnownedSlice(); context.line = utf8Line; context.col = utf8Col; context.commitCharacterBehavior = m_commitCharacterBehavior; + + if (SLANG_SUCCEEDED(context.tryCompleteInclude())) + { + return SLANG_OK; + } + if (SLANG_SUCCEEDED(context.tryCompleteImport())) + { + return SLANG_OK; + } + + if (args.context.triggerKind == + LanguageServerProtocol::kCompletionTriggerKindTriggerCharacter && + (args.context.triggerCharacter == "\"" || args.context.triggerCharacter == "/")) + { + // Trigger characters '"' and '/' are for include only. + m_connection->sendResult(NullResponse::get(), responseId); + return SLANG_OK; + } + + + // Insert a completion request token at cursor position. + auto originalText = doc->getText(); + StringBuilder newText; + newText << originalText.getUnownedSlice().head(cursorOffset + 1) << "#?" + << originalText.getUnownedSlice().tail(cursorOffset + 1); + doc->setText(newText.ProduceString()); + auto restoreDocText = makeDeferred([&]() { doc->setText(originalText); }); + + Module* parsedModule = version->getOrLoadModule(canonicalPath); + if (!parsedModule) + { + m_connection->sendResult(NullResponse::get(), responseId); + return SLANG_OK; + } + + context.parsedModule = parsedModule; if (SLANG_SUCCEEDED(context.tryCompleteAttributes())) { return SLANG_OK; @@ -689,8 +744,19 @@ SlangResult LanguageServer::completion( } SlangResult LanguageServer::completionResolve( - const LanguageServerProtocol::CompletionItem& args, const JSONValue& responseId) + const LanguageServerProtocol::CompletionItem& args, const LanguageServerProtocol::TextEditCompletionItem& editItem, const JSONValue& responseId) { + if (args.data.getLength() == 0) + { + if (editItem.textEdit.newText.getLength()) + { + m_connection->sendResult(&editItem, responseId); + return SLANG_OK; + } + m_connection->sendResult(&args, responseId); + return SLANG_OK; + } + LanguageServerProtocol::CompletionItem resolvedItem = args; int itemId = StringToInt(args.data); auto version = m_workspace->getCurrentCompletionVersion(); @@ -1455,6 +1521,9 @@ SlangResult LanguageServer::queueJSONCall(JSONRPCCall call) Slang::LanguageServerProtocol::CompletionItem args; SLANG_RETURN_ON_FAIL(m_connection->toNativeArgsOrSendError(call.params, &args, call.id)); cmd.completionResolveArgs = args; + Slang::LanguageServerProtocol::TextEditCompletionItem editArgs; + SLANG_RETURN_ON_FAIL(m_connection->toNativeArgsOrSendError(call.params, &editArgs, call.id)); + cmd.textEditCompletionResolveArgs = editArgs; } else if (call.method == DocumentSymbolParams::methodName) { @@ -1541,7 +1610,7 @@ SlangResult LanguageServer::runCommand(Command& call) } else if (call.method == "completionItem/resolve") { - return completionResolve(call.completionResolveArgs.get(), call.id); + return completionResolve(call.completionResolveArgs.get(), call.textEditCompletionResolveArgs.get(), call.id); } else if (call.method == DocumentSymbolParams::methodName) { diff --git a/source/slang/slang-language-server.h b/source/slang/slang-language-server.h index 7f297d18b..26894b3bd 100644 --- a/source/slang/slang-language-server.h +++ b/source/slang/slang-language-server.h @@ -58,6 +58,7 @@ struct Command Optional<LanguageServerProtocol::CompletionParams> completionArgs; Optional<LanguageServerProtocol::CompletionItem> completionResolveArgs; + Optional<LanguageServerProtocol::TextEditCompletionItem> textEditCompletionResolveArgs; Optional<LanguageServerProtocol::DocumentSymbolParams> documentSymbolArgs; Optional<LanguageServerProtocol::InlayHintParams> inlayHintArgs; Optional<LanguageServerProtocol::DocumentFormattingParams> formattingArgs; @@ -87,7 +88,7 @@ public: Verbose }; bool m_initialized = false; - TraceOptions m_traceOptions = TraceOptions::Messages; + TraceOptions m_traceOptions = TraceOptions::Off; CommitCharacterBehavior m_commitCharacterBehavior = CommitCharacterBehavior::MembersOnly; RefPtr<JSONRPCConnection> m_connection; ComPtr<slang::IGlobalSession> m_session; @@ -115,7 +116,7 @@ public: SlangResult completion( const LanguageServerProtocol::CompletionParams& args, const JSONValue& responseId); SlangResult completionResolve( - const LanguageServerProtocol::CompletionItem& args, const JSONValue& responseId); + const LanguageServerProtocol::CompletionItem& args, const LanguageServerProtocol::TextEditCompletionItem& editItem, const JSONValue& responseId); SlangResult semanticTokens( const LanguageServerProtocol::SemanticTokensParams& args, const JSONValue& responseId); SlangResult signatureHelp( diff --git a/source/slang/slang-workspace-version.cpp b/source/slang/slang-workspace-version.cpp index f09b8666d..2451290f5 100644 --- a/source/slang/slang-workspace-version.cpp +++ b/source/slang/slang-workspace-version.cpp @@ -167,6 +167,9 @@ void Workspace::init(List<URI> rootDirURI, slang::IGlobalSession* globalSession) auto nameSlice = UnownedStringSlice(name); if (pathType == SLANG_PATH_TYPE_DIRECTORY) { + // Ignore directories starting with '.' + if (nameSlice.getLength() && nameSlice[0] == '.') + return; dirContext->workList.add(Path::combine(dirContext->currentPath, name)); } else if (nameSlice.endsWithCaseInsensitive(".slang") || nameSlice.endsWithCaseInsensitive(".hlsl")) |
