diff --git a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index 3c4c75f03dd62..5fcf94c969ce4 100644 --- a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -315,6 +315,20 @@ public static TextSpan RangeToTextSpan(LSP.Range range, SourceText text) { var linePositionSpan = RangeToLinePositionSpan(range); + // Handle the specific case where the end position is exactly one line beyond the document bounds + // and the end character is 0 (start of the non-existent next line). + // This can happen when deleting the last line, where LSP clients are allowed (by the spec) to + // send an end position referencing the start of the next line (which doesn't exist). + if (text.Lines.Count > 0 && + linePositionSpan.End.Line == text.Lines.Count && + linePositionSpan.End.Character == 0) + { + // Clamp the end position to the end of the last line + var lastLine = text.Lines[text.Lines.Count - 1]; + var clampedEnd = new LinePosition(text.Lines.Count - 1, lastLine.End - lastLine.Start); + linePositionSpan = new LinePositionSpan(linePositionSpan.Start, clampedEnd); + } + try { try diff --git a/src/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs b/src/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs index f9b1ff4718416..b6ddc8d443996 100644 --- a/src/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs @@ -245,13 +245,48 @@ void M() Assert.Equal(32, textSpan.End); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80119")] + public void RangeToTextSpanDoesNotThrow_WhenReferencingStartOfNextLineAfterLastLine() + { + var markup = GetTestMarkup(); + var sourceText = SourceText.From(markup); + + // The spec allows clients to send a range referencing the start of the next line + // after the last line in the document (and outside the bounds of the document). + // This should not throw. + var lastLineIndex = sourceText.Lines.Count - 1; + var range = new Range() + { + Start = new Position(lastLineIndex, 0), + End = new Position(lastLineIndex + 1, 0) + }; + + var textSpan = ProtocolConversions.RangeToTextSpan(range, sourceText); + + // Should span from the start of the last line to the end of the document + var lastLine = sourceText.Lines[lastLineIndex]; + Assert.Equal(lastLine.Start, textSpan.Start); + Assert.Equal(sourceText.Length, textSpan.End); + } + + [Fact] + public void RangeToTextSpanThrows_LineOutOfRange() + { + var markup = GetTestMarkup(); + var sourceText = SourceText.From(markup); + + // Ranges that are outside the document bounds should throw. + var range = new Range() { Start = new Position(0, 0), End = new Position(sourceText.Lines.Count + 1, 0) }; + Assert.Throws(() => ProtocolConversions.RangeToTextSpan(range, sourceText)); + } + [Fact] - public void RangeToTextSpanLineOutOfRangeError() + public void RangeToTextSpanWThrows_CharacterOutOfRange() { var markup = GetTestMarkup(); var sourceText = SourceText.From(markup); - var range = new Range() { Start = new Position(0, 0), End = new Position(sourceText.Lines.Count, 0) }; + var range = new Range() { Start = new Position(0, 0), End = new Position(sourceText.Lines.Count, 5) }; Assert.Throws(() => ProtocolConversions.RangeToTextSpan(range, sourceText)); }