diff --git a/src/Analysis/Engine/Impl/Definitions/Position.cs b/src/Analysis/Engine/Impl/Definitions/Position.cs index 1a1c00936..8c8ab39a4 100644 --- a/src/Analysis/Engine/Impl/Definitions/Position.cs +++ b/src/Analysis/Engine/Impl/Definitions/Position.cs @@ -40,6 +40,6 @@ public struct Position { public static bool operator >(Position p1, Position p2) => p1.line > p2.line || p1.line == p2.line && p1.character > p2.character; public static bool operator <(Position p1, Position p2) => p1.line < p2.line || p1.line == p2.line && p1.character < p2.character; - public override string ToString() => ((SourceLocation)this).ToString(); + public override string ToString() => $"({line}, {character})"; } } diff --git a/src/Analysis/Engine/Impl/Definitions/Range.cs b/src/Analysis/Engine/Impl/Definitions/Range.cs index ec4a7e981..c0af7dbbd 100644 --- a/src/Analysis/Engine/Impl/Definitions/Range.cs +++ b/src/Analysis/Engine/Impl/Definitions/Range.cs @@ -24,6 +24,6 @@ public struct Range { public static implicit operator SourceSpan(Range r) => new SourceSpan(r.start, r.end); public static implicit operator Range(SourceSpan span) => new Range { start = span.Start, end = span.End }; - public override string ToString() => ((SourceSpan)this).ToString(); + public override string ToString() => $"{start} - {end}"; } } diff --git a/src/Analysis/Engine/Impl/Infrastructure/Extensions/StringBuilderExtensions.cs b/src/Analysis/Engine/Impl/Infrastructure/Extensions/StringBuilderExtensions.cs index 49a1dd3be..8c22853b0 100644 --- a/src/Analysis/Engine/Impl/Infrastructure/Extensions/StringBuilderExtensions.cs +++ b/src/Analysis/Engine/Impl/Infrastructure/Extensions/StringBuilderExtensions.cs @@ -25,19 +25,19 @@ public static StringBuilder TrimEnd(this StringBuilder sb) { return sb; } - public static StringBuilder EnsureEndsWithSpace(this StringBuilder sb, int count = 1, bool allowLeading = false) { - if (sb.Length == 0 && !allowLeading) { + public static StringBuilder EnsureEndsWithWhiteSpace(this StringBuilder sb, int whiteSpaceCount = 1) { + if (sb.Length == 0) { return sb; } - for (var i = sb.Length - 1; i >= 0 && char.IsWhiteSpace(sb[i]); i--) { - count--; + for (var i = sb.Length - 1; i >= 0 && whiteSpaceCount > 0 && char.IsWhiteSpace(sb[i]); i--) { + whiteSpaceCount--; } - if (count > 0) { - sb.Append(new string(' ', count)); + if (whiteSpaceCount > 0) { + sb.Append(' ', whiteSpaceCount); } - + return sb; } } diff --git a/src/Analysis/Engine/Impl/Parsing/Tokenizer.cs b/src/Analysis/Engine/Impl/Parsing/Tokenizer.cs index c5a8a93b8..e7b104800 100644 --- a/src/Analysis/Engine/Impl/Parsing/Tokenizer.cs +++ b/src/Analysis/Engine/Impl/Parsing/Tokenizer.cs @@ -130,17 +130,9 @@ public List ReadTokens(int characterCount) { return tokens; } - public object CurrentState { - get { - return _state; - } - } - - public SourceLocation CurrentPosition { - get { - return IndexToLocation(CurrentIndex); - } - } + public object CurrentState => _state; + public int CurrentLine => _newLineLocations.Count; + public SourceLocation CurrentPosition => IndexToLocation(CurrentIndex); public SourceLocation IndexToLocation(int index) { int match = _newLineLocations.BinarySearch(new NewLineLocation(index, NewLineKind.None)); @@ -2235,6 +2227,7 @@ private static void DumpToken(Token token) { Console.WriteLine("{0} `{1}`", token.Kind, token.Image.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t")); } + internal NewLineLocation GetNewLineLocation(int line) => _newLineLocations.Count == line ? new NewLineLocation(CurrentIndex, NewLineKind.None) : _newLineLocations[line]; internal NewLineLocation[] GetLineLocations() => _newLineLocations.ToArray(); internal SourceLocation[] GetCommentLocations() => _commentLocations.ToArray(); @@ -2564,11 +2557,7 @@ private bool AtBeginning { } } - private int CurrentIndex { - get { - return _tokenStartIndex + Math.Min(_position, _end) - _start; - } - } + private int CurrentIndex => _tokenStartIndex + Math.Min(_position, _end) - _start; private void DiscardToken() { CheckInvariants(); diff --git a/src/Analysis/Engine/Test/FluentAssertions/AssertionsFactory.cs b/src/Analysis/Engine/Test/FluentAssertions/AssertionsFactory.cs index d9d498c18..d157ffa3e 100644 --- a/src/Analysis/Engine/Test/FluentAssertions/AssertionsFactory.cs +++ b/src/Analysis/Engine/Test/FluentAssertions/AssertionsFactory.cs @@ -102,5 +102,8 @@ public static SignatureHelpAssertions Should(this SignatureHelp signatureHelp) public static SignatureInformationAssertions Should(this SignatureInformation signatureInformation) => new SignatureInformationAssertions(signatureInformation); + + public static TextEditCollectionAssertions Should(this IEnumerable textEdits) + => new TextEditCollectionAssertions(textEdits); } } \ No newline at end of file diff --git a/src/Analysis/Engine/Test/FluentAssertions/AssertionsUtilities.cs b/src/Analysis/Engine/Test/FluentAssertions/AssertionsUtilities.cs index e003a5dae..78d68e22d 100644 --- a/src/Analysis/Engine/Test/FluentAssertions/AssertionsUtilities.cs +++ b/src/Analysis/Engine/Test/FluentAssertions/AssertionsUtilities.cs @@ -205,6 +205,9 @@ public static IEnumerable FlattenAnalysisValues(IEnumerable PositionEquals(r1.start, r2.start) && PositionEquals(r1.end, r2.end); + public static bool PositionEquals(Position p1, Position p2) => p1.line == p2.line && p1.character == p2.character; + public static string DoubleEscape(string input) => input.Replace("\r", "\\\u200Br").Replace("\n", "\\\u200Bn").Replace("\t", @"\t"); } diff --git a/src/Analysis/Engine/Test/FluentAssertions/ReferenceCollectionAssertions.cs b/src/Analysis/Engine/Test/FluentAssertions/ReferenceCollectionAssertions.cs index bef8807df..a0ef985c5 100644 --- a/src/Analysis/Engine/Test/FluentAssertions/ReferenceCollectionAssertions.cs +++ b/src/Analysis/Engine/Test/FluentAssertions/ReferenceCollectionAssertions.cs @@ -75,10 +75,10 @@ public AndConstraint OnlyHaveReferences(IEnumerab if (excess.Length > 0) { var excessString = string.Join(", ", excess.Select(Format)); var errorMessage = expected.Length > 1 - ? $"Expected {GetName()} to have only {expected.Length} references{{reason}}, but it also has references: {excessString}." + ? $"Expected {GetSubjectName()} to have only {expected.Length} references{{reason}}, but it also has references: {excessString}." : expected.Length > 0 - ? $"Expected {GetName()} to have only one reference{{reason}}, but it also has references: {excessString}." - : $"Expected {GetName()} to have no references{{reason}}, but it has references: {excessString}."; + ? $"Expected {GetSubjectName()} to have only one reference{{reason}}, but it also has references: {excessString}." + : $"Expected {GetSubjectName()} to have no references{{reason}}, but it has references: {excessString}."; Execute.Assertion.BecauseOf(because, reasonArgs).FailWith(errorMessage); } @@ -87,10 +87,7 @@ public AndConstraint OnlyHaveReferences(IEnumerab } private static string Format((Uri uri, (int, int, int, int) range, ReferenceKind? kind) reference) - => $"({TestData.GetTestRelativePath(reference.uri)}, {Format(reference.range)}, {reference.kind})"; - - private static string Format((int startLine, int startCharacter, int endLine, int endCharacter) range) - => $"({range.startLine}, {range.startCharacter}) - ({range.endLine}, {range.endCharacter})"; + => $"({TestData.GetTestRelativePath(reference.uri)}, {reference.range.ToString()}, {reference.kind})"; [CustomAssertion] public AndConstraint HaveReferenceAt(Uri documentUri, int startLine, int startCharacter, int endLine, int endCharacter, ReferenceKind? referenceKind = null, string because = "", params object[] reasonArgs) { @@ -117,38 +114,29 @@ public AndConstraint HaveReferenceAt(Uri document private string FindReference(Uri documentUri, string moduleName, Range range, ReferenceKind? referenceKind = null) { var candidates = Subject.Where(av => Equals(av.uri, documentUri)).ToArray(); if (candidates.Length == 0) { - return $"Expected {GetName()} to have reference in the module '{moduleName}'{{reason}}, but no references has been found."; + return $"Expected {GetSubjectName()} to have reference in the module '{moduleName}'{{reason}}, but no references has been found."; } foreach (var candidate in candidates.Where(c => RangeEquals(c.range, range))) { return referenceKind.HasValue && candidate._kind != referenceKind - ? $"Expected {GetName()} to have reference of type '{referenceKind}'{{reason}}, but reference in module '{moduleName}' at {RangeToString(range)} has type '{candidate._kind}'" + ? $"Expected {GetSubjectName()} to have reference of type '{referenceKind}'{{reason}}, but reference in module '{moduleName}' at {range.ToString()} has type '{candidate._kind}'" : string.Empty; } - var errorMessage = $"Expected {GetName()} to have reference at {RangeToString(range)}{{reason}}, but module '{moduleName}' has no references at that range."; + var errorMessage = $"Expected {GetSubjectName()} to have reference at {range.ToString()}{{reason}}, but module '{moduleName}' has no references at that range."; if (!referenceKind.HasValue) { return errorMessage; } var matchingTypes = candidates.Where(av => av._kind == referenceKind).ToArray(); var matchingTypesString = matchingTypes.Length > 0 - ? $"References that match type '{referenceKind}' have spans {string.Join(" ,", matchingTypes.Select(av => RangeToString(av.range)))}" + ? $"References that match type '{referenceKind}' have spans {string.Join(" ,", matchingTypes.Select(av => av.range.ToString()))}" : $"There are no references with type '{referenceKind}' either"; return $"{errorMessage} {matchingTypesString}"; } - - private static string RangeToString(Range range) - => $"({range.start.line}, {range.start.character}) - ({range.end.line}, {range.end.character})"; - - private static bool RangeEquals(Range r1, Range r2) - => r1.start.line == r2.start.line - && r1.start.character == r2.start.character - && r1.end.line == r2.end.line - && r1.end.character == r2.end.character; - + [CustomAssertion] - private static string GetName() => CallerIdentifier.DetermineCallerIdentity() ?? "collection"; + private static string GetSubjectName() => CallerIdentifier.DetermineCallerIdentity() ?? "collection"; } } \ No newline at end of file diff --git a/src/Analysis/Engine/Test/FluentAssertions/TextEditCollectionAssertions.cs b/src/Analysis/Engine/Test/FluentAssertions/TextEditCollectionAssertions.cs new file mode 100644 index 000000000..069c8e604 --- /dev/null +++ b/src/Analysis/Engine/Test/FluentAssertions/TextEditCollectionAssertions.cs @@ -0,0 +1,113 @@ +// Python Tools for Visual Studio +// Copyright(c) Microsoft Corporation +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the License); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +// MERCHANTABLITY OR NON-INFRINGEMENT. +// +// See the Apache Version 2.0 License for specific language governing +// permissions and limitations under the License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using FluentAssertions; +using FluentAssertions.Collections; +using FluentAssertions.Execution; +using Microsoft.Python.LanguageServer; +using static Microsoft.PythonTools.Analysis.FluentAssertions.AssertionsUtilities; + +namespace Microsoft.PythonTools.Analysis.FluentAssertions { + [ExcludeFromCodeCoverage] + internal sealed class TextEditCollectionAssertions : SelfReferencingCollectionAssertions { + public TextEditCollectionAssertions(IEnumerable references) : base(references) { } + + protected override string Identifier => nameof(TextEdit) + "Collection"; + + + [CustomAssertion] + public AndConstraint OnlyHaveTextEdit(string expectedText, (int startLine, int startCharacter, int endLine, int endCharacter) expectedRange, string because = "", params object[] reasonArgs) + => OnlyHaveTextEdits(new[] {(expectedText, expectedRange)}, because, reasonArgs); + + [CustomAssertion] + public AndConstraint OnlyHaveTextEdits(params (string expectedText, (int startLine, int startCharacter, int endLine, int endCharacter) expectedRange)[] textEdits) + => OnlyHaveTextEdits(textEdits, string.Empty); + + [CustomAssertion] + public AndConstraint OnlyHaveTextEdits(IEnumerable<(string expectedText, (int startLine, int startCharacter, int endLine, int endCharacter) expectedRange)> textEdits, string because = "", params object[] reasonArgs) { + var expected = textEdits.ToArray(); + foreach (var (expectedText, (startLine, startCharacter, endLine, endCharacter)) in expected) { + HaveTextEditAt(expectedText, (startLine, startCharacter, endLine, endCharacter), because, reasonArgs); + } + + var excess = Subject.Select(r => (r.newText, (r.range.start.line, r.range.start.character, r.range.end.line, r.range.end.character))) + .Except(expected) + .ToArray(); + + if (excess.Length > 0) { + var excessString = string.Join(", ", excess.Select(((string text, (int, int, int, int) range) te) => $"({te.text}, {te.range.ToString()})")); + var errorMessage = expected.Length > 1 + ? $"Expected {GetSubjectName()} to have only {expected.Length} textEdits{{reason}}, but it also has textEdits: {excessString}." + : expected.Length > 0 + ? $"Expected {GetSubjectName()} to have only one reference{{reason}}, but it also has textEdits: {excessString}." + : $"Expected {GetSubjectName()} to have no textEdits{{reason}}, but it has textEdits: {excessString}."; + + Execute.Assertion.BecauseOf(because, reasonArgs).FailWith(errorMessage); + } + + return new AndConstraint(this); + } + + [CustomAssertion] + public AndConstraint HaveTextEditAt(string expectedText, (int startLine, int startCharacter, int endLine, int endCharacter) expectedRange, string because = "", params object[] reasonArgs) { + var range = new Range { + start = new Position { line = expectedRange.startLine, character = expectedRange.startCharacter }, + end = new Position { line = expectedRange.endLine, character = expectedRange.endCharacter } + }; + + var errorMessage = GetHaveTextEditErrorMessage(expectedText, range); + if (errorMessage != string.Empty) { + var assertion = Execute.Assertion.BecauseOf(because, reasonArgs); + assertion.AddNonReportable("expectedText", expectedText); + assertion.AddNonReportable("expectedRange", range); + assertion.AddNonReportable("currentTexts", GetQuotedNames(Subject.Select(te => te.newText))); + assertion.FailWith(errorMessage); + } + + return new AndConstraint(this); + } + + [CustomAssertion] + private string GetHaveTextEditErrorMessage(string expectedText, Range expectedRange) { + var candidates = Subject.Where(av => string.Equals(av.newText, expectedText, StringComparison.Ordinal)).ToArray(); + if (candidates.Length == 0) { + return "Expected {context:subject} to have text edit with newText \'{expectedText}\'{reason}, but " + + (Subject.Any() ? "it has {currentTexts}" : "it is empty"); + } + + var candidatesWithRange = candidates.Where(c => RangeEquals(c.range, expectedRange)).ToArray(); + if (candidatesWithRange.Length > 1) { + return $"Expected {{context:subject}} to have only one text edit with newText '{{expectedText}}' and range {{expectedRange}}{{reason}}, but there are {candidatesWithRange.Length}"; + } + + if (candidatesWithRange.Length == 0) { + return "Expected {context:subject} to have text edit with newText \'{expectedText}\' in range {expectedRange}{reason}, but " + + (candidates.Length == 1 + ? $"it has range {candidates[0].range.ToString()}" + : $"they are in ranges {string.Join(", ", candidates.Select(te => te.range.ToString()))}"); + } + + return string.Empty; + } + + [CustomAssertion] + private static string GetSubjectName() => CallerIdentifier.DetermineCallerIdentity() ?? "collection"; + } +} \ No newline at end of file diff --git a/src/Analysis/Engine/Test/LanguageServerTests.cs b/src/Analysis/Engine/Test/LanguageServerTests.cs index 0d05de69c..fae6da258 100644 --- a/src/Analysis/Engine/Test/LanguageServerTests.cs +++ b/src/Analysis/Engine/Test/LanguageServerTests.cs @@ -1010,31 +1010,13 @@ public async Task OnTypeFormatting() { // Extended tests for line formatting are in LineFormatterTests. // These just verify that the language server formats and returns something correct. var edits = await s.SendDocumentOnTypeFormatting(uri, new SourceLocation(2, 1), "\n"); - edits.Should().OnlyContain(new TextEdit { - newText = "def foo():", - range = new Range { - start = new SourceLocation(1, 1), - end = new SourceLocation(1, 15) - } - }); + edits.Should().OnlyHaveTextEdit("def foo():", (0, 0, 0, 14)); edits = await s.SendDocumentOnTypeFormatting(uri, new SourceLocation(3, 1), "\n"); - edits.Should().OnlyContain(new TextEdit { - newText = "x = a + b", - range = new Range { - start = new SourceLocation(2, 5), - end = new SourceLocation(2, 14) - } - }); + edits.Should().OnlyHaveTextEdit("x = a + b", (1, 4, 1, 13)); edits = await s.SendDocumentOnTypeFormatting(uri, new SourceLocation(4, 1), "\n"); - edits.Should().OnlyContain(new TextEdit { - newText = "x += 1", - range = new Range { - start = new SourceLocation(3, 5), - end = new SourceLocation(3, 10) - } - }); + edits.Should().OnlyHaveTextEdit("x += 1", (2, 4, 2, 9)); } } diff --git a/src/Analysis/Engine/Test/LineFormatterTests.cs b/src/Analysis/Engine/Test/LineFormatterTests.cs index b1fa11290..3a2a133ce 100644 --- a/src/Analysis/Engine/Test/LineFormatterTests.cs +++ b/src/Analysis/Engine/Test/LineFormatterTests.cs @@ -23,6 +23,7 @@ using Microsoft.PythonTools; using Microsoft.PythonTools.Analysis; using Microsoft.PythonTools.Analysis.FluentAssertions; +using Microsoft.PythonTools.Analysis.Infrastructure; using Microsoft.PythonTools.Parsing; using Microsoft.VisualStudio.TestTools.UnitTesting; using TestUtilities; @@ -44,8 +45,8 @@ public void TestCleanup() { [TestMethod, Priority(0)] public void LineOutOfBounds() { - AssertNoEdits("a+b", line: 0); AssertNoEdits("a+b", line: -1); + AssertNoEdits("a+b", line: 1); } [DataRow("")] @@ -108,12 +109,12 @@ public void TrailingComment() { [TestMethod, Priority(0)] public void SingleComment() { - AssertSingleLineFormat("# comment"); + AssertSingleLineFormat("# comment", "# comment"); } [TestMethod, Priority(0)] public void CommentWithLeadingWhitespace() { - AssertSingleLineFormat(" # comment", "# comment", editStart: 4); + AssertSingleLineFormat(" # comment", "# comment", editStart: 3); } [TestMethod, Priority(0)] @@ -207,9 +208,11 @@ public void UnaryOperators(string code, string expected) { AssertSingleLineFormat(code, expected); } - [TestMethod, Priority(0)] - public void EqualsWithTypeHints() { - AssertSingleLineFormat("def foo(x:int=3,x=100.)", "def foo(x: int = 3, x=100.)"); + [DataRow("def foo(x:int=3,x=100.)", "def foo(x: int = 3, x=100.)")] + [DataRow("def foo(x:Union[int,str]=3,x=100.)", "def foo(x: Union[int, str] = 3, x=100.)")] + [DataTestMethod, Priority(0)] + public void EqualsWithTypeHints(string code, string expected) { + AssertSingleLineFormat(code, expected); } [TestMethod, Priority(0)] @@ -227,8 +230,8 @@ public void LambdaArguments() { AssertSingleLineFormat("l4= lambda x =lambda y =lambda z= 1: z: y(): x()", "l4 = lambda x=lambda y=lambda z=1: z: y(): x()"); } - [DataRow("x = foo(\n * param1,\n * param2\n)", "*param1,", 2, 3)] - [DataRow("x = foo(\n * param1,\n * param2\n)", "*param2", 3, 3)] + [DataRow("x = foo(\n * param1,\n * param2\n)", "*param1,", 1, 2)] + [DataRow("x = foo(\n * param1,\n * param2\n)", "*param2", 2, 2)] [DataTestMethod, Priority(0)] public void StarInMultilineArguments(string code, string expected, int line, int editStart) { AssertSingleLineFormat(code, expected, line: line, editStart: editStart); @@ -236,21 +239,21 @@ public void StarInMultilineArguments(string code, string expected, int line, int [TestMethod, Priority(0)] public void Arrow() { - AssertSingleLineFormat("def f(a, \n ** k: 11) -> 12: pass", "**k: 11) -> 12: pass", line: 2, editStart: 5); + AssertSingleLineFormat("def f(a, \n ** k: 11) -> 12: pass", "**k: 11) -> 12: pass", line: 1, editStart: 4); } - [DataRow("def foo(x = 1)", "def foo(x=1)", 1, 1)] - [DataRow("def foo(a\n, x = 1)", ", x=1)", 2, 1)] - [DataRow("foo(a ,b,\n x = 1)", "x=1)", 2, 3)] - [DataRow("if True:\n if False:\n foo(a , bar(\n x = 1)", "x=1)", 4, 7)] - [DataRow("z=foo (0 , x= 1, (3+7) , y , z )", "z = foo(0, x=1, (3 + 7), y, z)", 1, 1)] - [DataRow("foo (0,\n x= 1,", "x=1,", 2, 2)] + [DataRow("def foo(x = 1)", "def foo(x=1)", 0, 0)] + [DataRow("def foo(a\n, x = 1)", ", x=1)", 1, 0)] + [DataRow("foo(a ,b,\n x = 1)", "x=1)", 1, 2)] + [DataRow("if True:\n if False:\n foo(a , bar(\n x = 1)", "x=1)", 3, 6)] + [DataRow("z=foo (0 , x= 1, (3+7) , y , z )", "z = foo(0, x=1, (3 + 7), y, z)", 0, 0)] + [DataRow("foo (0,\n x= 1,", "x=1,", 1, 1)] [DataRow(@"async def fetch(): async with aiohttp.ClientSession() as session: async with session.ws_connect( - ""http://127.0.0.1:8000/"", headers = cookie) as ws: # add unwanted spaces", @"""http://127.0.0.1:8000/"", headers=cookie) as ws: # add unwanted spaces", 4, 9)] - [DataRow("def pos0key1(*, key): return key\npos0key1(key= 100)", "pos0key1(key=100)", 2, 1)] - [DataRow("def test_string_literals(self):\n x= 1; y =2; self.assertTrue(len(x) == 0 and x == y)", "x = 1; y = 2; self.assertTrue(len(x) == 0 and x == y)", 2, 3)] + ""http://127.0.0.1:8000/"", headers = cookie) as ws: # add unwanted spaces", @"""http://127.0.0.1:8000/"", headers=cookie) as ws: # add unwanted spaces", 3, 8)] + [DataRow("def pos0key1(*, key): return key\npos0key1(key= 100)", "pos0key1(key=100)", 1, 0)] + [DataRow("def test_string_literals(self):\n x= 1; y =2; self.assertTrue(len(x) == 0 and x == y)", "x = 1; y = 2; self.assertTrue(len(x) == 0 and x == y)", 1, 2)] [DataTestMethod, Priority(0)] public void MultilineFunctionCall(string code, string expected, int line, int editStart) { AssertSingleLineFormat(code, expected, line: line, editStart: editStart); @@ -268,7 +271,7 @@ public void RemoveTrailingSpace() { [DataRow("a, *b, = 1, 2, 3")] [DataTestMethod, Priority(0)] public void IterableUnpacking(string code) { - AssertSingleLineFormat(code); + AssertSingleLineFormat(code, code); } // https://github.com/Microsoft/vscode-python/issues/1792 @@ -279,13 +282,13 @@ public void IterableUnpacking(string code) { [DataRow("ham[1: 9], ham[1 : 9], ham[1 :9 :3]", "ham[1:9], ham[1:9], ham[1:9:3]")] [DataRow("ham[lower : : upper]", "ham[lower::upper]")] [DataRow("ham[ : upper]", "ham[:upper]")] - [DataRow("foo[-5:]", null)] - [DataRow("foo[:-5]", null)] - [DataRow("foo[+5:]", null)] - [DataRow("foo[:+5]", null)] - [DataRow("foo[~5:]", null)] - [DataRow("foo[:~5]", null)] - [DataRow("foo[-a:]", null)] + [DataRow("foo[-5:]", "foo[-5:]")] + [DataRow("foo[:-5]", "foo[:-5]")] + [DataRow("foo[+5:]", "foo[+5:]")] + [DataRow("foo[:+5]", "foo[:+5]")] + [DataRow("foo[~5:]", "foo[~5:]")] + [DataRow("foo[:~5]", "foo[:~5]")] + [DataRow("foo[-a:]", "foo[-a:]")] [DataTestMethod, Priority(0)] public void SlicingPetPeeves(string code, string expected) { AssertSingleLineFormat(code, expected); @@ -327,7 +330,7 @@ public void Ellipsis() { [DataRow("{**{'x': 2}, 'x': 1}")] [DataTestMethod, Priority(0)] public void PEP448(string code) { - AssertSingleLineFormat(code); + AssertSingleLineFormat(code, code); } [TestMethod, Priority(0)] @@ -345,15 +348,18 @@ public void LineContinuation() { AssertSingleLineFormat("a+b+ \\\n", "a + b + \\"); } - [DataRow("foo.a() \\\n .b() \\\n .c()", "foo.a() \\", 1, 1)] - [DataRow("foo.a() \\\n .b() \\\n .c()", ".b() \\", 2, 4)] - [DataRow("foo.a() \\\n .b() \\\n .c()", ".c()", 3, 4)] + [DataRow("foo.a() \\\n .b() \\\n .c()", "foo.a() \\", 0, 0, 9)] + [DataRow("foo.a() \\\r\n .b() \\\r\n .c()", "foo.a() \\", 0, 0, 9)] + [DataRow("foo.a() \\\n .b() \\\n .c()", ".b() \\", 1, 3, 9)] + [DataRow("foo.a() \\\r\n .b() \\\r\n .c()", ".b() \\", 1, 3, 9)] + [DataRow("foo.a() \\\n .b() \\\n .c()", ".c()", 2, 3, 7)] + [DataRow("foo.a() \\\r\n .b() \\\r\n .c()", ".c()", 2, 3, 7)] [DataTestMethod, Priority(0)] - public void MultilineChainedCall(string code, string expected, int line, int editStart) { - AssertSingleLineFormat(code, expected, line: line, editStart: editStart); + public void MultilineChainedCall(string code, string expected, int line, int characterStart, int characterEnd) { + var edits = new LineFormatter(new StringReader(code), PythonLanguageVersion.V36).FormatLine(line); + edits.Should().OnlyHaveTextEdit(expected, (line, characterStart, line, characterEnd)); } - [DataRow("a[:, :, :, 1]")] [DataRow("a[x:y, x + 1 :y, :, 1]")] [DataRow("a[:, 1:3]")] @@ -361,12 +367,12 @@ public void MultilineChainedCall(string code, string expected, int line, int edi [DataRow("a[:, 3:, :]")] [DataTestMethod, Priority(0)] public void BracketCommas(string code) { - AssertSingleLineFormat(code); + AssertSingleLineFormat(code, code); } [TestMethod, Priority(0)] public void MultilineStringTrailingComment() { - AssertSingleLineFormat("'''\nfoo\n''' # comment", " # comment", line: 3, editStart: 4); + AssertSingleLineFormat("'''\nfoo\n''' # comment", " # comment", line: 2, editStart: 3); } [DataRow("`a`")] @@ -374,7 +380,7 @@ public void MultilineStringTrailingComment() { [DataRow("`a` if a else 'oops'")] [DataTestMethod, Priority(0)] public void Backtick(string code) { - AssertSingleLineFormat(code, languageVersion: PythonLanguageVersion.V27); + AssertSingleLineFormat(code, code, languageVersion: PythonLanguageVersion.V27); } [DataRow("exec code", PythonLanguageVersion.V27)] @@ -382,7 +388,7 @@ public void Backtick(string code) { [DataRow("exec(code)", PythonLanguageVersion.V37)] [DataTestMethod, Priority(0)] public void ExecStatement(string code, PythonLanguageVersion version) { - AssertSingleLineFormat(code, languageVersion: version); + AssertSingleLineFormat(code, code, languageVersion: version); } [TestMethod, Priority(0)] @@ -402,8 +408,7 @@ public void CommentAfterOperator() { public void StringConcat(string code, string expected) { AssertSingleLineFormat(code, expected); } - - + [TestMethod, Priority(0)] public void GrammarFile() { var src = TestData.GetPath("TestData", "Formatting", "pythonGrammar.py"); @@ -419,9 +424,7 @@ public void GrammarFile() { var lineFormatter = new LineFormatter(reader, PythonLanguageVersion.V37); for (var i = 0; i < lines.Length; i++) { - var lineNum = i + 1; - - var edits = lineFormatter.FormatLine(lineNum); + var edits = lineFormatter.FormatLine(i); edits.Should().NotBeNull().And.HaveCountLessOrEqualTo(1); if (edits.Length == 0) { @@ -436,7 +439,7 @@ public void GrammarFile() { end.line.Should().Be(i); var lineText = lines[i]; - edit.newText.Should().Be(lineText.Substring(start.character, end.character - start.character - 1), $"because line {lineNum} should be unchanged"); + edit.newText.Should().Be(lineText.Substring(start.character, end.character - start.character), $"because line {i} should be unchanged"); } } } @@ -444,43 +447,27 @@ public void GrammarFile() { /// /// Checks that a single line of input text is formatted as expected. /// - /// Input code to format - /// The expected result from the formatter. If null, then text is used. + /// Input code to format. + /// The expected result from the formatter. /// The line number to request to be formatted. /// Python language version to format. /// Where the edit should begin (i.e. when whitespace or a multi-line string begins a line). - public static void AssertSingleLineFormat(string text, string expected = null, int line = 1, PythonLanguageVersion languageVersion = PythonLanguageVersion.V37, int editStart = 1) { - if (text == null) { - throw new ArgumentNullException(nameof(text)); - } - - if (expected == null) { - expected = text; - } + public static void AssertSingleLineFormat(string text, string expected, int line = 0, PythonLanguageVersion languageVersion = PythonLanguageVersion.V37, int editStart = 0) { + Check.ArgumentNull(nameof(text), text); + Check.ArgumentNull(nameof(expected), expected); using (var reader = new StringReader(text)) { - var lineFormatter = new LineFormatter(reader, languageVersion); - - var edits = lineFormatter.FormatLine(line); - - edits.Should().OnlyContain(new TextEdit { - newText = expected, - range = new Range { - start = new SourceLocation(line, editStart), - end = new SourceLocation(line, text.Split('\n')[line - 1].Length + 1) - } - }); + var edits = new LineFormatter(reader, languageVersion).FormatLine(line); + edits.Should().OnlyHaveTextEdit(expected, (line, editStart, line, text.Split('\n')[line].Length)); } } - public static void AssertNoEdits(string text, int line = 1, PythonLanguageVersion languageVersion = PythonLanguageVersion.V37) { - if (text == null) { - throw new ArgumentNullException(nameof(text)); - } + public static void AssertNoEdits(string text, int line = 0, PythonLanguageVersion languageVersion = PythonLanguageVersion.V37) { + Check.ArgumentNull(nameof(text), text); using (var reader = new StringReader(text)) { - var lineFormatter = new LineFormatter(reader, languageVersion); - lineFormatter.FormatLine(line).Should().BeEmpty(); + var edits = new LineFormatter(reader, languageVersion).FormatLine(line); + edits.Should().BeEmpty(); } } } diff --git a/src/LanguageServer/Impl/Implementation/LineFormatter.cs b/src/LanguageServer/Impl/Implementation/LineFormatter.cs index e16240361..69ab9a6ba 100644 --- a/src/LanguageServer/Impl/Implementation/LineFormatter.cs +++ b/src/LanguageServer/Impl/Implementation/LineFormatter.cs @@ -77,30 +77,20 @@ private void AddToken(TokenExt token) { /// A function which returns true if the token should be added to the final list. If null, all tokens will be added. /// A non-null list of tokens on that line. private List TokenizeLine(int line, Func includeToken = null) { - Check.Argument(nameof(line), () => line > 0); + Check.Argument(nameof(line), () => line >= 0); var extraToken = true; - - var peeked = _tokenizer.Peek(); - while (peeked != null && (peeked.Line <= line || extraToken)) { - var token = _tokenizer.Next(); - - if (includeToken == null || includeToken(token)) { - AddToken(token); + for (var current = _tokenizer.Current ?? _tokenizer.Next(); current != null && (current.Line <= line || extraToken); current = _tokenizer.Next()) { + if (includeToken == null || includeToken(current)) { + AddToken(current); } - peeked = _tokenizer.Peek(); - - if (token.Line > line && !token.IsIgnored) { + if (current.Line > line && !current.IsIgnored) { extraToken = false; } } - if (!_lineTokens.TryGetValue(line, out List tokens)) { - return new List(); - } - - return tokens; + return _lineTokens.TryGetValue(line, out var tokens) ? tokens : new List(); } /// @@ -109,11 +99,10 @@ private List TokenizeLine(int line, Func includeToken /// One-indexed line number. /// A list of TextEdits needed to format the line. public TextEdit[] FormatLine(int line) { - if (line < 1) { + if (line < 0) { return NoEdits; } - - // Keep ExplictLineJoin because it has text associated with it. + // Keep ExplicitLineJoin because it has text associated with it. var tokens = TokenizeLine(line, t => !t.IsIgnored || t.Kind == TokenKind.ExplicitLineJoin); if (tokens.Count == 0) { @@ -122,18 +111,18 @@ public TextEdit[] FormatLine(int line) { var builder = new StringBuilder(); var first = tokens[0]; - var beginCol = first.Span.Start.Column; + var startIndex = first.Span.Start; var startIdx = 0; if (first.IsMultilineString) { // If the first token is a multiline string, start the edit afterward, // skip looking at the first token, and ensure that there's a space // after it if needed (i.e. in the case of a following comment). - beginCol = first.Span.End.Column; + startIndex = first.Span.End; startIdx = 1; - builder.EnsureEndsWithSpace(allowLeading: true); + builder.Append(' '); } - + for (var i = startIdx; i < tokens.Count; i++) { var token = tokens[i]; var prev = tokens.ElementAtOrDefault(i - 1); @@ -141,17 +130,42 @@ public TextEdit[] FormatLine(int line) { switch (token.Kind) { case TokenKind.Comment: - builder.EnsureEndsWithSpace(2); + builder.EnsureEndsWithWhiteSpace(2); builder.Append(token); break; - case TokenKind.Assign: - if (token.IsInsideFunctionArgs && prev?.PrevNonIgnored?.Kind != TokenKind.Colon) { - builder.Append(token); - break; + case TokenKind.Assign when token.IsInsideFunctionArgs: + // Search backwards through the tokens looking for a colon for this argument, + // indicating that there's a type hint and spacing should surround the equals. + for (var p = token.PrevNonIgnored; p != null; p = p.PrevNonIgnored) { + if (p == token.Inside) { + // Hit the surrounding left parenthesis, so stop the search. + builder.Append(token); + break; + } + + if (p.Inside != token.Inside) { + // Inside another grouping than the =, so skip over it. + continue; + } + + if (p.Kind == TokenKind.Comma) { + // Found a comma, indicating the end of another argument, so stop. + builder.Append(token); + break; + } + + if (p.Kind == TokenKind.Colon) { + // Found a colon before hitting another argument or the opening parenthesis, so add spacing. + AppendTokenEnsureWhiteSpacesAround(builder, token); + break; + } } + break; - goto case TokenKind.AddEqual; + case TokenKind.Assign: + AppendTokenEnsureWhiteSpacesAround(builder, token); + break; // "Normal" assignment and function parameters with type hints case TokenKind.AddEqual: @@ -167,80 +181,68 @@ public TextEdit[] FormatLine(int line) { case TokenKind.BitwiseAndEqual: case TokenKind.BitwiseOrEqual: case TokenKind.ExclusiveOrEqual: - builder.EnsureEndsWithSpace(); - builder.Append(token); - builder.EnsureEndsWithSpace(); + AppendTokenEnsureWhiteSpacesAround(builder, token); break; case TokenKind.Comma: builder.Append(token); if (next != null && !next.IsClose && next.Kind != TokenKind.Colon) { - builder.EnsureEndsWithSpace(); + builder.EnsureEndsWithWhiteSpace(); } break; - case TokenKind.Colon: - // Slicing - if (token.Inside?.Kind == TokenKind.LeftBracket) { - if (!token.IsSimpleSliceToLeft) { - builder.EnsureEndsWithSpace(); - } - - builder.Append(token); + // Slicing + case TokenKind.Colon when token.Inside?.Kind == TokenKind.LeftBracket: + if (!token.IsSimpleSliceToLeft) { + builder.EnsureEndsWithWhiteSpace(); + } - if (!token.IsSimpleSliceToRight) { - builder.EnsureEndsWithSpace(); - } + builder.Append(token); - break; + if (!token.IsSimpleSliceToRight) { + builder.EnsureEndsWithWhiteSpace(); } + break; + + case TokenKind.Colon: builder.Append(token); - if (next != null && !next.Is(TokenKind.Colon, TokenKind.Comma)) { - builder.EnsureEndsWithSpace(); + if (next != null && !next.IsColonOrComma) { + builder.EnsureEndsWithWhiteSpace(); } break; case TokenKind.At: if (prev != null) { - goto case TokenKind.MatMultiply; + AppendTokenEnsureWhiteSpacesAround(builder, token); + } else { + builder.Append(token); } - - builder.Append(token); break; // Unary case TokenKind.Add: case TokenKind.Subtract: case TokenKind.Twiddle: - if (prev != null && (prev.IsOperator || prev.IsOpen || prev.Is(TokenKind.Comma, TokenKind.Colon))) { + if (prev != null && (prev.IsOperator || prev.IsOpen || prev.IsColonOrComma)) { builder.Append(token); - break; + } else { + AppendTokenEnsureWhiteSpacesAround(builder, token); } - goto case TokenKind.MatMultiply; + break; case TokenKind.Power: case TokenKind.Multiply: - if (token.Inside != null) { - var actualPrev = token.PrevNonIgnored; - if (actualPrev != null) { - if (actualPrev.Kind == TokenKind.Comma || actualPrev.IsOpen || token.Inside.Kind == TokenKind.KeywordLambda) { - builder.Append(token); - break; - } - } - } - - if (token.Kind == TokenKind.Multiply) { + var actualPrev = token.PrevNonIgnored; + if (token.Inside != null && actualPrev != null && (actualPrev.Kind == TokenKind.Comma || actualPrev.IsOpen || token.Inside.Kind == TokenKind.KeywordLambda)) { + builder.Append(token); // Check unpacking case - var actualPrev = token.PrevNonIgnored; - if (actualPrev == null || (actualPrev.Kind != TokenKind.Name && actualPrev.Kind != TokenKind.Constant && !actualPrev.IsClose)) { - builder.Append(token); - break; - } + } else if (token.Kind == TokenKind.Multiply && (actualPrev == null || actualPrev.Kind != TokenKind.Name && actualPrev.Kind != TokenKind.Constant && !actualPrev.IsClose)) { + builder.Append(token); + } else { + AppendTokenEnsureWhiteSpacesAround(builder, token); } - - goto case TokenKind.MatMultiply; + break; // Operators case TokenKind.MatMultiply: @@ -260,14 +262,12 @@ public TextEdit[] FormatLine(int line) { case TokenKind.NotEquals: case TokenKind.LessThanGreaterThan: case TokenKind.Arrow: - builder.EnsureEndsWithSpace(); - builder.Append(token); - builder.EnsureEndsWithSpace(); + AppendTokenEnsureWhiteSpacesAround(builder, token); break; case TokenKind.Dot: if (prev != null && (prev.Kind == TokenKind.KeywordFrom || prev.IsNumber)) { - builder.EnsureEndsWithSpace(); + builder.EnsureEndsWithWhiteSpace(); } builder.Append(token); @@ -284,16 +284,17 @@ public TextEdit[] FormatLine(int line) { case TokenKind.Semicolon: builder.Append(token); - builder.EnsureEndsWithSpace(); + builder.EnsureEndsWithWhiteSpace(); + break; + + case TokenKind.Constant when next != null && next.IsString: + builder.Append(token); + builder.EnsureEndsWithWhiteSpace(); break; case TokenKind.Constant: - if (token.IsString && next != null && next.IsString) { - builder.Append(token); - builder.EnsureEndsWithSpace(); - break; - } - goto case TokenKind.Name; + builder.Append(token); + break; case TokenKind.Name: case TokenKind.KeywordFalse: @@ -303,7 +304,7 @@ public TextEdit[] FormatLine(int line) { break; case TokenKind.ExplicitLineJoin: - builder.EnsureEndsWithSpace(); + builder.EnsureEndsWithWhiteSpace(); builder.Append("\\"); // Hardcoded string so that any following whitespace doesn't make it in. break; @@ -311,42 +312,35 @@ public TextEdit[] FormatLine(int line) { builder.Append(token); break; - default: - if (token.Kind == TokenKind.KeywordLambda) { - if (token.IsInsideFunctionArgs && prev?.Kind == TokenKind.Assign) { - builder.Append(token); - - if (next?.Kind != TokenKind.Colon) { - builder.EnsureEndsWithSpace(); - } + case TokenKind.KeywordLambda when token.IsInsideFunctionArgs && prev?.Kind == TokenKind.Assign: + builder.Append(token); - break; - } + if (next?.Kind != TokenKind.Colon) { + builder.EnsureEndsWithWhiteSpace(); } + break; + + default: if (token.IsKeyword) { if (prev != null && !prev.IsOpen) { - builder.EnsureEndsWithSpace(); + builder.EnsureEndsWithWhiteSpace(); } builder.Append(token); if (next != null && next.Kind != TokenKind.Colon && next.Kind != TokenKind.Semicolon) { - builder.EnsureEndsWithSpace(); + builder.EnsureEndsWithWhiteSpace(); } - - break; + } else { + // No tokens should make it to this case, but try to keep things separated. + AppendTokenEnsureWhiteSpacesAround(builder, token); } - - // No tokens should make it to this case, but try to keep things separated. - builder.EnsureEndsWithSpace(); - builder.Append(token); - builder.EnsureEndsWithSpace(); break; } } - var endCol = _tokenizer.EndOfLineCol(line); + var endIndex = _tokenizer.GetLineEndIndex(line); var afterLast = tokens.Last().Next; if (afterLast != null && afterLast.IsMultilineString) { @@ -357,16 +351,17 @@ public TextEdit[] FormatLine(int line) { } builder.TrimEnd(); - var newText = builder.ToString(); - - if (newText.Length == 0) { + if (builder.Length == 0) { return NoEdits; } + var newText = builder.ToString(); + var lineStartIndex = _tokenizer.GetLineStartIndex(line); + var edit = new TextEdit { range = new Range { - start = new SourceLocation(line, beginCol), - end = new SourceLocation(line, endCol) + start = new Position{ line = line, character = startIndex - lineStartIndex }, + end = new Position{ line = line, character = endIndex - lineStartIndex } }, newText = newText }; @@ -374,26 +369,43 @@ public TextEdit[] FormatLine(int line) { return new[] { edit }; } + private static void AppendTokenEnsureWhiteSpacesAround(StringBuilder builder, TokenExt token) + => builder.EnsureEndsWithWhiteSpace() + .Append(token) + .EnsureEndsWithWhiteSpace(); + private class TokenExt { - public Token Token { get; set; } - public SourceSpan Span { get; set; } - public int Line => Span.End.Line; + public TokenExt(Token token, string precedingWhitespace, IndexSpan span, int line, bool isMultiLine, + TokenExt prev) { + Token = token; + PrecedingWhitespace = precedingWhitespace; + Span = span; + Line = line; + Prev = prev; + IsMultilineString = IsString && isMultiLine; + } + + public Token Token { get; } + public IndexSpan Span { get; } + public int Line { get; } public TokenExt Inside { get; set; } - public TokenExt Prev { get; set; } + public TokenExt Prev { get; } public TokenExt Next { get; set; } - public string PreceedingWhitespace { get; set; } + public string PrecedingWhitespace { get; } + public bool IsMultilineString { get; } + public TokenKind Kind => Token.Kind; public override string ToString() => Token.VerbatimImage; - public bool Is(params TokenKind[] kinds) => kinds.Contains(Kind); - public bool IsIgnored => Is(TokenKind.NewLine, TokenKind.NLToken, TokenKind.Indent, TokenKind.Dedent, TokenKind.ExplicitLineJoin); public bool IsOpen => Is(TokenKind.LeftBrace, TokenKind.LeftBracket, TokenKind.LeftParenthesis); public bool IsClose => Is(TokenKind.RightBrace, TokenKind.RightBracket, TokenKind.RightParenthesis); + public bool IsColonOrComma => Is(TokenKind.Colon, TokenKind.Comma); + public bool MatchesClose(TokenExt other) { switch (Kind) { case TokenKind.LeftBrace: @@ -418,9 +430,7 @@ public bool MatchesClose(TokenExt other) { public bool IsKeyword => (Kind >= TokenKind.FirstKeyword && Kind <= TokenKind.LastKeyword) || Kind == TokenKind.KeywordAsync || Kind == TokenKind.KeywordAwait; public bool IsString => Kind == TokenKind.Constant && Token != Tokens.NoneToken && (Token.Value is string || Token.Value is AsciiString); - - public bool IsMultilineString => Span.Start.Line != Span.End.Line && IsString; - + public bool IsSimpleSliceToLeft { get { if (Kind != TokenKind.Colon) { @@ -511,6 +521,8 @@ public TokenExt NextNonIgnored { return null; } } + + private bool Is(params TokenKind[] kinds) => kinds.Contains(Kind); } /// @@ -522,26 +534,17 @@ public TokenExt NextNonIgnored { private class TokenizerWrapper { private readonly Tokenizer _tokenizer; private readonly Stack _insides = new Stack(); - private TokenExt _peeked = null; - private TokenExt _prev = null; + public TokenExt Current { get; private set; } public TokenizerWrapper(Tokenizer tokenizer) { _tokenizer = tokenizer; } /// - /// Returns the next token, and advances the tokenizer. Note that - /// the returned token's Next will not be set until the tokenizer - /// actually reads that next token. + /// Returns the next token, and advances the tokenizer. /// /// The next token public TokenExt Next() { - if (_peeked != null) { - var tmp = _peeked; - _peeked = null; - return tmp; - } - if (_tokenizer.IsEndOfFile) { return null; } @@ -553,14 +556,18 @@ public TokenExt Next() { } var tokenSpan = _tokenizer.TokenSpan; - var sourceSpan = new SourceSpan(_tokenizer.IndexToLocation(tokenSpan.Start), _tokenizer.IndexToLocation(tokenSpan.End)); - - var tokenExt = new TokenExt { - Token = token, - PreceedingWhitespace = _tokenizer.PreceedingWhiteSpace, - Span = sourceSpan, - Prev = _prev - }; + var line = _tokenizer.CurrentLine; + var lineStart = GetLineStartIndex(line); + var isMultiLine = tokenSpan.Start < lineStart; + + var tokenExt = new TokenExt( + token, + _tokenizer.PreceedingWhiteSpace, + tokenSpan, + line, + isMultiLine, + Current + ); if (tokenExt.IsClose) { if (_insides.Count == 0 || !_insides.Peek().MatchesClose(tokenExt)) { @@ -579,52 +586,27 @@ public TokenExt Next() { _insides.Push(tokenExt); } - if (_prev != null) { - _prev.Next = tokenExt; + if (Current != null) { + Current.Next = tokenExt; } - _prev = tokenExt; + Current = tokenExt; return tokenExt; } /// - /// Returns the next token without advancing the tokenizer. Note that - /// the returned token's Next will not be set until the tokenizer - /// actually reads that next token. + /// Gets the index of the start of the line /// - /// The next token - public TokenExt Peek() { - if (_peeked != null) { - return _peeked; - } - - _peeked = Next(); - return _peeked; - } + /// Line number. + public int GetLineStartIndex(int line) => line > 0 ? _tokenizer.GetNewLineLocation(line - 1).EndIndex : 0; /// - /// Gets the one-indexed column number of the end of a line. The - /// tokenizer must be past the line's newline (or at EOF) in order - /// for this function to work. + /// Gets the index of the end of the line, excluding line break /// - /// A one-indexed line number. - /// One-indexed column number for the end of the line - public int EndOfLineCol(int line) { - if (line > _tokenizer.CurrentPosition.Line || (line == _tokenizer.CurrentPosition.Line && !_tokenizer.IsEndOfFile)) { - throw new ArgumentException("tokenizer must be at EOF or past line's newline", nameof(line)); - } - - var idx = line - 1; - var lines = _tokenizer.GetLineLocations(); - - if (idx < lines.Length) { - var nlLoc = lines[idx]; - - var sourceLocation = _tokenizer.IndexToLocation(nlLoc.EndIndex - 1); - return sourceLocation.Column; - } - - return _tokenizer.CurrentPosition.Column; + /// Line number. + public int GetLineEndIndex(int line) { + var newLineLocation = _tokenizer.GetNewLineLocation(line); + return newLineLocation.EndIndex - newLineLocation.Kind.GetSize(); } } diff --git a/src/LanguageServer/Impl/Implementation/Server.OnTypeFormatting.cs b/src/LanguageServer/Impl/Implementation/Server.OnTypeFormatting.cs index 90e07ec12..046596b21 100644 --- a/src/LanguageServer/Impl/Implementation/Server.OnTypeFormatting.cs +++ b/src/LanguageServer/Impl/Implementation/Server.OnTypeFormatting.cs @@ -26,10 +26,10 @@ public override Task DocumentOnTypeFormatting(DocumentOnTypeFormatti switch (@params.ch) { case "\n": - targetLine = @params.position.line; + targetLine = @params.position.line - 1; break; case ";": - targetLine = @params.position.line + 1; + targetLine = @params.position.line; break; default: throw new ArgumentException("unexpected trigger character", nameof(@params.ch));