Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7192705
Avoid leaking a MeterListener per Cache in DEBUG builds (#19995)
NatElkins Jul 8, 2026
3ebcc74
Bump FCSMinorVersion to 13 (keep main above 10.0.4xx servicing 43.12.…
T-Gro Jul 9, 2026
87ad51b
Update dependencies from https://github.com/dotnet/msbuild build 2026…
dotnet-maestro[bot] Jul 9, 2026
d11945a
Update dependencies from https://github.com/dotnet/roslyn build 20260…
dotnet-maestro[bot] Jul 9, 2026
60d315a
Add ResetCompilerGeneratedNameState to compiler-generated name genera…
NatElkins Jul 9, 2026
5928e91
Update dependencies from https://github.com/dotnet/msbuild build 2026…
dotnet-maestro[bot] Jul 10, 2026
4eefd05
[main] Update dependencies from dotnet/msbuild (#20055)
dotnet-maestro[bot] Jul 21, 2026
2cd254e
[main] Update dependencies from dotnet/roslyn (#20052)
dotnet-maestro[bot] Jul 21, 2026
9fc230a
Localized file check-in by OneLocBuild Task: Build definition ID 499:…
dotnet-bot Jul 21, 2026
e729d97
[main] Update dependencies from dotnet/arcade (#20054)
dotnet-maestro[bot] Jul 22, 2026
69fca7f
Tests/source context: support multiple carets (#20077)
auduchinok Jul 22, 2026
2f07589
Move VS language-service logic tests to FSharp.Compiler.Service.Tests…
T-Gro Jul 23, 2026
24d731a
Add Roslyn-format EnC CustomDebugInformation codec and portable PDB m…
NatElkins Jul 24, 2026
2e838bb
Add stable synthesized-name replay infrastructure for hot reload (#20…
NatElkins Jul 24, 2026
4a2749e
Fix #19457: lift CE constructs from plain let RHS in computation expr…
T-Gro Jul 24, 2026
1d8dc39
Fix attribute resolution in recursive module/namespace scopes (#19744)
T-Gro Jul 24, 2026
1dc395a
Correct StructLayout size emission for data-less struct unions (#19759)
Copilot Jul 24, 2026
8c0e444
Report FS3888 for generic attribute type abbreviations instead of FS0…
T-Gro Jul 26, 2026
fd6ed49
Move to .NET 11 (SDK, Arcade, product TargetFramework) (#20080)
T-Gro Jul 28, 2026
0c94e4e
Support NotNullIfNotNullAttribute (#19977)
kerams Jul 28, 2026
5dfbf7f
Move SDL/TSA validation to 1ES templates after Arcade 11 upgrade (#20…
abonie Jul 29, 2026
17cb503
Compiled ToStrings under -reflectionfree for DUs and Records (#19976)
charlesroddie Jul 30, 2026
c00299f
Run ilverify via the tool manifest instead of a hard-coded cache path…
abonie Jul 30, 2026
f4b785f
Record spreads (#18927)
brianrourkeboll Aug 1, 2026
d3403ca
Implement interpolated strings via String.Concat (#19971)
charlesroddie Aug 1, 2026
f5c88eb
[main] Source code updates from dotnet/dotnet (#20058)
dotnet-maestro[bot] Aug 3, 2026
647548b
Secure release-note checks for fork pull requests (#20081)
NatElkins Aug 3, 2026
536800c
Update test project to net11 (#20104)
abonie Aug 3, 2026
a6b69ce
Update dependencies from https://github.com/dotnet/arcade build 20260…
dotnet-maestro[bot] Aug 4, 2026
3cc7755
Move to Roslyn's unified ExternalAccess library (#20099)
JoeRobich Aug 4, 2026
ea778bb
LexFilter: drop non-strict mode (#20106)
auduchinok Aug 4, 2026
36ce348
Enable Central Package Management with transitive pinning (#20084)
T-Gro Aug 4, 2026
d89529c
Implement direct delegates (#19993)
kerams Aug 4, 2026
7e2af62
Merge remote-tracking branch 'origin/main' into darc-release/10.0.4xx…
invalid-email-address Aug 4, 2026
92a3700
Fix check_release_notes 403 by restoring pull-requests: write and mak…
T-Gro Aug 4, 2026
8ed0ce1
Restore NuGetRepack UsingTask workaround for Arcade 10 in Microsoft.F…
invalid-email-address Aug 4, 2026
4d65a64
Merge remote-tracking branch 'origin/main' into darc-release/10.0.4xx…
invalid-email-address Aug 4, 2026
3bc2155
Update dependencies from https://github.com/dotnet/arcade build 20260…
dotnet-maestro[bot] Aug 5, 2026
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
Prev Previous commit
Next Next commit
Tests/source context: support multiple carets (#20077)
  • Loading branch information
auduchinok authored Jul 22, 2026
commit 69fca7f6e1412e3272a3a4224608cbae4ad165f4
148 changes: 108 additions & 40 deletions tests/FSharp.Compiler.Service.Tests/Checker.fs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
namespace FSharp.Compiler.Service.Tests

open System
open System.Text.RegularExpressions
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Text
open FSharp.Compiler.Text.Range
open FSharp.Compiler.Tokenization
open FSharp.Test.Assert

type SourceContext =
{ Source: string
Expand All @@ -32,83 +34,149 @@ type CodeCompletionContext =

[<RequireQualifiedAccess>]
module SourceContext =
let private markers = ["{caret}"; "{selstart}"; "{selend}"]
type private Marker =
{ Text: string
Position: pos }

type private SourceMarkers =
{ Caret: Marker option
SelectionStart: Marker option
SelectionEnd: Marker option
Id: int option }

let private markers = ["caret"; "selstart"; "selend"]

let getLines (source: string) =
source.Split([|"\r\n"; "\n"|], StringSplitOptions.None)

let rec private extractMarkersOnLine markersAcc (line, lineText: string) =
let private stripMarkers (markedSource: string) =
let names = String.concat "|" markers
Regex.Replace(markedSource, $@"\{{({names})\d*\}}", "")

let rec private extractMarkersOnLine (markersAcc: (int option * Marker) list) (line, lineText: string) =
let markersOnLine =
markers
|> List.choose (fun (marker: string) ->
match lineText.IndexOf(marker) with
| -1 -> None
| column -> Some(marker, column)
let regexMatch = Regex.Match(lineText, $@"\{{{marker}(\d*)\}}")
if regexMatch.Success then Some(marker, regexMatch) else None
)

if markersOnLine.IsEmpty then
markersAcc
else
let marker, column = List.minBy snd markersOnLine
let marker, regexMatch = markersOnLine |> List.minBy (fun (_, m) -> m.Index)

let markerPos =
let column =
match marker with
| "{caret}" -> column - 1
| _ -> column
let column =
match marker with
| "caret" -> regexMatch.Index - 1
| _ -> regexMatch.Index

Position.mkPos (line + 1) column
let markerPos = Position.mkPos (line + 1) column

if markersAcc |> List.map fst |> List.contains marker then
failwith $"Duplicate marker: {marker}"
let id =
match regexMatch.Groups.[1].Value with
| "" -> None
| value -> Some(int value)

let markersAcc = (marker, markerPos) :: markersAcc
let lineText = lineText.Replace(marker, "")
if markersAcc |> List.exists (fun (markerId, m) -> m.Text = marker && markerId = id) then
failwith $"Duplicate marker: {regexMatch.Value}"

let markersAcc = (id, { Text = marker; Position = markerPos }) :: markersAcc
let lineText = lineText.Replace(regexMatch.Value, "")

extractMarkersOnLine markersAcc (line, lineText)

let fromMarkedSource (markedSource: string) : SourceContext =
let markerPositions =
let private extractSourceMarkers (markedSource: string) : string * SourceMarkers list =
let sourceMarkers =
getLines markedSource
|> Seq.indexed
|> Seq.fold extractMarkersOnLine []
|> List.groupBy fst
|> List.map (fun (id, group) ->
let markers = group |> List.map snd
let tryFind text = markers |> List.tryFind (fun m -> m.Text = text)

let source =
markerPositions
|> List.map fst
|> List.fold (fun (source: string) marker -> source.Replace(marker, "")) markedSource
{ Id = id
Caret = tryFind "caret"
SelectionStart = tryFind "selstart"
SelectionEnd = tryFind "selend" })

let markerPositions = markerPositions |> dict
stripMarkers markedSource, sourceMarkers

let tryGetPos marker =
match markerPositions.TryGetValue(marker) with
| true, pos -> Some pos
| _ -> None
let private toSourceContext (source: string) (sourceMarkers: SourceMarkers) : SourceContext =
let reportError message =
let prefix =
match sourceMarkers with
| { Id = Some id } -> $"{id}: "
| _ -> ""

failwith (prefix + message)

let caretPos, selectedRange =
match tryGetPos "{caret}", tryGetPos "{selstart}", tryGetPos "{selend}" with
| Some caretPos, None, None ->
caretPos, None
match sourceMarkers.Caret, sourceMarkers.SelectionStart, sourceMarkers.SelectionEnd with
| Some caret, None, None ->
caret.Position, None

| Some caretPos, Some startPos, Some endPos ->
let selectedRange = mkRange "Test.fsx" startPos endPos
caretPos, Some selectedRange
| Some caret, Some selStart, Some selEnd ->
let selectedRange = mkRange "Test.fsx" selStart.Position selEnd.Position
caret.Position, Some selectedRange

| None, Some startPos, Some endPos ->
let selectedRange = mkRange "Test.fsx" startPos endPos
let caretPos = Position.mkPos endPos.Line (endPos.Column - 1)
| None, Some selStart, Some selEnd ->
let selectedRange = mkRange "Test.fsx" selStart.Position selEnd.Position
let caretPos = Position.mkPos selEnd.Position.Line (selEnd.Position.Column - 1)
caretPos, Some selectedRange

| _, None, Some _ -> failwith "Missing selected range start"
| _, Some _, None -> failwith "Missing selected range end"

| None, None, None -> failwith "Missing caret marker"
| _, None, Some _ -> reportError "Missing selected range start"
| _, Some _, None -> reportError "Missing selected range end"
| None, None, None -> reportError "Missing caret marker"

let lines = getLines source
let lineText = Array.get lines (caretPos.Line - 1)

{ Source = source; CaretPos = caretPos; LineText = lineText; SelectedRange = selectedRange }

let fromMarkedSource (markedSource: string) : SourceContext =
let source, sourceMarkers = extractSourceMarkers markedSource
let sourceMarkers = sourceMarkers |> List.exactlyOne
sourceMarkers.Id |> shouldBe None

toSourceContext source sourceMarkers

let fromOrderedMarkedSource (orderedMarkedSource: string) : SourceContext list =
let source, sourceMarkers = extractSourceMarkers orderedMarkedSource
sourceMarkers |> List.iter (fun markers -> markers.Id.IsSome |> shouldBeTrue)

sourceMarkers
|> List.sortBy _.Id
|> List.map (toSourceContext source)

let toMarkedSource (context: SourceContext) : string =
let skipCaretMarker =
match context.SelectedRange with
| Some range -> context.CaretPos.Line = range.End.Line && context.CaretPos.Column = range.End.Column - 1
| None -> false

let insertions =
[ if not skipCaretMarker then
context.CaretPos.Line, context.CaretPos.Column + 1, "{caret}"

match context.SelectedRange with
| Some range ->
range.Start.Line, range.Start.Column, "{selstart}"
range.End.Line, range.End.Column, "{selend}"
| None -> () ]

let lines = getLines context.Source

for line, column, marker in insertions |> List.sortByDescending (fun (line, column, _) -> line, column) do
lines[line - 1] <- lines[line - 1].Insert(column, marker)

String.concat "\n" lines

let extractOrderedMarkedSources (markedSource: string) : string list =
fromOrderedMarkedSource markedSource
|> List.map toMarkedSource


[<AutoOpen>]
module CheckResultsExtensions =
Expand Down
12 changes: 12 additions & 0 deletions tests/FSharp.Compiler.Service.Tests/CheckerExtensionsTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module FSharp.Compiler.Service.Tests.CheckerExtensionsTests

open Xunit
open FSharp.Test.Assert

[<Fact>]
let ``Extract ordered marked sources`` () =
let markedSources = SourceContext.extractOrderedMarkedSources "let a{caret1}, b{caret2} = 1, 2"

markedSources
|> shouldBe [ "let a{caret}, b = 1, 2"
"let a, b{caret} = 1, 2" ]
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
</Compile>
<Compile Include="Common.fs" />
<Compile Include="Checker.fs" />
<Compile Include="CheckerExtensionsTests.fs" />
<Compile Include="TypeChecker\TypeCheckerRecoveryTests.fs" />
<Compile Include="TypeChecker\Obsolete.fs" />
<Compile Include="GeneratedCodeSymbolsTests.fs" />
Expand Down
Loading