Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
09e40d7
POC for improved interpolation
abonie Oct 21, 2022
c4e3308
Add new error for too many consecutive braces
abonie Oct 26, 2022
295056e
Handle percent sign characters
abonie Dec 7, 2022
4570987
Fix closing braces error
abonie Dec 8, 2022
c7391a3
Fix error reporting on too many lbraces
abonie Dec 8, 2022
821a62e
Encode # of $ in interpolated strings
abonie Jan 13, 2023
9a47de2
Rename numDollars to delimLength
abonie Jan 18, 2023
ca20665
Add unit tests for multi-$ interpolated strings
abonie Jan 18, 2023
7fd2f50
Add tests for coloring interpolated strings
abonie Jan 19, 2023
8ebface
Add comments in lex.fsl, fix linter errors
abonie Jan 20, 2023
f80e136
Highlighting format specs in interpolated strings
abonie Feb 21, 2023
e7b6640
Add tests for coloring of format specifiers
abonie Feb 24, 2023
50d7423
Fix brace matching for many braces
abonie Mar 1, 2023
88cce36
Add baseline tests for syntax tree
abonie Mar 21, 2023
92d8c34
Rename variable
abonie Mar 22, 2023
6175016
Add test for nested interpolated strings coloring
abonie Mar 27, 2023
50a572e
Fix syntax highlighting with nested strings
abonie Apr 17, 2023
2d13f31
Extract lexer changes into a new lexer rule
abonie Apr 17, 2023
a6826f6
Update comments in lex.fsl, refactor slightly
abonie Apr 18, 2023
63390c3
Put lexer change under feature flag
abonie Apr 19, 2023
1bef0cd
Pass lang version to FSharpSourceTokenizer
abonie Apr 21, 2023
7749a86
Rename delimLength to interpolationDelimiterLength
abonie Apr 25, 2023
ef8caee
Remove an unnecessary check
abonie Apr 25, 2023
503e5e2
Use errorR rather than diagnosticsLogger.ErrorR
abonie Apr 26, 2023
d33883b
Apply code review comments
abonie Apr 27, 2023
0e5434a
Add more brace matching tests
abonie Apr 27, 2023
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
64 changes: 44 additions & 20 deletions src/Compiler/Checking/CheckFormatStrings.fs
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,28 @@ let makeFmts (context: FormatStringCheckContext) (fragRanges: range list) (fmt:
let sourceText = context.SourceText
let lineStartPositions = context.LineStartPositions

// Number of curly braces required to delimiter interpolation holes
// = Number of $ chars starting a (triple quoted) string literal
// Set when we process first fragment range, default = 1
let mutable delimLen = 1

let mutable nQuotes = 1
[ for i, r in List.indexed fragRanges do
if r.StartLine - 1 < lineStartPositions.Length && r.EndLine - 1 < lineStartPositions.Length then
let startIndex = lineStartPositions[r.StartLine - 1] + r.StartColumn
let rLength = lineStartPositions[r.EndLine - 1] + r.EndColumn - startIndex
let offset =
if i = 0 then
match sourceText.GetSubTextString(startIndex, rLength) with
| PrefixedBy "$\"\"\"" len
| PrefixedBy "\"\"\"" len ->
let fullRangeText = sourceText.GetSubTextString(startIndex, rLength)
delimLen <-
fullRangeText
|> Seq.takeWhile (fun c -> c = '$')
|> Seq.length
let tripleQuotePrefix =
[String.replicate delimLen "$"; "\"\"\""]
|> String.concat ""
match fullRangeText with
| PrefixedBy tripleQuotePrefix len ->
nQuotes <- 3
len
| PrefixedBy "$@\"" len
Expand All @@ -91,13 +103,13 @@ let makeFmts (context: FormatStringCheckContext) (fragRanges: range list) (fmt:
| _ -> 1
else
1 // <- corresponds to '}' that's closing an interpolation hole
let fragLen = rLength - offset - (if i = numFrags - 1 then nQuotes else 1)
let fragLen = rLength - offset - (if i = numFrags - 1 then nQuotes else delimLen)
(offset, sourceText.GetSubTextString(startIndex + offset, fragLen), r)
else (1, fmt, r)
]
], delimLen


module internal Parsing =
module internal Parse =

let flags (info: FormatInfoRegister) (fmt: string) (fmtPos: int) =
let len = fmt.Length
Expand Down Expand Up @@ -231,10 +243,10 @@ let parseFormatStringInternal
//
let escapeFormatStringEnabled = g.langVersion.SupportsFeature Features.LanguageFeature.EscapeDotnetFormattableStrings

let fmt, fragments =
let fmt, fragments, delimLen =
match context with
| Some context when fragRanges.Length > 0 ->
let fmts = makeFmts context fragRanges fmt
let fmts, delimLen = makeFmts context fragRanges fmt

// Join the fragments with holes. Note this join is only used on the IDE path,
// the CheckExpressions.fs does its own joining with the right alignments etc. substituted
Expand All @@ -245,11 +257,11 @@ let parseFormatStringInternal
(0, fmts) ||> List.mapFold (fun i (offset, fmt, fragRange) ->
(i, offset, fragRange), i + fmt.Length + 4) // the '4' is the length of '%P()' joins

fmt, fragments
fmt, fragments, delimLen
| _ ->
// Don't muck with the fmt when there is no source code context to go get the original
// source code (i.e. when compiling or background checking)
(if escapeFormatStringEnabled then escapeDotnetFormatString fmt else fmt), [ (0, 1, m) ]
(if escapeFormatStringEnabled then escapeDotnetFormatString fmt else fmt), [ (0, 1, m) ], 1

let len = fmt.Length

Expand Down Expand Up @@ -299,32 +311,44 @@ let parseFormatStringInternal

and parseSpecifier acc (i, fragLine, fragCol) fragments =
let startFragCol = fragCol
let fragCol = fragCol+1
if fmt[i..(i+1)] = "%%" then
let nPercentSigns =
fmt[i..]
|> Seq.takeWhile (fun c -> c = '%')
|> Seq.length
if delimLen <= 1 && fmt[i..(i+1)] = "%%" then
match context with
| Some _ ->
specifierLocations.Add(
(Range.mkFileIndexRange m.FileIndex
(Position.mkPos fragLine startFragCol)
(Position.mkPos fragLine (fragCol + 1))), 0)
(Position.mkPos fragLine fragCol)
(Position.mkPos fragLine (fragCol+2))), 0)
| None -> ()
appendToDotnetFormatString "%"
parseLoop acc (i+2, fragLine, fragCol+1) fragments
parseLoop acc (i+2, fragLine, fragCol+2) fragments
elif delimLen > 1 && nPercentSigns < delimLen then
appendToDotnetFormatString fmt[i..(i+nPercentSigns-1)]
parseLoop acc (i + nPercentSigns, fragLine, fragCol + nPercentSigns) fragments
else
let i = i+1
let fragCol, i =
if delimLen > 1 then
if nPercentSigns > delimLen then
"%" |> String.replicate (nPercentSigns - delimLen) |> appendToDotnetFormatString
fragCol + nPercentSigns, i + nPercentSigns
else
fragCol + 1, i + 1
if i >= len then failwith (FSComp.SR.forMissingFormatSpecifier())
let info = newInfo()

let oldI = i
let posi, i = Parsing.position fmt i
let posi, i = Parse.position fmt i
let fragCol = fragCol + i - oldI

let oldI = i
let i = Parsing.flags info fmt i
let i = Parse.flags info fmt i
let fragCol = fragCol + i - oldI

let oldI = i
let widthArg,(widthValue, (precisionArg,i)) = Parsing.widthAndPrecision info fmt i
let widthArg,(widthValue, (precisionArg,i)) = Parse.widthAndPrecision info fmt i
let fragCol = fragCol + i - oldI

if i >= len then failwith (FSComp.SR.forBadPrecision())
Expand All @@ -340,7 +364,7 @@ let parseFormatStringInternal
| Some n -> failwith (FSComp.SR.forDoesNotSupportPrefixFlag(c.ToString(), n.ToString()))
| None -> ()

let skipPossibleInterpolationHole pos = Parsing.skipPossibleInterpolationHole isInterpolated isFormattableString fmt pos
let skipPossibleInterpolationHole pos = Parse.skipPossibleInterpolationHole isInterpolated isFormattableString fmt pos

// Implicitly typed holes in interpolated strings are translated to '... %P(...)...' in the
// type checker. They should always have '(...)' after for format string.
Expand Down
5 changes: 5 additions & 0 deletions src/Compiler/FSComp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,10 @@ lexIfOCaml,"IF-FSHARP/IF-CAML regions are no longer supported"
1245,lexInvalidUnicodeLiteral,"\U%s is not a valid Unicode character escape sequence"
1246,tcCallerInfoWrongType,"'%s' must be applied to an argument of type '%s', but has been applied to an argument of type '%s'"
1247,tcCallerInfoNotOptional,"'%s' can only be applied to optional arguments"
1248,lexTooManyLBracesInTripleQuote,"The interpolated triple quoted string literal does not start with enough '$' characters to allow this many consecutive opening braces as content."
1249,lexUnmatchedRBracesInTripleQuote,"The interpolated string contains unmatched closing braces."
1250,lexTooManyPercentsInTripleQuote,"The interpolated triple quoted string literal does not start with enough '$' characters to allow this many consecutive '%%' characters."
1251,lexExtendedStringInterpolationNotSupported,"Extended string interpolation is not supported in this version of F#."
# reshapedmsbuild.fs
1300,toolLocationHelperUnsupportedFrameworkVersion,"The specified .NET Framework version '%s' is not supported. Please specify a value from the enumeration Microsoft.Build.Utilities.TargetDotNetFrameworkVersion."
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -1567,6 +1571,7 @@ featureWarningWhenCopyAndUpdateRecordChangesAllFields,"Raises warnings when an c
featureStaticMembersInInterfaces,"Static members in interfaces"
featureNonInlineLiteralsAsPrintfFormat,"String values marked as literals and IL constants as printf format"
featureNestedCopyAndUpdate,"Nested record field copy-and-update"
featureExtendedStringInterpolation,"Extended string interpolation similar to C# raw string literals."
3353,fsiInvalidDirective,"Invalid directive '#%s %s'"
3354,tcNotAFunctionButIndexerNamedIndexingNotYetEnabled,"This value supports indexing, e.g. '%s.[index]'. The syntax '%s[index]' requires /langversion:preview. See https://aka.ms/fsharp-index-notation."
3354,tcNotAFunctionButIndexerIndexingNotYetEnabled,"This expression supports indexing, e.g. 'expr.[index]'. The syntax 'expr[index]' requires /langversion:preview. See https://aka.ms/fsharp-index-notation."
Expand Down
3 changes: 3 additions & 0 deletions src/Compiler/Facilities/LanguageFeatures.fs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type LanguageFeature =
| StaticMembersInInterfaces
| NonInlineLiteralsAsPrintfFormat
| NestedCopyAndUpdate
| ExtendedStringInterpolation

/// LanguageVersion management
type LanguageVersion(versionText) =
Expand Down Expand Up @@ -155,6 +156,7 @@ type LanguageVersion(versionText) =
LanguageFeature.StaticMembersInInterfaces, previewVersion
LanguageFeature.NonInlineLiteralsAsPrintfFormat, previewVersion
LanguageFeature.NestedCopyAndUpdate, previewVersion
LanguageFeature.ExtendedStringInterpolation, previewVersion

]

Expand Down Expand Up @@ -276,6 +278,7 @@ type LanguageVersion(versionText) =
| LanguageFeature.StaticMembersInInterfaces -> FSComp.SR.featureStaticMembersInInterfaces ()
| LanguageFeature.NonInlineLiteralsAsPrintfFormat -> FSComp.SR.featureNonInlineLiteralsAsPrintfFormat ()
| LanguageFeature.NestedCopyAndUpdate -> FSComp.SR.featureNestedCopyAndUpdate ()
| LanguageFeature.ExtendedStringInterpolation -> FSComp.SR.featureExtendedStringInterpolation ()

/// Get a version string associated with the given feature.
static member GetFeatureVersionString feature =
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Facilities/LanguageFeatures.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type LanguageFeature =
| StaticMembersInInterfaces
| NonInlineLiteralsAsPrintfFormat
| NestedCopyAndUpdate
| ExtendedStringInterpolation

/// LanguageVersion management
type LanguageVersion =
Expand Down
24 changes: 17 additions & 7 deletions src/Compiler/Service/FSharpCheckerResults.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2352,15 +2352,19 @@ module internal ParseAndCheckFile =

let rec matchBraces stack =
match lexfun lexbuf, stack with
| tok2, (tok1, m1) :: stackAfterMatch when parenTokensBalance tok1 tok2 ->
| tok2, (tok1, braceOffset, m1) :: stackAfterMatch when parenTokensBalance tok1 tok2 ->
let m2 = lexbuf.LexemeRange

// For INTERP_STRING_PART and INTERP_STRING_END grab the one character
// range that corresponds to the "}" at the start of the token
let m2Start =
match tok2 with
| INTERP_STRING_PART _
| INTERP_STRING_END _ -> mkFileIndexRange m2.FileIndex m2.Start (mkPos m2.Start.Line (m2.Start.Column + 1))
| INTERP_STRING_END _ ->
mkFileIndexRange
m2.FileIndex
(mkPos m2.Start.Line (m2.Start.Column - braceOffset))
(mkPos m2.Start.Line (m2.Start.Column + 1))
| _ -> m2

matchingBraces.Add(m1, m2Start)
Expand All @@ -2371,15 +2375,15 @@ module internal ParseAndCheckFile =
match tok2 with
| INTERP_STRING_PART _ ->
let m2End =
mkFileIndexRange m2.FileIndex (mkPos m2.End.Line (max (m2.End.Column - 1) 0)) m2.End
mkFileIndexRange m2.FileIndex (mkPos m2.End.Line (max (m2.End.Column - 1 - braceOffset) 0)) m2.End

(tok2, m2End) :: stackAfterMatch
(tok2, braceOffset, m2End) :: stackAfterMatch
| _ -> stackAfterMatch

matchBraces stackAfterMatch

| LPAREN | LBRACE _ | LBRACK | LBRACE_BAR | LBRACK_BAR | LQUOTE _ | LBRACK_LESS as tok, _ ->
matchBraces ((tok, lexbuf.LexemeRange) :: stack)
matchBraces ((tok, 0, lexbuf.LexemeRange) :: stack)

// INTERP_STRING_BEGIN_PART corresponds to $"... {" at the start of an interpolated string
//
Expand All @@ -2389,12 +2393,18 @@ module internal ParseAndCheckFile =
//
// Either way we start a new potential match at the last character
| INTERP_STRING_BEGIN_PART _ | INTERP_STRING_PART _ as tok, _ ->
let braceOffset =
match tok with
| INTERP_STRING_BEGIN_PART (_, SynStringKind.TripleQuote, (LexerContinuation.Token (_, (_, _, dl, _) :: _))) ->
dl - 1
| _ -> 0

let m = lexbuf.LexemeRange

let m2 =
mkFileIndexRange m.FileIndex (mkPos m.End.Line (max (m.End.Column - 1) 0)) m.End
mkFileIndexRange m.FileIndex (mkPos m.End.Line (max (m.End.Column - 1 - braceOffset) 0)) m.End

matchBraces ((tok, m2) :: stack)
matchBraces ((tok, braceOffset, m2) :: stack)

| (EOF _ | LEX_FAILURE _), _ -> ()
| _ -> matchBraces stack
Expand Down
Loading