Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@
* Constraint solver: `TryD` is now `inline` with `[<InlineIfLambda>]` on its always-run continuation, so the argument closures are no longer allocated at the (very hot) constraint-solver call sites; `IgnoreFailedMemberConstraintResolution` is `inline` so its forwarded continuation stays a literal. ([PR #20367](https://github.com/dotnet/fsharp/pull/20367))
* `DelayedILModuleReader` no longer boxes its cached `ILModuleReader` on every read: the field is typed `ILModuleReader | null` and matched directly. ([PR #20413](https://github.com/dotnet/fsharp/pull/20413))
* Optimizer: passing a partial application of a non-inline module-level function to an `[<InlineIfLambda>]` parameter (e.g. `xs |> Option.map (f a b)`) no longer allocates a per-call `FSharpFunc` closure when a captured argument is non-trivial (a field read, a call). Under optimization the argument is eta-expanded to a lambda with its captured evaluations floated above the binding, so the parameter's uses beta-reduce and the closure is eliminated. Captured arguments are still evaluated exactly once, in their original left-to-right order, and the binding keeps its sequence point. Partial applications of inline/SRTP functions and curried members can still allocate closures. ([PR #20487](https://github.com/dotnet/fsharp/pull/20487))
* `FSharp.Compiler.EditorServices.Structure.getOutliningRanges` scans the source lines without building a trimmed string per comment line, and no longer stores the text of a comment group next to its line numbers. ([PR #20443](https://github.com/dotnet/fsharp/pull/20443))

### Changed
* The `--warnaserror` option now ignores unrecognized diagnostic identifiers in warning lists while still applying recognized F# warning codes. ([PR #20246](https://github.com/dotnet/fsharp/pull/20246))
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/.VisualStudio/18.vNext.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
* Unused analyzers: disable in VS when file has errors ([PR #19892](https://github.com/dotnet/fsharp/pull/19892))
* Move to Roslyn's unified ExternalAccess library ([PR #20099](https://github.com/dotnet/fsharp/pull/20099))
* Remove trailing whitespace from source files. No functional change: whitespace inside string literals and inactive `#if` regions is preserved. ([PR #20355](https://github.com/dotnet/fsharp/pull/20355))
* Reduce editor allocations when computing block structure (code folding) by slicing the source text once per outlining pass instead of copying every line. ([PR #20443](https://github.com/dotnet/fsharp/pull/20443))
87 changes: 47 additions & 40 deletions src/Compiler/Service/ServiceStructure.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace FSharp.Compiler.EditorServices

open System
open Internal.Utilities.Library
open FSharp.Compiler.Syntax
open FSharp.Compiler.SyntaxTreeOps
Expand Down Expand Up @@ -186,27 +187,38 @@ module Structure =
}

type LineNumber = int
type LineStr = string

type CommentType =
| SingleLine
| XmlDoc

/// <summary>
/// Determines if a line is a single line or xml documentation comment.
/// </summary>
/// <remarks>
/// Kept at module scope: a local recursive function capturing a <see cref="T:System.ReadOnlySpan`1"/>-typed
/// helper as a closure field would need to instantiate <see cref="T:Microsoft.FSharp.Core.FSharpFunc`2"/>
/// over it, which the CLR disallows for byref-like type arguments (FS0412).
/// </remarks>
let commentTypeOf (line: ReadOnlySpan<char>) =
if line.StartsWithOrdinal("///") then ValueSome XmlDoc
elif line.StartsWithOrdinal("//") then ValueSome SingleLine
else ValueNone

[<NoComparison>]
type CommentList =
{
Lines: ResizeArray<LineNumber * LineStr>
Lines: ResizeArray<LineNumber>
Type: CommentType
}

static member New ty lineStr =
static member New ty lineNum =
{
Type = ty
Lines = ResizeArray [ lineStr ]
Lines = ResizeArray [ lineNum ]
}

/// Returns outlining ranges for given parsed input.
let getOutliningRanges (sourceLines: string[]) (parsedInput: ParsedInput) =
let getOutliningRangesFromLineSlices (sourceLines: ReadOnlyMemory<char>[]) (parsedInput: ParsedInput) =
let acc = ResizeArray()

/// Validation function to ensure that ranges yielded for outlining span 2 or more lines
Expand Down Expand Up @@ -661,7 +673,7 @@ module Structure =
| r :: rest, last :: _ when
r.StartLine = last.EndLine + 1
|| sourceLines[last.EndLine .. r.StartLine - 2]
|> Array.forall System.String.IsNullOrWhiteSpace
|> Array.forall _.Span.IsWhiteSpace()
->
loop rest res (r :: currentBulk)
| r :: rest, _ -> loop rest (currentBulk :: res) [ r ]
Expand Down Expand Up @@ -719,7 +731,7 @@ module Structure =

let collectConditionalDirectives directives sourceLines =
// Adds a fold region from prevRange.Start to the line above nextLine
let addSectionFold (prevRange: range) (nextLine: int) (sourceLines: string array) =
let addSectionFold (prevRange: range) (nextLine: int) (sourceLines: ReadOnlyMemory<char>[]) =
let startLineIndex = nextLine - 2

if startLineIndex >= 0 then
Expand Down Expand Up @@ -753,7 +765,7 @@ module Structure =
| ConditionalDirectiveTrivia.Else r -> ValueSome r
| _ -> ValueNone

let rec group directives stack (sourceLines: string array) =
let rec group directives stack (sourceLines: ReadOnlyMemory<char>[]) =
match directives with
| [] -> ()
| ConditionalDirectiveTrivia.If _ as ifDirective :: directives -> group directives (ifDirective :: stack) sourceLines
Expand Down Expand Up @@ -822,50 +834,42 @@ module Structure =
collectOpens decls
List.iter parseDeclaration decls

/// Determine if a line is a single line or xml documentation comment
let (|Comment|_|) (line: string) =
if line.StartsWithOrdinal("///") then Some XmlDoc
elif line.StartsWithOrdinal("//") then Some SingleLine
else None

let getCommentRanges trivia (lines: string[]) =
let rec loop (lastLineNum, currentComment, result as state) (lines: string list) lineNum =
match lines with
| [] -> state
| lineStr :: rest ->
match lineStr.TrimStart(), currentComment with
| Comment commentType, Some comment ->
let getCommentRanges trivia (lines: ReadOnlyMemory<char>[]) =
let rec loop (struct (lastLineNum, currentComment, result) as state) lineNum =
if lineNum = lines.Length then
state
else
match commentTypeOf (lines[lineNum].Span.TrimStart()), currentComment with
| ValueSome commentType, Some comment ->
loop
(if comment.Type = commentType && lineNum = lastLineNum + 1 then
comment.Lines.Add(lineNum, lineStr)
lineNum, currentComment, result
comment.Lines.Add lineNum
struct (lineNum, currentComment, result)
else
let comments = CommentList.New commentType (lineNum, lineStr)
lineNum, Some comments, comment :: result)
rest
let comments = CommentList.New commentType lineNum
struct (lineNum, Some comments, comment :: result))
(lineNum + 1)
| Comment commentType, None ->
let comments = CommentList.New commentType (lineNum, lineStr)
loop (lineNum, Some comments, result) rest (lineNum + 1)
| _, Some comment -> loop (lineNum, None, comment :: result) rest (lineNum + 1)
| _ -> loop (lineNum, None, result) rest (lineNum + 1)
| ValueSome commentType, None ->
let comments = CommentList.New commentType lineNum
loop (struct (lineNum, Some comments, result)) (lineNum + 1)
| ValueNone, Some comment -> loop (struct (lineNum, None, comment :: result)) (lineNum + 1)
| ValueNone, None -> loop (struct (lineNum, None, result)) (lineNum + 1)

let comments =
let _, lastComment, comments = loop (-1, None, []) (List.ofArray lines) 0
let struct (_, lastComment, comments) = loop (struct (-1, None, [])) 0

match lastComment with
| Some comment -> comment :: comments
| _ -> comments
|> List.rev

comments
|> List.filter (fun comment -> comment.Lines.Count > 1)
|> List.map (fun comment ->
let lines = comment.Lines
let startLine, startStr = lines[0]
let endLine, endStr = lines[lines.Count - 1]
let startCol = startStr.IndexOf '/'
let endCol = endStr.TrimEnd().Length
|> Seq.filter (fun comment -> comment.Lines.Count > 1)
|> Seq.map (fun comment ->
let startLine = comment.Lines[0]
let endLine = comment.Lines[comment.Lines.Count - 1]
let startCol = lines[startLine].Span.IndexOf '/'
let endCol = lines[endLine].Span.TrimEnd().Length

let scopeType =
match comment.Type with
Expand Down Expand Up @@ -1104,3 +1108,6 @@ module Structure =
getCommentRanges file.Trivia.CodeComments sourceLines

acc :> seq<_>

let getOutliningRanges (sourceLines: string[]) (parsedInput: ParsedInput) =
getOutliningRangesFromLineSlices (sourceLines |> Array.map _.AsMemory()) parsedInput
8 changes: 8 additions & 0 deletions src/Compiler/Service/ServiceStructure.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace FSharp.Compiler.EditorServices

open System
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text

Expand Down Expand Up @@ -80,3 +81,10 @@ module public Structure =

/// Returns outlining ranges for given parsed input.
val getOutliningRanges: sourceLines: string[] -> parsedInput: ParsedInput -> seq<ScopeRange>

/// <summary>
/// Returns outlining ranges for given parsed input, taking the source lines as slices of text the
/// caller already holds rather than as strings of their own.
/// </summary>
val internal getOutliningRangesFromLineSlices:
sourceLines: ReadOnlyMemory<char>[] -> parsedInput: ParsedInput -> seq<ScopeRange>
58 changes: 58 additions & 0 deletions src/Compiler/Utilities/illib.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ open System.Collections.Generic
open System.Collections.Concurrent
open System.Diagnostics
open System.IO
open System.Linq
open System.Threading
open System.Threading.Tasks
open System.Runtime.CompilerServices
Expand Down Expand Up @@ -112,6 +113,63 @@ module internal PervasiveAutoOpens =
member inline x.IndexOfOrdinal(value: string, startIndex, count) =
x.IndexOf(value, startIndex, count, StringComparison.Ordinal)

[<AbstractClass; Sealed>]
type ReadOnlySpanCharExtensions =

static member inline StartsWithOrdinal(str: ReadOnlySpan<char>, value: ReadOnlySpan<char>) =
str.StartsWith(value, StringComparison.Ordinal)

static member inline StartsWithOrdinal(str: ReadOnlySpan<char>, value: string) =
str.StartsWith(value.AsSpan(), StringComparison.Ordinal)

static member inline EndsWithOrdinal(str: ReadOnlySpan<char>, value: ReadOnlySpan<char>) =
str.EndsWith(value, StringComparison.Ordinal)

static member inline EndsWithOrdinal(str: ReadOnlySpan<char>, value: string) =
str.EndsWith(value.AsSpan(), StringComparison.Ordinal)

static member inline EndsWithOrdinalIgnoreCase(str: ReadOnlySpan<char>, value: ReadOnlySpan<char>) =
str.EndsWith(value, StringComparison.OrdinalIgnoreCase)

static member inline EndsWithOrdinalIgnoreCase(str: ReadOnlySpan<char>, value: string) =
str.EndsWith(value.AsSpan(), StringComparison.OrdinalIgnoreCase)

static member IndexOf(str: ReadOnlySpan<char>, value: char) =
let mutable index = -1
let mutable i = 0

while i < str.Length && index = -1 do
if str[i] = value then index <- i else i <- i + 1

index

static member inline IndexOfOrdinal(str: ReadOnlySpan<char>, value: ReadOnlySpan<char>) =
str.IndexOf(value, StringComparison.Ordinal)

static member inline IndexOfOrdinal(str: ReadOnlySpan<char>, value: string) =
str.IndexOf(value.AsSpan(), StringComparison.Ordinal)

// Searching a slice answers with an index into that slice, so the offset goes back on to
// report a position in `str` - what the String siblings these mirror return. A miss stays -1.

static member inline IndexOfOrdinal(str: ReadOnlySpan<char>, value: ReadOnlySpan<char>, startIndex) =
let i = str.Slice(startIndex).IndexOf(value, StringComparison.Ordinal)
if i < 0 then i else i + startIndex

static member inline IndexOfOrdinal(str: ReadOnlySpan<char>, value: string, startIndex) =
let i = str.Slice(startIndex).IndexOf(value.AsSpan(), StringComparison.Ordinal)
if i < 0 then i else i + startIndex

static member inline IndexOfOrdinal(str: ReadOnlySpan<char>, value: ReadOnlySpan<char>, startIndex, count) =
let i = str.Slice(startIndex, count).IndexOf(value, StringComparison.Ordinal)
if i < 0 then i else i + startIndex

static member inline IndexOfOrdinal(str: ReadOnlySpan<char>, value: string, startIndex, count) =
let i =
str.Slice(startIndex, count).IndexOf(value.AsSpan(), StringComparison.Ordinal)

if i < 0 then i else i + startIndex

/// Get an initialization hole
let getHole (r: _ ref) =
match r.Value with
Expand Down
61 changes: 61 additions & 0 deletions src/Compiler/Utilities/illib.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,67 @@ module internal PervasiveAutoOpens =

member inline IndexOfOrdinal: value: string * startIndex: int * count: int -> int

[<AbstractClass; Sealed; Extension>]
type ReadOnlySpanCharExtensions =

[<Extension>]
static member inline StartsWithOrdinal: str: ReadOnlySpan<char> * value: ReadOnlySpan<char> -> bool

[<Extension>]
static member inline StartsWithOrdinal: str: ReadOnlySpan<char> * value: string -> bool

[<Extension>]
static member inline EndsWithOrdinal: str: ReadOnlySpan<char> * value: ReadOnlySpan<char> -> bool

[<Extension>]
static member inline EndsWithOrdinal: str: ReadOnlySpan<char> * value: string -> bool

[<Extension>]
static member inline EndsWithOrdinalIgnoreCase: str: ReadOnlySpan<char> * value: ReadOnlySpan<char> -> bool

[<Extension>]
static member inline EndsWithOrdinalIgnoreCase: str: ReadOnlySpan<char> * value: string -> bool

[<Extension>]
static member IndexOf: str: ReadOnlySpan<char> * value: char -> int

[<Extension>]
static member inline IndexOfOrdinal: str: ReadOnlySpan<char> * value: ReadOnlySpan<char> -> int

[<Extension>]
static member inline IndexOfOrdinal: str: ReadOnlySpan<char> * value: string -> int

/// <summary>
/// Returns a position in <paramref name="str"/>, not in the slice searched, matching the
/// <see cref="T:System.String"/> overloads these mirror. -1 when there is no match.
Comment thread
xperiandri marked this conversation as resolved.
/// </summary>
[<Extension>]
static member inline IndexOfOrdinal:
str: ReadOnlySpan<char> * value: ReadOnlySpan<char> * startIndex: int -> int

/// <summary>
/// Returns a position in <paramref name="str"/>, not in the slice searched, matching the
/// <see cref="T:System.String"/> overloads these mirror. -1 when there is no match.
/// </summary>
[<Extension>]
static member inline IndexOfOrdinal: str: ReadOnlySpan<char> * value: string * startIndex: int -> int

/// <summary>
/// Returns a position in <paramref name="str"/>, not in the slice searched, matching the
/// <see cref="T:System.String"/> overloads these mirror. -1 when there is no match.
/// </summary>
[<Extension>]
static member inline IndexOfOrdinal:
str: ReadOnlySpan<char> * value: ReadOnlySpan<char> * startIndex: int * count: int -> int

/// <summary>
/// Returns a position in <paramref name="str"/>, not in the slice searched, matching the
/// <see cref="T:System.String"/> overloads these mirror. -1 when there is no match.
/// </summary>
[<Extension>]
static member inline IndexOfOrdinal:
str: ReadOnlySpan<char> * value: string * startIndex: int * count: int -> int

type Async with

/// Runs the computation synchronously, always starting on the current thread.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,12 @@
<ProjectReference Include="..\service\data\CSharp_Analysis\CSharp_Analysis.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
<!-- FSharp.Compiler.Service's PackageReference to System.Memory (for ReadOnlySpan/ReadOnlyMemory) is only
transitive here, so the SetTargetFramework override above does not carry it in on net472; pin it directly.
Not needed on the .NET Core inner build: there System.Memory ships with the framework and NuGet's
package-pruning check (NU1510) errors on an explicit reference to it. -->
<PackageReference Include="System.Memory" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions vsintegration/src/FSharp.Editor/Common/Extensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,14 @@ type SourceText with
member this.ToFSharpSourceText() =
SourceText.weakTable.GetValue(this, Runtime.CompilerServices.ConditionalWeakTable<_, _>.CreateValueCallback(SourceText.create))

/// The lines of the text, as slices of a single string rather than one string per line.
member this.GetLinesAsMemory() =
let text = this.ToString()

Array.init this.Lines.Count (fun i ->
let line = this.Lines[i]
text.AsMemory(line.Start, line.End - line.Start))

type NavigationItem with

member x.RoslynGlyph: FSharpRoslynGlyph =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ module internal BlockStructure =
let ellipsis = "..."

let createBlockSpans isBlockStructureEnabled (sourceText: SourceText) (parsedInput: ParsedInput) =
let linetext = sourceText.Lines |> Seq.map (fun x -> x.ToString()) |> Seq.toArray
let linetext = sourceText.GetLinesAsMemory()

Structure.getOutliningRanges linetext parsedInput
Structure.getOutliningRangesFromLineSlices linetext parsedInput
|> Seq.distinctBy (fun x -> x.Range.StartLine)
|> Seq.chooseV (fun scopeRange ->
// the range of text to collapse
Expand Down
Loading