From 15b01fe039171c9da381e2b857f8ebcd17e7703f Mon Sep 17 00:00:00 2001 From: 3w36zj6 <52315048+3w36zj6@users.noreply.github.com> Date: Fri, 28 Aug 2026 23:57:06 +0900 Subject: [PATCH] fix: use textlint ranges for diagnostics --- src/server/diagnostics.test.ts | 31 +++++++++++++++++++++++-------- src/server/diagnostics.ts | 26 +++++++------------------- src/server/validation.ts | 2 +- tests/e2e/diagnostics.test.ts | 5 ++++- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/server/diagnostics.test.ts b/src/server/diagnostics.test.ts index 1e3168f..1a114aa 100644 --- a/src/server/diagnostics.test.ts +++ b/src/server/diagnostics.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { describe, test } from "node:test"; import { DiagnosticSeverity } from "vscode-languageserver/node"; +import { TextDocument } from "vscode-languageserver-textdocument"; import { toDiagnostic, toDiagnosticSeverity } from "./diagnostics.ts"; import { textlintMessage } from "./test-fixtures.ts"; @@ -11,17 +12,31 @@ void describe("diagnostic core", () => { assert.strictEqual(toDiagnosticSeverity(0), DiagnosticSeverity.Information); assert.strictEqual(toDiagnosticSeverity(3), DiagnosticSeverity.Information); }); +}); + +void describe("diagnostic ranges", () => { + void test("uses textlint ranges for single-line and zero-width diagnostics", () => { + const textDocument = TextDocument.create("file:///test.txt", "plaintext", 1, "0123456789"); + const word = toDiagnostic(textDocument, textlintMessage("word", [2, 5]))[1]; + const insertion = toDiagnostic(textDocument, textlintMessage("insert", [7, 7]))[1]; + + assert.deepStrictEqual(word.range, { + start: { line: 0, character: 2 }, + end: { line: 0, character: 5 }, + }); + assert.deepStrictEqual(insertion.range, { + start: { line: 0, character: 7 }, + end: { line: 0, character: 7 }, + }); + }); - void test("preserves the current message-based range behavior", () => { - const plain = toDiagnostic(textlintMessage("plain", [2, 5]))[1]; - const arrow = toDiagnostic(textlintMessage("arrow", [2, 5], "arrow", "before -> after"))[1]; - const quoted = toDiagnostic(textlintMessage("quoted", [2, 5], "quoted", 'replace "word"'))[1]; + void test("converts multiline and surrogate-pair offsets with the document", () => { + const textDocument = TextDocument.create("file:///test.txt", "plaintext", 1, "😀abc\ndef"); + const multiline = toDiagnostic(textDocument, textlintMessage("multiline", [2, 8]))[1]; - assert.deepStrictEqual(plain.range, { + assert.deepStrictEqual(multiline.range, { start: { line: 0, character: 2 }, - end: { line: 0, character: 2 }, + end: { line: 1, character: 2 }, }); - assert.strictEqual(arrow.range.end.character, 8); - assert.strictEqual(quoted.range.end.character, 6); }); }); diff --git a/src/server/diagnostics.ts b/src/server/diagnostics.ts index 6e64bf8..611bccc 100644 --- a/src/server/diagnostics.ts +++ b/src/server/diagnostics.ts @@ -1,5 +1,6 @@ -import { DiagnosticSeverity, Position, Range } from "vscode-languageserver/node"; +import { DiagnosticSeverity, Range } from "vscode-languageserver/node"; import type { Diagnostic } from "vscode-languageserver/node"; +import type { TextDocument } from "vscode-languageserver-textdocument"; import type { TextlintMessage } from "@textlint/types"; export type DiagnosticEntry = readonly [TextlintMessage, Diagnostic]; @@ -18,30 +19,17 @@ export function toDiagnosticSeverity(severity: TextlintMessage["severity"]): Dia } } -export function toDiagnostic(message: TextlintMessage): DiagnosticEntry { - const startPosition = Position.create( - Math.max(0, message.loc.start.line - 1), - Math.max(0, message.loc.start.column - 1), - ); - let offset = 0; - if (message.message.includes("->")) { - offset = message.message.indexOf(" ->"); - } - const quoteIndex = message.message.indexOf(`"`); - if (quoteIndex >= 0) { - offset = Math.max(0, message.message.indexOf(`"`, quoteIndex + 1) - quoteIndex - 1); - } - const endPosition = Position.create( - Math.max(0, message.loc.start.line - 1), - Math.max(0, message.loc.start.column - 1) + offset, - ); +export function toDiagnostic(document: TextDocument, message: TextlintMessage): DiagnosticEntry { return [ message, { message: message.message, severity: toDiagnosticSeverity(message.severity), source: "textlint", - range: Range.create(startPosition, endPosition), + range: Range.create( + document.positionAt(message.range[0]), + document.positionAt(message.range[1]), + ), code: message.ruleId, }, ]; diff --git a/src/server/validation.ts b/src/server/validation.ts index 85af8d6..8cbc1eb 100644 --- a/src/server/validation.ts +++ b/src/server/validation.ts @@ -103,7 +103,7 @@ async function lintDocument( document.uri, document.version, slot, - result.messages.map((message) => toDiagnostic(message)), + result.messages.map((message) => toDiagnostic(document, message)), ); } diff --git a/tests/e2e/diagnostics.test.ts b/tests/e2e/diagnostics.test.ts index 2458976..1b3f232 100644 --- a/tests/e2e/diagnostics.test.ts +++ b/tests/e2e/diagnostics.test.ts @@ -69,7 +69,10 @@ function assertLintDiagnostics(diagnostics: readonly Diagnostic[]): void { line: diagnostic.range.end.line, character: diagnostic.range.end.character, }, - expectedDiagnostics[index], + { + line: expectedDiagnostics[index].line, + character: expectedDiagnostics[index].character + 1, + }, ); } }