Merge pull request #118598 from Finishxx/gdscript-lsp-color-bbcode-support

GDScript LSP: Convert [color] BBCode to <span> HTML for capable clients
This commit is contained in:
Thaddeus Crews
2026-06-25 08:32:24 -05:00
6 changed files with 110 additions and 42 deletions
@@ -250,6 +250,13 @@ Variant GDScriptLanguageProtocol::initialize(const Dictionary &p_params) {
client->behavior.use_snippets_for_brace_completion = get_deep(capabilities, false,
"textDocument", "completion", "completionItem", "snippetSupport");
Array allowed_tags = get_deep(capabilities, Array(), "general", "markdown", "allowedTags");
for (const Variant &tag : allowed_tags) {
if (tag.is_string()) {
client->behavior.markdown_allowed_html_tags.insert(tag);
}
}
return ret.to_json();
}
@@ -440,6 +447,12 @@ ExtendGDScriptParser *GDScriptLanguageProtocol::get_parse_result(const String &p
return *cached_parser;
}
const HashSet<String> &GDScriptLanguageProtocol::get_client_markdown_allowed_html_tags() const {
static const HashSet<String> default_tags = {};
LSP_CLIENT_V(default_tags);
return client->behavior.markdown_allowed_html_tags;
}
void GDScriptLanguageProtocol::lsp_did_open(const Dictionary &p_params) {
LSP_CLIENT;
@@ -53,6 +53,8 @@ public:
struct ClientBehavior {
/** If `true` use snippet insert mode to position the cursor between braces of completion options. If `false` strip braces from completion options since we can't provide good UX for them. */
bool use_snippets_for_brace_completion = false;
/** HTML tags, e.g. `span`, which the client is capable of rendering inside Markdown content. Assumes full support for attributes e.g. `style`. */
HashSet<String> markdown_allowed_html_tags;
};
private:
@@ -167,6 +169,14 @@ public:
*/
ExtendGDScriptParser *get_parse_result(const String &p_path);
/**
* Returns the HTML tags the client can render inside Markdown content, e.g. `span`.
* Defaults to an empty set if no client is connected.
*
* TODO: Remove after moving endpoints into unified class. Access non-null client directly.
*/
const HashSet<String> &get_client_markdown_allowed_html_tags() const;
GDScriptLanguageProtocol();
~GDScriptLanguageProtocol() {
clients.clear();
@@ -244,7 +244,8 @@ Dictionary GDScriptTextDocument::resolve(const Dictionary &p_params) {
}
if (symbol) {
item.documentation = symbol->render();
const HashSet<String> &allowed_tags = GDScriptLanguageProtocol::get_singleton()->get_client_markdown_allowed_html_tags();
item.documentation = symbol->render(allowed_tags);
}
if (item.kind == LSP::CompletionItemKind::Event) {
@@ -299,7 +300,8 @@ Variant GDScriptTextDocument::hover(const Dictionary &p_params) {
const LSP::DocumentSymbol *symbol = GDScriptLanguageProtocol::get_singleton()->get_workspace()->resolve_symbol(params);
if (symbol) {
LSP::Hover hover;
hover.contents = symbol->render();
const HashSet<String> &allowed_tags = GDScriptLanguageProtocol::get_singleton()->get_client_markdown_allowed_html_tags();
hover.contents = symbol->render(allowed_tags);
hover.range.start = params.position;
hover.range.end = params.position;
return hover.to_json();
@@ -309,9 +311,10 @@ Variant GDScriptTextDocument::hover(const Dictionary &p_params) {
Array contents;
List<const LSP::DocumentSymbol *> list;
GDScriptLanguageProtocol::get_singleton()->resolve_related_symbols(params, list);
const HashSet<String> &allowed_tags = GDScriptLanguageProtocol::get_singleton()->get_client_markdown_allowed_html_tags();
for (const LSP::DocumentSymbol *&E : list) {
if (const LSP::DocumentSymbol *s = E) {
contents.push_back(s->render().value);
contents.push_back(s->render(allowed_tags).value);
}
}
ret["contents"] = contents;
@@ -771,7 +771,8 @@ Error GDScriptWorkspace::resolve_signature(const LSP::TextDocumentPositionParams
if (symbol->kind == LSP::SymbolKind::Method || symbol->kind == LSP::SymbolKind::Function) {
LSP::SignatureInformation signature_info;
signature_info.label = symbol->detail;
signature_info.documentation = symbol->render();
const HashSet<String> &allowed_tags = GDScriptLanguageProtocol::get_singleton()->get_client_markdown_allowed_html_tags();
signature_info.documentation = symbol->render(allowed_tags);
for (int i = 0; i < symbol->children.size(); i++) {
const LSP::DocumentSymbol &arg = symbol->children[i];
+36 -7
View File
@@ -48,7 +48,7 @@ namespace LSP {
typedef String DocumentUri;
/** Format BBCode documentation from DocData to markdown */
static String marked_documentation(const String &p_bbcode);
static String marked_documentation(const String &p_bbcode, const HashSet<String> &p_allowed_html_tags);
/**
* Text documents are identified using a URI. On the protocol level, URIs are passed as strings.
@@ -1306,13 +1306,13 @@ struct DocumentSymbol {
return dict;
}
_FORCE_INLINE_ MarkupContent render() const {
_FORCE_INLINE_ MarkupContent render(const HashSet<String> &p_allowed_html_tags) const {
MarkupContent markdown;
if (detail.length()) {
markdown.value = "\t" + detail + "\n\n";
}
if (documentation.length()) {
markdown.value += marked_documentation(documentation) + "\n\n";
markdown.value += marked_documentation(documentation, p_allowed_html_tags) + "\n\n";
}
if (script_path.length()) {
markdown.value += "Defined in [" + script_path + "](" + uri + ")";
@@ -1916,7 +1916,8 @@ struct GodotCapabilities {
};
/** Format BBCode documentation from DocData to markdown */
static String marked_documentation(const String &p_bbcode) {
static String marked_documentation(const String &p_bbcode, const HashSet<String> &p_allowed_html_tags) {
bool span_allowed = p_allowed_html_tags.has("span");
String markdown = p_bbcode.strip_edges();
Vector<String> lines = markdown.split("\n");
@@ -1996,7 +1997,6 @@ static String marked_documentation(const String &p_bbcode) {
line = line.replace("[center]", "");
line = line.replace("[/center]", "");
line = line.replace("[/font]", "");
line = line.replace("[/color]", "");
line = line.replace("[/img]", "");
// Convert remaining simple bracketed class names to backticks and literal brackets.
@@ -2069,6 +2069,35 @@ static String marked_documentation(const String &p_bbcode) {
pos += replacement.length();
}
// Convert [color] BBCode to <span>, if the client is capable of rendering it, strip it otherwise.
pos = 0;
while ((pos = line.find("[color=", pos)) != -1) {
constexpr int COLOR_OPEN_TAG_LENGTH = 7; // Length of "[color=".
constexpr int COLOR_CLOSE_TAG_LENGTH = 8; // Length of "[/color]".
int color_end = line.find_char(']', pos);
int close_start = line.find("[/color]", color_end);
if (color_end == -1 || close_start == -1) {
break;
}
String text = line.substr(color_end + 1, close_start - color_end - 1);
String replacement;
if (span_allowed) {
const String color = line.substr(pos + COLOR_OPEN_TAG_LENGTH, color_end - pos - COLOR_OPEN_TAG_LENGTH).strip_edges();
if (Color::html_is_valid(color)) {
replacement = "<span style=\"color:" + color + "\">" + text + "</span>";
} else if (int named_color_index = Color::find_named_color(color); named_color_index != -1) {
replacement = "<span style=\"color:#" + Color::get_named_color(named_color_index).to_html(false) + "\">" + text + "</span>";
}
}
// If no span replacement was produced (client can't render spans, or invalid color), just keep the inner text.
if (replacement.is_empty()) {
replacement = text;
}
line = line.substr(0, pos) + replacement + line.substr(close_start + COLOR_CLOSE_TAG_LENGTH);
pos += replacement.length();
}
// Replace the various link types with inline code ([class MyNode] to `MyNode`).
// Uses a while loop because there can occasionally be multiple links of the same type in a single line.
const Vector<String> link_start_patterns = {
@@ -2090,10 +2119,10 @@ static String marked_documentation(const String &p_bbcode) {
}
}
// Remove tags with attributes like [color=red], as they don't have a direct Markdown
// Remove tags with attributes like [font=Arial], as they don't have a direct Markdown
// equivalent supported by external tools.
const String attribute_tags[] = {
"color", "font", "img"
"font", "img"
};
for (const String &tag_name : attribute_tags) {
int tag_pos = 0;