diff --git a/src/Compiler/Facilities/prim-lexing.fs b/src/Compiler/Facilities/prim-lexing.fs index 785b7fbdf6..0e073dc80e 100644 --- a/src/Compiler/Facilities/prim-lexing.fs +++ b/src/Compiler/Facilities/prim-lexing.fs @@ -6,7 +6,6 @@ namespace FSharp.Compiler.Text open System open System.IO -open FSharp.Compiler type ISourceText = @@ -28,6 +27,8 @@ type ISourceText = abstract CopyTo: sourceIndex: int * destination: char[] * destinationIndex: int * count: int -> unit + abstract GetSubTextFromRange: range: range -> string + [] type StringText(str: string) = @@ -108,6 +109,41 @@ type StringText(str: string) = member _.CopyTo(sourceIndex, destination, destinationIndex, count) = str.CopyTo(sourceIndex, destination, destinationIndex, count) + member this.GetSubTextFromRange(range) = + let totalAmountOfLines = getLines.Value.Length + + if + range.StartLine = 0 + && range.StartColumn = 0 + && range.EndLine = 0 + && range.EndColumn = 0 + then + String.Empty + elif + range.StartLine < 1 + || (range.StartLine - 1) > totalAmountOfLines + || range.EndLine < 1 + || (range.EndLine - 1) > totalAmountOfLines + then + invalidArg (nameof range) "The range is outside the file boundaries" + else + let sourceText = this :> ISourceText + let startLine = range.StartLine - 1 + let line = sourceText.GetLineString startLine + + if range.StartLine = range.EndLine then + let length = range.EndColumn - range.StartColumn + line.Substring(range.StartColumn, length) + else + let firstLineContent = line.Substring(range.StartColumn) + let sb = System.Text.StringBuilder().AppendLine(firstLineContent) + + for lineNumber in range.StartLine .. range.EndLine - 2 do + sb.AppendLine(sourceText.GetLineString lineNumber) |> ignore + + let lastLine = sourceText.GetLineString(range.EndLine - 1) + sb.Append(lastLine.Substring(0, range.EndColumn)).ToString() + module SourceText = let ofString str = StringText(str) :> ISourceText diff --git a/src/Compiler/Facilities/prim-lexing.fsi b/src/Compiler/Facilities/prim-lexing.fsi index a7c919991d..6e5f6da4f2 100644 --- a/src/Compiler/Facilities/prim-lexing.fsi +++ b/src/Compiler/Facilities/prim-lexing.fsi @@ -35,6 +35,10 @@ type ISourceText = /// Copies a section of the input to the given destination ad the given index abstract CopyTo: sourceIndex: int * destination: char[] * destinationIndex: int * count: int -> unit + /// Gets a section of the input based on a given range. + /// Throws an exception when the input range is outside the file boundaries. + abstract GetSubTextFromRange: range: range -> string + /// Functions related to ISourceText objects module SourceText = diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl index 62e2842792..e5d58f2ed1 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl @@ -10179,6 +10179,7 @@ FSharp.Compiler.Text.ISourceText: Int32 GetLineCount() FSharp.Compiler.Text.ISourceText: Int32 Length FSharp.Compiler.Text.ISourceText: Int32 get_Length() FSharp.Compiler.Text.ISourceText: System.String GetLineString(Int32) +FSharp.Compiler.Text.ISourceText: System.String GetSubTextFromRange(FSharp.Compiler.Text.Range) FSharp.Compiler.Text.ISourceText: System.String GetSubTextString(Int32, Int32) FSharp.Compiler.Text.ISourceText: System.Tuple`2[System.Int32,System.Int32] GetLastCharacterPosition() FSharp.Compiler.Text.ISourceText: Void CopyTo(Int32, Char[], Int32, Int32) diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl index 62e2842792..e5d58f2ed1 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl @@ -10179,6 +10179,7 @@ FSharp.Compiler.Text.ISourceText: Int32 GetLineCount() FSharp.Compiler.Text.ISourceText: Int32 Length FSharp.Compiler.Text.ISourceText: Int32 get_Length() FSharp.Compiler.Text.ISourceText: System.String GetLineString(Int32) +FSharp.Compiler.Text.ISourceText: System.String GetSubTextFromRange(FSharp.Compiler.Text.Range) FSharp.Compiler.Text.ISourceText: System.String GetSubTextString(Int32, Int32) FSharp.Compiler.Text.ISourceText: System.Tuple`2[System.Int32,System.Int32] GetLastCharacterPosition() FSharp.Compiler.Text.ISourceText: Void CopyTo(Int32, Char[], Int32, Int32) diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj index b761ced68f..ad4c29b02e 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -89,6 +89,7 @@ RangeTests.fs + Program.fs diff --git a/tests/FSharp.Compiler.Service.Tests/SourceTextTests.fs b/tests/FSharp.Compiler.Service.Tests/SourceTextTests.fs new file mode 100644 index 0000000000..0190c2f467 --- /dev/null +++ b/tests/FSharp.Compiler.Service.Tests/SourceTextTests.fs @@ -0,0 +1,47 @@ +module FSharp.Compiler.Service.Tests.SourceTextTests + +open System +open FSharp.Compiler.Text +open NUnit.Framework + +[] +let ``Select text from a single line via the range`` () = + let sourceText = SourceText.ofString """ +let a = 2 +""" + let m = Range.mkRange "Sample.fs" (Position.mkPos 2 4) (Position.mkPos 2 5) + let v = sourceText.GetSubTextFromRange m + Assert.AreEqual("a", v) + +[] +let ``Select text from multiple lines via the range`` () = + let sourceText = SourceText.ofString """ +let a b c = + // comment + 2 +""" + let m = Range.mkRange "Sample.fs" (Position.mkPos 2 4) (Position.mkPos 4 5) + let v = sourceText.GetSubTextFromRange m + let sanitized = v.Replace("\r", "") + Assert.AreEqual("a b c =\n // comment\n 2", sanitized) + +[] +let ``Inconsistent return carriage return correct text`` () = + let sourceText = SourceText.ofString "let a =\r\n // foo\n 43" + let m = Range.mkRange "Sample.fs" (Position.mkPos 1 4) (Position.mkPos 3 6) + let v = sourceText.GetSubTextFromRange m + let sanitized = v.Replace("\r", "") + Assert.AreEqual("a =\n // foo\n 43", sanitized) + +[] +let ``Zero range should return empty string`` () = + let sourceText = SourceText.ofString "a" + let v = sourceText.GetSubTextFromRange Range.Zero + Assert.AreEqual(String.Empty, v) + +[] +let ``Invalid range should throw argument exception`` () = + let sourceText = SourceText.ofString "a" + let mInvalid = Range.mkRange "Sample.fs" (Position.mkPos 3 6) (Position.mkPos 1 4) + Assert.Throws(fun () -> sourceText.GetSubTextFromRange mInvalid |> ignore) + |> ignore diff --git a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SourceText.fs b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SourceText.fs index 49d46568ff..89d911d9b7 100644 --- a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SourceText.fs +++ b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SourceText.fs @@ -64,6 +64,40 @@ module internal SourceText = member __.CopyTo(sourceIndex, destination, destinationIndex, count) = sourceText.CopyTo(sourceIndex, destination, destinationIndex, count) + + member this.GetSubTextFromRange range = + let totalAmountOfLines = sourceText.Lines.Count + + if + range.StartLine = 0 + && range.StartColumn = 0 + && range.EndLine = 0 + && range.EndColumn = 0 + then + String.Empty + elif + range.StartLine < 1 + || (range.StartLine - 1) > totalAmountOfLines + || range.EndLine < 1 + || (range.EndLine - 1) > totalAmountOfLines + then + invalidArg (nameof range) "The range is outside the file boundaries" + else + let startLine = range.StartLine - 1 + let line = this.GetLineString startLine + + if range.StartLine = range.EndLine then + let length = range.EndColumn - range.StartColumn + line.Substring(range.StartColumn, length) + else + let firstLineContent = line.Substring(range.StartColumn) + let sb = System.Text.StringBuilder().AppendLine(firstLineContent) + + for lineNumber in range.StartLine .. range.EndLine - 2 do + sb.AppendLine(this.GetLineString lineNumber) |> ignore + + let lastLine = this.GetLineString(range.EndLine - 1) + sb.Append(lastLine.Substring(0, range.EndColumn)).ToString() } sourceText diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index d05b5166a0..8b432dfdf7 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -163,6 +163,40 @@ module private SourceText = member _.CopyTo(sourceIndex, destination, destinationIndex, count) = sourceText.CopyTo(sourceIndex, destination, destinationIndex, count) + + member this.GetSubTextFromRange range = + let totalAmountOfLines = sourceText.Lines.Count + + if + range.StartLine = 0 + && range.StartColumn = 0 + && range.EndLine = 0 + && range.EndColumn = 0 + then + String.Empty + elif + range.StartLine < 1 + || (range.StartLine - 1) > totalAmountOfLines + || range.EndLine < 1 + || (range.EndLine - 1) > totalAmountOfLines + then + invalidArg (nameof range) "The range is outside the file boundaries" + else + let startLine = range.StartLine - 1 + let line = this.GetLineString startLine + + if range.StartLine = range.EndLine then + let length = range.EndColumn - range.StartColumn + line.Substring(range.StartColumn, length) + else + let firstLineContent = line.Substring(range.StartColumn) + let sb = System.Text.StringBuilder().AppendLine(firstLineContent) + + for lineNumber in range.StartLine .. range.EndLine - 2 do + sb.AppendLine(this.GetLineString lineNumber) |> ignore + + let lastLine = this.GetLineString(range.EndLine - 1) + sb.Append(lastLine.Substring(0, range.EndColumn)).ToString() } sourceText