Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Added

- Lines prefixed by dollar sign (`$`) in Luau code blocks are now hidden from documentation comments ([#960](https://github.com/JohnnyMorganz/luau-lsp/pull/960)).

## [1.40.0] - 2025-03-01

### Added
Expand Down
145 changes: 117 additions & 28 deletions src/DocumentationParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ std::vector<std::string> WorkspaceFolder::getComments(const Luau::ModuleName& mo
return {};

std::vector<std::string> comments{};
// 0 means not in a code block, otherwise the number of backticks used (minimum 3 for a code block)
size_t inCodeBlock = 0;
bool isCodeBlockLanguageLuau = false;
for (auto& comment : commentLocations)
{
if (comment.type == Luau::Lexeme::Type::BrokenComment)
Expand All @@ -403,7 +406,38 @@ std::vector<std::string> WorkspaceFolder::getComments(const Luau::ModuleName& mo
{
if (Luau::startsWith(commentText, "--- "))
{
comments.emplace_back(commentText.substr(4));
auto line = std::string_view(commentText).substr(4);
if (inCodeBlock > 0)
{
if (isCodeBlockLanguageLuau && !line.empty() && line[0] == '$') continue;
if (Luau::startsWith(line, "```") && line.length() == inCodeBlock)
{
auto firstNonBacktick = line.find_first_not_of('`', 3);
if (firstNonBacktick == std::string::npos)
{
inCodeBlock = 0;
isCodeBlockLanguageLuau = false;
}
}
}
else if (Luau::startsWith(line, "```"))
{
auto firstNonBacktick = line.find_first_not_of('`', 3);
if (firstNonBacktick == std::string::npos)
{
inCodeBlock = line.length();
}
else
{
inCodeBlock = firstNonBacktick;
auto firstWordBeginning = line.find_first_not_of(" \n\r\t", firstNonBacktick);
auto firstWord = firstWordBeginning == std::string::npos ? std::string_view{} : line.substr(firstWordBeginning);
auto firstWordEnd = firstWord.find_first_of(" \n\r\t");
firstWord = firstWord.substr(0, firstWordEnd);
if (firstWord == "luau") isCodeBlockLanguageLuau = true;
}
}
comments.emplace_back(line);
}
else if (commentText == "---")
{
Expand All @@ -412,43 +446,98 @@ std::vector<std::string> WorkspaceFolder::getComments(const Luau::ModuleName& mo
}
else if (comment.type == Luau::Lexeme::Type::BlockComment)
{
std::regex comment_regex("^--\\[(=*)\\[");
std::smatch comment_match;
// This is a block comment, which always starts with --[[ and ends with ]] (6 characters at least).
// If the closing sequence is missing, the comment is considered broken (`Luau::Lexeme::Type::BrokenComment`), which isn't the case here.
LUAU_ASSERT(commentText.length() >= 6);

size_t commentWidth = commentText.find_first_not_of('=', 3) - 3;

auto commentWithNoStartAndEnd = std::string_view(commentText).substr(
// Skip --[[ and any '=' signs
4 + commentWidth,
// The final length is the total comment length minus --[[ and ]] (6 characters) and any '=' signs, which are repeated at both the start and end.
commentText.length() - 6 - 2 * commentWidth
);

size_t firstNonSpaceCharacter = commentWithNoStartAndEnd.find_first_not_of(" \r\t");

if (std::regex_search(commentText, comment_match, comment_regex))
if (firstNonSpaceCharacter == std::string::npos) continue;
if (commentWithNoStartAndEnd[firstNonSpaceCharacter] == '\n')
{
LUAU_ASSERT(comment_match.size() == 2);
// Construct "--[=[" and "--]=]" which will be ignored
std::string start_string = "--[" + std::string(comment_match[1].length(), '=') + "[";
std::string end_string = "]" + std::string(comment_match[1].length(), '=') + "]";
if (commentWithNoStartAndEnd.length() == firstNonSpaceCharacter + 1) continue;
commentWithNoStartAndEnd = commentWithNoStartAndEnd.substr(firstNonSpaceCharacter + 1);
}

size_t lastNonSpaceCharacter = commentWithNoStartAndEnd.find_last_not_of(" \r\t");

// Parse each line separately
for (auto& line : Luau::split(commentText, '\n'))
if (lastNonSpaceCharacter == std::string::npos) continue;
if (commentWithNoStartAndEnd[lastNonSpaceCharacter] == '\n')
{
if (lastNonSpaceCharacter == 0) continue;
lastNonSpaceCharacter = commentWithNoStartAndEnd.find_last_not_of(" \n\r\t", lastNonSpaceCharacter);
if (lastNonSpaceCharacter == std::string::npos) continue;
commentWithNoStartAndEnd = commentWithNoStartAndEnd.substr(0, lastNonSpaceCharacter + 1);
}

// Parse each line separately
auto lines = Luau::split(commentWithNoStartAndEnd, '\n');

// Trim common indentation, but ignore empty lines
size_t indentLevel = std::string::npos;

for (auto& line : lines)
{
auto lastNonSpace = line.find_last_not_of(" \n\r\t");
if (lastNonSpace == std::string::npos)
{
line = std::string_view{};
continue;
}
else
{
auto lineText = std::string(line);
trim_end(lineText);

auto trimmedLineText = std::string(line);
trim(trimmedLineText);
if (trimmedLineText == start_string || trimmedLineText == end_string)
continue;
comments.emplace_back(lineText);
line = line.substr(0, lastNonSpace + 1);
}
indentLevel = std::min(indentLevel, line.find_first_not_of(" \n\r\t"));
}

for (auto& line : lines)
{
if (!line.empty()) line = line.substr(indentLevel);
}

// Trim common indentation, but ignore empty lines
size_t indentLevel = std::string::npos;
for (auto& line : comments)
for (auto& line : lines)
{
if (inCodeBlock > 0)
{
if (line == "")
continue;
indentLevel = std::min(indentLevel, line.find_first_not_of(" \n\r\t"));
if (isCodeBlockLanguageLuau && !line.empty() && line[0] == '$') continue;
if (Luau::startsWith(line, "```") && line.length() == inCodeBlock)
{
auto firstNonBacktick = line.find_first_not_of('`', 3);
if (firstNonBacktick == std::string::npos)
{
inCodeBlock = 0;
isCodeBlockLanguageLuau = false;
}
}
}
for (auto& line : comments)
else if (Luau::startsWith(line, "```"))
{
if (line == "")
continue;
line.erase(0, indentLevel);
auto firstNonBacktick = line.find_first_not_of('`', 3);
if (firstNonBacktick == std::string::npos)
{
inCodeBlock = line.length();
}
else
{
inCodeBlock = firstNonBacktick;
auto firstWordBeginning = line.find_first_not_of(" \n\r\t", firstNonBacktick);
auto firstWord = firstWordBeginning == std::string::npos ? std::string_view{} : line.substr(firstWordBeginning);
auto firstWordEnd = firstWord.find_first_of(" \n\r\t");
firstWord = firstWord.substr(0, firstWordEnd);
if (firstWord == "luau") isCodeBlockLanguageLuau = true;
}
}
comments.emplace_back(line);
}
}
}
Expand Down
98 changes: 98 additions & 0 deletions tests/Documentation.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -506,4 +506,102 @@ TEST_CASE_FIXTURE(Fixture, "singleline_comments_preserve_newlines")
CHECK_EQ("A sample class.", comments[2]);
}

TEST_CASE_FIXTURE(Fixture, "hide_lines_prefixed_by_dollar_sign_in_luau_code_blocks")
{
auto result = check(R"(
--- ```php
--- $should_still_work = "because we are not in a `luau` code block";
--- ```
--- ````luau
--- ```
--- $ ↑ despite it being incorrect syntax,
--- $ it shouldn't do anything, because we started with 4 backticks
--- print("Hello, world!")
--- ````shouldn't do anything
--- $ ↑ that also shouldn't do anything, because we only close a code block with exactly ````.
--- $ this ↓ has 4 spaces after ````, but it shouldn't matter (whitespace is trimmed) and it should still close.
--- ````
--- $ this shouldn't be hidden, since we're not in a code block anymore
--- ```
--- $ this shouldn't be hidden
--- ```
local MyClass = {}
)");

REQUIRE_EQ(0, result.errors.size());

auto ty = requireType("MyClass");
auto ttv = Luau::get<Luau::TableType>(ty);
REQUIRE(ttv);

auto comments = getComments(ttv->definitionLocation);
REQUIRE_EQ(12, comments.size());

CHECK_EQ("```php", comments[0]);
CHECK_EQ("$should_still_work = \"because we are not in a `luau` code block\";", comments[1]);
CHECK_EQ("```", comments[2]);

CHECK_EQ("````luau", comments[3]);
CHECK_EQ("```", comments[4]);
CHECK_EQ("print(\"Hello, world!\")", comments[5]);
CHECK_EQ("````shouldn't do anything", comments[6]);
CHECK_EQ("````", comments[7]);

CHECK_EQ("$ this shouldn't be hidden, since we're not in a code block anymore", comments[8]);

CHECK_EQ("```", comments[9]);
CHECK_EQ("$ this shouldn't be hidden", comments[10]);
CHECK_EQ("```", comments[11]);
}

TEST_CASE_FIXTURE(Fixture, "hide_lines_prefixed_by_dollar_sign_in_luau_code_blocks_multiline")
{
auto result = check(R"(
--[[
```php
$should_still_work = "because we are not in a `luau` code block";
```
````luau
```
$ ↑ despite it being incorrect syntax,
$ it shouldn't do anything, because we started with 4 backticks
print("Hello, world!")
````shouldn't do anything
$ ↑ that also shouldn't do anything, because we only close a code block with exactly ````.
$ this ↓ has 4 spaces after ````, but it shouldn't matter (whitespace is trimmed) and it should still close.
````
$ this shouldn't be hidden, since we're not in a code block anymore
```
$ this shouldn't be hidden
```
]]
local MyClass = {}
)");

REQUIRE_EQ(0, result.errors.size());

auto ty = requireType("MyClass");
auto ttv = Luau::get<Luau::TableType>(ty);
REQUIRE(ttv);

auto comments = getComments(ttv->definitionLocation);
REQUIRE_EQ(12, comments.size());

CHECK_EQ("```php", comments[0]);
CHECK_EQ("$should_still_work = \"because we are not in a `luau` code block\";", comments[1]);
CHECK_EQ("```", comments[2]);

CHECK_EQ("````luau", comments[3]);
CHECK_EQ("```", comments[4]);
CHECK_EQ("print(\"Hello, world!\")", comments[5]);
CHECK_EQ("````shouldn't do anything", comments[6]);
CHECK_EQ("````", comments[7]);

CHECK_EQ("$ this shouldn't be hidden, since we're not in a code block anymore", comments[8]);

CHECK_EQ("```", comments[9]);
CHECK_EQ("$ this shouldn't be hidden", comments[10]);
CHECK_EQ("```", comments[11]);
}

TEST_SUITE_END();
Loading