Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 additions & 0 deletions src/Compiler/Service/QuickParse.fs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ module QuickParse =
&& (lineStr[index] = '|' || IsIdentifierPartCharacter lineStr[index])
->
Some index
// Handle optional parameter syntax: if we're on '?' and the next char is an identifier, use the next position
| _ when
(index < lineStr.Length)
&& lineStr[index] = '?'
&& (index + 1 < lineStr.Length)
&& IsIdentifierPartCharacter lineStr[index + 1]
->
Some(index + 1)
| _ -> None // not on a word or '.'

let (|Char|_|) p =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<Compile Include="RangeTests.fs" />
<Compile Include="TooltipTests.fs" />
<Compile Include="TokenizerTests.fs" />
<Compile Include="QuickParseTests.fs" />
<Compile Include="CompilerTestHelpers.fs" />
<Compile Include="ManglingNameOfProvidedTypes.fs" />
<Compile Include="HashIfExpression.fs" />
Expand Down
67 changes: 67 additions & 0 deletions tests/FSharp.Compiler.Service.Tests/QuickParseTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module FSharp.Compiler.Service.Tests.QuickParseTests

open Xunit
open FSharp.Compiler.EditorServices

[<Fact>]
let ``QuickParse handles optional parameter identifier extraction when cursor is on question mark``() =
let lineStr = "member _.memb(?optional:string) = optional"

// Test when cursor is exactly on the '?' character
let posOnQuestionMark = 14
Assert.Equal('?', lineStr[posOnQuestionMark])

let island = QuickParse.GetCompleteIdentifierIsland false lineStr posOnQuestionMark

// We expect to get "optional" as the identifier
Assert.True(Option.isSome island, "Should extract identifier island when positioned on '?'")

match island with
| Some(ident, startCol, isQuoted) ->
Assert.Equal("optional", ident)
Assert.False(isQuoted)
// The identifier should start after the '?'
Assert.True(startCol >= 15, sprintf "Start column %d should be >= 15" startCol)
| None ->
Assert.Fail("Expected to find identifier 'optional' when positioned on '?'")

[<Fact>]
let ``QuickParse handles optional parameter identifier extraction when cursor is on identifier``() =
let lineStr = "member _.memb(?optional:string) = optional"

// Test when cursor is on the identifier "optional" after the '?'
let posOnOptional = 17
Assert.Equal('t', lineStr[posOnOptional])

let island = QuickParse.GetCompleteIdentifierIsland false lineStr posOnOptional

// We expect to get "optional" as the identifier
Assert.True(Option.isSome island, "Should extract identifier island when positioned on identifier")

match island with
| Some(ident, startCol, isQuoted) ->
Assert.Equal("optional", ident)
Assert.False(isQuoted)
| None ->
Assert.Fail("Expected to find identifier 'optional'")

[<Fact>]
let ``QuickParse does not treat question mark as identifier in other contexts``() =
let lineStr = "let x = y ? z"

// Test when cursor is on the '?' in a different context (not optional parameter)
let posOnQuestionMark = 10
Assert.Equal('?', lineStr[posOnQuestionMark])

let island = QuickParse.GetCompleteIdentifierIsland false lineStr posOnQuestionMark

// In this context, '?' is followed by space, not an identifier start
// So we should get None or the next identifier 'z'
// Let's check what we actually get
match island with
| Some(ident, _, _) ->
// If we get something, it should be 'z' (the next identifier after the space)
Assert.Equal("z", ident)
| None ->
// Or we might get None, which is also acceptable
()
23 changes: 23 additions & 0 deletions tests/FSharp.Compiler.Service.Tests/TokenizerTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,26 @@ let ``Unfinished idents``() =
["IDENT", "```"]]

actual |> Assert.shouldBe expected

[<Fact>]
let ``Tokenizer test - optional parameters with question mark``() =
let tokenizedLines =
tokenizeLines
[| "member _.memb(?optional:string) = optional" |]

let actual =
[ for lineNo, lineToks in tokenizedLines do
yield lineNo, [ for str, info in lineToks do yield info.TokenName, str ] ]

let expected =
[(0,
[("MEMBER", "member"); ("WHITESPACE", " "); ("UNDERSCORE", "_"); ("DOT", ".");
("IDENT", "memb"); ("LPAREN", "("); ("QMARK", "?");
("IDENT", "optional"); ("COLON", ":"); ("IDENT", "string");
("RPAREN", ")"); ("WHITESPACE", " "); ("EQUALS", "="); ("WHITESPACE", " ");
("IDENT", "optional")])]

if actual <> expected then
printfn "actual = %A" actual
printfn "expected = %A" expected
actual |> Assert.shouldBeEqualWith expected (sprintf "actual and expected did not match,actual =\n%A\nexpected=\n%A\n" actual expected)
Loading