Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

To interpolated string #1143

Merged
merged 9 commits into from
Jul 15, 2023
Merged
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
111 changes: 111 additions & 0 deletions src/FsAutoComplete.Core/FCSPatches.fs
Original file line number Diff line number Diff line change
Expand Up @@ -495,3 +495,114 @@ module SyntaxTreeOps =


walkExpr inpExpr

open System
open System.Reflection
open Microsoft.FSharp.Reflection

module ReflectionDelegates =

let public BindingFlagsToSeeAll: BindingFlags =
BindingFlags.Static
||| BindingFlags.FlattenHierarchy
||| BindingFlags.Instance
||| BindingFlags.NonPublic
||| BindingFlags.Public

let createFuncArity1<'returnType> (instanceType: Type) (arg1: Type) (getterName: string) =
let method = instanceType.GetMethod(getterName, BindingFlagsToSeeAll)

let getFunc =
typedefof<Func<_, _, _>>
.MakeGenericType(instanceType, arg1, typeof<'returnType>)

let delegate2 = method.CreateDelegate(getFunc)
// TODO: Emit IL for performance
fun (instance, arg1) -> delegate2.DynamicInvoke [| instance; arg1 |] |> unbox<bool>

let createGetter<'returnType> (instanceType: System.Type) (getterName: string) =
let method =
instanceType.GetProperty(getterName, BindingFlagsToSeeAll).GetGetMethod(true)

let getFunc =
typedefof<Func<_, _>>.MakeGenericType(instanceType, typeof<'returnType>)

let delegate2 = method.CreateDelegate(getFunc)
// TODO: Emit IL for performance
fun instance -> delegate2.DynamicInvoke [| instance |] |> unbox<bool>


/// <summary>
/// Reflection Shim around the <see href="https://github.com/dotnet/fsharp/blob/7725ddbd61ab3e5bf7e2fc35d76a0ece3903a5d9/src/Compiler/Facilities/LanguageFeatures.fs#L18">LanguageFeature</see> in FSharp.Compiler.Service
/// </summary>
type LanguageFeatureShim(langFeature: string) =
static let LanguageFeatureTy =
lazy (Type.GetType("FSharp.Compiler.Features+LanguageFeature, FSharp.Compiler.Service"))

static let cases =
lazy (FSharpType.GetUnionCases(LanguageFeatureTy.Value, ReflectionDelegates.BindingFlagsToSeeAll))

let case =
lazy
(let v = cases.Value |> Array.tryFind (fun c -> c.Name = langFeature)

v
|> Option.map (fun x -> FSharpValue.MakeUnion(x, [||], ReflectionDelegates.BindingFlagsToSeeAll)))

member x.Case = case.Value

static member Type = LanguageFeatureTy.Value

// Worth keeping commented out as they shouldn't be used until we need to find other properties/methods to support
// member x.Properties = LanguageFeatureShim.Type.GetProperties(ReflectionDelegates.BindingFlagsToSeeAll)
// member x.Methods = LanguageFeatureShim.Type.GetMethods(ReflectionDelegates.BindingFlagsToSeeAll)
// member x.Fields = LanguageFeatureShim.Type.GetFields(ReflectionDelegates.BindingFlagsToSeeAll)
// member x.Cases = cases.Value

/// <summary>
/// Reflection Shim around the <see href="https://github.com/dotnet/fsharp/blob/7725ddbd61ab3e5bf7e2fc35d76a0ece3903a5d9/src/Compiler/Facilities/LanguageFeatures.fs#L76">LanguageVersion</see> in FSharp.Compiler.Service
/// </summary>
type LanguageVersionShim(versionText: string) =
static let LanguageVersionTy =
lazy (Type.GetType("FSharp.Compiler.Features+LanguageVersion, FSharp.Compiler.Service"))

static let ctor = lazy (LanguageVersionTy.Value.GetConstructor([| typeof<string> |]))

static let isPreviewEnabled =
lazy (ReflectionDelegates.createGetter<bool> LanguageVersionTy.Value "IsPreviewEnabled")

static let supportsFeature =
lazy (ReflectionDelegates.createFuncArity1<bool> LanguageVersionTy.Value LanguageFeatureShim.Type "SupportsFeature")

let realLanguageVersion = ctor.Value.Invoke([| versionText |])

member x.IsPreviewEnabled = isPreviewEnabled.Value realLanguageVersion

member x.SupportsFeature(featureId: LanguageFeatureShim) =
match featureId.Case with
| None -> false
| Some x -> supportsFeature.Value(realLanguageVersion, x)

member x.Real = realLanguageVersion
static member Type = LanguageVersionTy.Value

// Worth keeping commented out as they shouldn't be used until we need to find other properties/methods to support
// member x.Properties = LanguageVersionShim.Type.GetProperties(ReflectionDelegates.BindingFlagsToSeeAll)
// member x.Methods = LanguageVersionShim.Type.GetMethods(ReflectionDelegates.BindingFlagsToSeeAll)
// member x.Fields = LanguageVersionShim.Type.GetFields(ReflectionDelegates.BindingFlagsToSeeAll)

module LanguageVersionShim =

/// <summary>Default is "latest"</summary>
/// <returns></returns>
let defaultLanguageVersion = lazy (LanguageVersionShim("latest"))

/// <summary>Tries to parse out "--langversion:" from OtherOptions if it can't find it, returns defaultLanguageVersion</summary>
/// <param name="fpo">The FSharpProjectOptions to use</param>
/// <returns>A LanguageVersionShim from the parsed "--langversion:" or defaultLanguageVersion </returns>
let fromFSharpProjectOptions (fpo: FSharpProjectOptions) =
fpo.OtherOptions
|> Array.tryFind (fun x -> x.StartsWith("--langversion:"))
|> Option.map (fun x -> x.Split(":")[1])
|> Option.map (fun x -> LanguageVersionShim(x))
|> Option.defaultWith (fun () -> defaultLanguageVersion.Value)
5 changes: 5 additions & 0 deletions src/FsAutoComplete.Core/State.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ open FSharp.Compiler.EditorServices
open FSharp.Compiler.Syntax
open FSharp.Compiler.CodeAnalysis
open FsToolkit.ErrorHandling
open FCSPatches

[<AutoOpen>]
module ProjInfoExtensions =
Expand Down Expand Up @@ -203,6 +204,10 @@ type State =

member x.FSharpProjectOptions = x.ProjectController.ProjectOptions

member x.LanguageVersions =
x.FSharpProjectOptions
|> Seq.map (fun (s, proj: FSharpProjectOptions) -> s, LanguageVersionShim.fromFSharpProjectOptions proj)

member x.TryGetFileVersion(file: string<LocalPath>) : int option =
x.Files.TryFind file |> Option.bind (fun f -> f.Version)

Expand Down
4 changes: 4 additions & 0 deletions src/FsAutoComplete/CodeFixes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type FcsPos = FSharp.Compiler.Text.Position
module LspTypes = Ionide.LanguageServerProtocol.Types

module Types =
open FsAutoComplete.FCSPatches
open System.Threading.Tasks

type IsEnabled = unit -> bool

Expand All @@ -29,6 +31,8 @@ module Types =
-> FSharp.Compiler.Text.Position
-> Async<ResultOrString<ParseAndCheckResults * string * IFSACSourceText>>

type GetLanguageVersion = string<LocalPath> -> Async<LanguageVersionShim>

type GetProjectOptionsForFile =
string<LocalPath> -> Async<ResultOrString<FSharp.Compiler.CodeAnalysis.FSharpProjectOptions>>

Expand Down
153 changes: 153 additions & 0 deletions src/FsAutoComplete/CodeFixes/ToInterpolatedString.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
module FsAutoComplete.CodeFix.ToInterpolatedString

open System.Text.RegularExpressions
open FsToolkit.ErrorHandling
open FsAutoComplete.CodeFix.Types
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete
open FsAutoComplete.LspHelpers
open FSharp.Compiler.Symbols
open FSharp.Compiler.Text.Range
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FsAutoComplete.FCSPatches

let title = "To interpolated string"

let languageFeature = lazy (LanguageFeatureShim("StringInterpolation"))

/// See https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/plaintext-formatting#format-specifiers-for-printf
let specifierRegex =
Regex("\\%(\\+|\\-)?\\d*(b|s|c|d|i|u|x|X|o|B|e|E|f|F|g|G|M|O|A)")

let validFunctionNames = set [| "printf"; "printfn"; "sprintf" |]

let inline synExprNeedsSpaces synExpr =
match synExpr with
| SynExpr.AnonRecd _
| SynExpr.Record _
| SynExpr.ObjExpr _ -> true
| _ -> false

let tryFindSprintfApplication (parseAndCheck: ParseAndCheckResults) (sourceText: IFSACSourceText) lineStr fcsPos =
let application =
SyntaxTraversal.Traverse(
fcsPos,
parseAndCheck.GetParseResults.ParseTree,
{ new SyntaxVisitorBase<_>() with
member _.VisitExpr(path, traverseSynExpr, defaultTraverse, synExpr) =
match synExpr with
| SynExpr.App(ExprAtomicFlag.NonAtomic,
false,
SynExpr.Ident(functionIdent),
SynExpr.Const(SynConst.String(synStringKind = SynStringKind.Regular), mString),
mApp) ->
// Don't trust the value of SynConst.String, it is already a somewhat optimized version of what the user actually code.
match sourceText.GetText mString with
| Error _ -> None
| Ok formatString ->
if
validFunctionNames.Contains functionIdent.idText
&& rangeContainsPos mApp fcsPos
&& mApp.StartLine = mApp.EndLine // only support single line for now
then
// Find all the format parameters in the source string
// Things like `%i` or `%s`
let arguments =
specifierRegex.Matches(formatString) |> Seq.cast<Match> |> Seq.toList

if arguments.IsEmpty || path.Length < arguments.Length then
None
else
let xs =
let argumentsInPath = List.take arguments.Length path

(arguments, argumentsInPath)
||> List.zip
|> List.choose (fun (regexMatch, node) ->
match node with
| SyntaxNode.SynExpr(SynExpr.App(argExpr = ae)) ->
Some(regexMatch, ae.Range, synExprNeedsSpaces ae)
| _ -> None)

List.tryLast xs
|> Option.bind (fun (_, mLastArg, _) ->
// Ensure the last argument of the current application is also on the same line.
if mApp.StartLine <> mLastArg.EndLine then
None
else
Some(functionIdent, mString, xs, mLastArg))
else
None
| _ -> defaultTraverse synExpr }
)

application
|> Option.bind (fun (functionIdent, mString, xs, mLastArg) ->
parseAndCheck.TryGetSymbolUse functionIdent.idRange.End lineStr
|> Option.bind (fun symbolUse ->
match symbolUse.Symbol with
| :? FSharpMemberOrFunctionOrValue as mfv when mfv.Assembly.QualifiedName.StartsWith("FSharp.Core") ->
// Verify the function is from F# Core.
Some(functionIdent, mString, xs, mLastArg)
| _ -> None))

let fix (getParseResultsForFile: GetParseResultsForFile) (getLanguageVersion: GetLanguageVersion) : CodeFix =
fun codeActionParams ->
asyncResult {
let filePath = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
let! languageVersion = getLanguageVersion filePath

if not (languageVersion.SupportsFeature languageFeature.Value) then
return []
else
let fcsPos = protocolPosToPos codeActionParams.Range.Start
let! parseAndCheck, lineString, sourceText = getParseResultsForFile filePath fcsPos

match tryFindSprintfApplication parseAndCheck sourceText lineString fcsPos with
| None -> return []
| Some(functionIdent, mString, arguments, mLastArg) ->
let functionEdit =
if functionIdent.idText = "sprintf" then
// Remove the `sprintf` function call
{ Range = fcsRangeToLsp (unionRanges functionIdent.idRange mString.StartRange)
NewText = "$" }
else
// Insert the dollar sign before the string
{ Range = fcsRangeToLsp mString.StartRange
NewText = "$" }

let insertArgumentEdits =
arguments
|> List.choose (fun (regexMatch, mArg, surroundWithSpaces) ->
match sourceText.GetText(mArg) with
| Error _ -> None
| Ok argText ->
let mReplace =
let stringPos =
Position.mkPos mString.StartLine (mString.StartColumn + regexMatch.Index + regexMatch.Length)

mkRange functionIdent.idRange.FileName stringPos stringPos

Some
{ Range = fcsRangeToLsp mReplace
NewText =
sprintf
"%s%s%s"
(if surroundWithSpaces then "{ " else "{")
argText
(if surroundWithSpaces then " }" else "}") })

let removeArgumentEdits =
let m = mkRange functionIdent.idRange.FileName mString.End mLastArg.End

{ Range = fcsRangeToLsp m
NewText = "" }

return
[ { Edits = [| yield functionEdit; yield! insertArgumentEdits; yield removeArgumentEdits |]
File = codeActionParams.TextDocument
Title = title
SourceDiagnostic = None
Kind = FixKind.Refactor } ]
}
Loading