diff --git a/lsp-server/src/core/server.cppm b/lsp-server/src/core/server.cppm index 79ee8b6..dea66d5 100644 --- a/lsp-server/src/core/server.cppm +++ b/lsp-server/src/core/server.cppm @@ -16,6 +16,7 @@ import lsp.manager.manager_hub; import lsp.protocol; import lsp.provider.manifest; import lsp.scheduler.async_executor; +import lsp.utils.text_coordinates; namespace transform = lsp::codec; @@ -753,10 +754,10 @@ namespace lsp::core for (const auto& error : errors) { protocol::Diagnostic diagnostic; - diagnostic.range.start.line = error.location.start_line; - diagnostic.range.start.character = error.location.start_column; - diagnostic.range.end.line = error.location.end_line; - diagnostic.range.end.character = error.location.end_column; + diagnostic.range.start = utils::text_coordinates::ToPosition( + TSPoint{ error.location.start_line, error.location.start_column }, content); + diagnostic.range.end = utils::text_coordinates::ToPosition( + TSPoint{ error.location.end_line, error.location.end_column }, content); switch (error.severity) { diff --git a/lsp-server/src/utils/text_coordinates.cppm b/lsp-server/src/utils/text_coordinates.cppm index 3c0efe6..ea902d0 100644 --- a/lsp-server/src/utils/text_coordinates.cppm +++ b/lsp-server/src/utils/text_coordinates.cppm @@ -16,6 +16,7 @@ export namespace lsp::utils::text_coordinates }; BytePosition ToBytePosition(const protocol::Position& position, const protocol::string& content); + protocol::Position ToPosition(TSPoint point, const protocol::string& content); protocol::uinteger ToOffset(const protocol::Position& position, const protocol::string& content); TSPoint CalculateEndPoint(const protocol::string& text, TSPoint start); } @@ -120,6 +121,37 @@ namespace lsp::utils::text_coordinates }; } + protocol::Position ToPosition(TSPoint point, const protocol::string& content) + { + std::size_t offset = 0; + std::uint32_t row = 0; + + while (offset < content.size() && row < point.row) + { + if (content[offset++] == '\n') + ++row; + } + + protocol::uinteger utf16_units = 0; + std::size_t byte_column = 0; + while (offset < content.size() && content[offset] != '\n' && + byte_column < point.column) + { + const auto decoded = DecodeCharacter(content, offset); + if (decoded.byte_count > point.column - byte_column) + break; + + offset += decoded.byte_count; + byte_column += decoded.byte_count; + utf16_units += decoded.utf16_units; + } + + return { + .line = row, + .character = utf16_units, + }; + } + protocol::uinteger ToOffset(const protocol::Position& position, const protocol::string& content) { return ToBytePosition(position, content).offset; diff --git a/lsp-server/test/test_provider/text_coordinates_test.cppm b/lsp-server/test/test_provider/text_coordinates_test.cppm index f3c737c..26fbcd5 100644 --- a/lsp-server/test/test_provider/text_coordinates_test.cppm +++ b/lsp-server/test/test_provider/text_coordinates_test.cppm @@ -21,6 +21,7 @@ export namespace lsp::test::provider static TestResult TestMultilineAndClampedPositions(); static TestResult TestMalformedUtf8IsBounded(); static TestResult TestCalculateEndPointUsesByteColumns(); + static TestResult TestBytePointsToUtf16Positions(); }; } @@ -37,6 +38,14 @@ namespace lsp::test::provider assertEqual(row, actual.point.row, "Tree-sitter row should match"); assertEqual(column, actual.point.column, "Tree-sitter byte column should match"); } + + void ExpectLspPosition(const protocol::Position& actual, + protocol::uinteger line, + protocol::uinteger character) + { + assertEqual(line, actual.line, "LSP line should match"); + assertEqual(character, actual.character, "LSP UTF-16 character should match"); + } } void TextCoordinatesTests::Register(TestRunner& runner) @@ -46,6 +55,7 @@ namespace lsp::test::provider runner.addTest("text coordinates clamp multiline positions", TestMultilineAndClampedPositions); runner.addTest("text coordinates bound malformed UTF-8", TestMalformedUtf8IsBounded); runner.addTest("text coordinates calculate byte end points", TestCalculateEndPointUsesByteColumns); + runner.addTest("text coordinates convert byte points to UTF-16 positions", TestBytePointsToUtf16Positions); } TestResult TextCoordinatesTests::TestAsciiAndBmpPositions() @@ -93,4 +103,16 @@ namespace lsp::test::provider assertEqual(5U, end.column, "multibyte text should advance the byte column"); return { "", true, "ok" }; } + + TestResult TextCoordinatesTests::TestBytePointsToUtf16Positions() + { + const protocol::string content = "A中😀Z\n😀x"; + + ExpectLspPosition(utils::text_coordinates::ToPosition({ 0U, 0U }, content), 0U, 0U); + ExpectLspPosition(utils::text_coordinates::ToPosition({ 0U, 4U }, content), 0U, 2U); + ExpectLspPosition(utils::text_coordinates::ToPosition({ 0U, 8U }, content), 0U, 4U); + ExpectLspPosition(utils::text_coordinates::ToPosition({ 1U, 4U }, content), 1U, 2U); + ExpectLspPosition(utils::text_coordinates::ToPosition({ 0U, 6U }, content), 0U, 2U); + return { "", true, "ok" }; + } }