Skip to content

Commit

Permalink
add file script context endpoint and test
Browse files Browse the repository at this point in the history
  • Loading branch information
baronfel committed Jul 14, 2019
1 parent faa8d0b commit 6bc5a21
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 51 deletions.
48 changes: 28 additions & 20 deletions src/FsAutoComplete.Core/Commands.fs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ type CoreResponse =
| FakeRuntime of runtimePath: string
| DotnetNewList of Template list
| DotnetNewGetDetails of DetailedTemplate
| ProjectScriptContext of projectFileName: string * scriptLines: string list * fsiOptions: string list
| ProjectScriptContext of projectFile: string * scriptLines: string list * fsiOptions: string list
| FileScriptContext of file: string * scriptLines: string list * fsiOptions: string list

[<RequireQualifiedAccess>]
type NotificationEvent =
Expand Down Expand Up @@ -936,14 +937,7 @@ type Commands (serialize : Serializer, backgroundServiceEnabled) =
state.Projects.[projectFileName] <- proj

let projectLoadedSuccessfully projectFileName response =
let project =
match state.Projects.TryFind projectFileName with
| Some prj -> prj
| None ->
let proj = new Project(projectFileName, onChange)
state.Projects.[projectFileName] <- proj
proj

let project = state.Projects.AddOrUpdate(projectFileName, (fun _ -> new Project(projectFileName, onChange)), (fun key existing -> existing))
project.Response <- Some response

onProjectLoaded projectFileName response
Expand Down Expand Up @@ -1141,15 +1135,29 @@ type Commands (serialize : Serializer, backgroundServiceEnabled) =
return [CoreResponse.FakeRuntime runtimePath]
}

member x.ProjectScriptContext (projectFile: ProjectFilePath) =
async {
match state.Projects.TryFind projectFile with
| Some project ->
match project.Response with
member x.ProjectScriptContext (projectFile: ProjectFilePath) = async {
match state.Projects.TryFind projectFile with
| Some project ->
match project.Response with
| Some projectInfo ->
let script, options = ScriptContext.makeForProject projectInfo
return CoreResponse.ProjectScriptContext (projectFile, script, options)
| None -> return CoreResponse.ErrorRes (sprintf "No project info for project '%s'" projectFile)
| None ->
return CoreResponse.ErrorRes (sprintf "Project '%s' could not be found" projectFile)
}

member x.FileScriptContext (file: string) = async {
match state.TryGetFileCheckerOptionsWithSource file with
| ResultOrString.Error e ->
return CoreResponse.ErrorRes e
| Ok (projectOptions, _fileLines) ->
match state.Projects.TryFind projectOptions.ProjectFileName with
| Some projectInfo ->
match projectInfo.Response with
| Some projectInfo ->
let script, options = ScriptContext.makeForProject projectInfo
return CoreResponse.ProjectScriptContext (projectFile, script, options)
| None -> return CoreResponse.ErrorRes (sprintf "No project info for project '%s'" projectFile)
| None ->
return CoreResponse.ErrorRes (sprintf "Project '%s' could not be found" projectFile)
}
let script, fsiOptions = ScriptContext.makeForFileInProject file projectInfo
return CoreResponse.FileScriptContext (file, script, fsiOptions)
| None -> return CoreResponse.ErrorRes (sprintf "No project info for project '%s'" projectOptions.ProjectFileName)
| None -> return CoreResponse.ErrorRes (sprintf "No project info for project '%s'" projectOptions.ProjectFileName)
}
31 changes: 24 additions & 7 deletions src/FsAutoComplete.Core/ScriptContext.fs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
/// Wraps up logic around converting project files into lists of arguments for FSI
module FsAutoComplete.ScriptContext

let inline isRefAssembly (reference: string) =
reference.Contains("/ref/")

let isValidFSIReference (reference: string) = not <| isRefAssembly reference


module List =
let satisfyAll tests list =
let combinedTest = tests |> List.reduce (fun f j -> fun x -> f x && j x)
list |> List.filter combinedTest


let inline isReference (option: string) = option.StartsWith "-r:" || option.StartsWith ("--reference:")

let inline isRefAssembly (reference: string) = reference.Contains("/ref/")

let isValidFSIReference (reference: string) = not <| isRefAssembly reference

let isValidFSIOption =
let badOptions =
Set.ofList
Expand All @@ -20,7 +21,7 @@ let isValidFSIOption =
"--highentropyva-" ]
badOptions.Contains >> not

let isNotReference (option: string) = not <| option.StartsWith "-r:"
let isNotReference = isReference >> not

let isNotOutput (option: string) = not <| option.StartsWith "-o:"

Expand Down Expand Up @@ -48,3 +49,19 @@ let makeForProject (projectInfo: ProjectCrackerCache) =
dllReferences @ otherOptions @ referencedProjects

[], allFSIOptions

let makeForFileInProject (sourceFilePath: string) (projectInfo: ProjectCrackerCache) =
// gather script/options from the project-level
let script, options = makeForProject projectInfo

// append to this a 'load' of each file in the project that comes before the given file
let filesBefore = projectInfo.Files |> List.takeWhile (fun file -> file <> sourceFilePath)

// Use 'load' instead of 'use' because 'use' is intended for fsx files, and we're loading fs source files here.
// By adding these as fsi parameters instead of #load directives inside a script, we should ensure that they are part of the initially-compiled
// dynamic assembly, rather than any that are generated as part of parsing the 'script' generated by user interactions.
let fsiLoadOperations =
filesBefore
|> List.map (sprintf "--load:%s")

script, options @ fsiLoadOperations
2 changes: 0 additions & 2 deletions src/FsAutoComplete.Core/State.fs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ type State =
| Some opts -> Ok (opts, volFile.Lines)

member x.TryGetFileCheckerOptionsWithSource(file: SourceFilePath) : ResultOrString<FSharpProjectOptions * string> =
let file = Utils.normalizePath file
match x.TryGetFileCheckerOptionsWithLines(file) with
| ResultOrString.Error x -> ResultOrString.Error x
| Ok (opts, lines) -> Ok (opts, String.concat "\n" lines)
Expand All @@ -140,7 +139,6 @@ type State =
| Some f -> Ok (f.Lines)

member x.TryGetFileCheckerOptionsWithLinesAndLineStr(file: SourceFilePath, pos : pos) : ResultOrString<FSharpProjectOptions * LineStr[] * LineStr> =
let file = Utils.normalizePath file
match x.TryGetFileCheckerOptionsWithLines(file) with
| ResultOrString.Error x -> ResultOrString.Error x
| Ok (opts, lines) ->
Expand Down
14 changes: 13 additions & 1 deletion src/FsAutoComplete/CommandResponse.fs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,13 @@ module CommandResponse =
InterpreterOptions: string list
}


type FileScriptContextResponse = {
FilePath: string
ScriptLines: string list
InterpreterOptions: string list
}

let info (serialize : Serializer) (s: string) = serialize { Kind = "info"; Data = s }

let errorG (serialize : Serializer) (errorData: ErrorData) message =
Expand Down Expand Up @@ -832,7 +839,10 @@ module CommandResponse =
serialize { Kind = "fakeRuntime"; Data = runtimePath }

let projectScriptContext (serialize: Serializer) (projectFile: string) (scriptLines: string list) (interpreterOptions: string list) =
serialize { Kind = "projectFileContext"; Data = { ProjectFile = projectFile; ScriptLines = scriptLines; InterpreterOptions = interpreterOptions } }
serialize { Kind = "projectScriptContext"; Data = { ProjectFile = projectFile; ScriptLines = scriptLines; InterpreterOptions = interpreterOptions } }

let fileScriptContext (serialize: Serializer) (filePath: string) (scriptLines: string list) (interpreterOptions: string list) =
serialize { Kind = "fileScriptContext"; Data = { FilePath = filePath; ScriptLines = scriptLines; InterpreterOptions = interpreterOptions } }

let serialize (s: Serializer) = function
| CoreResponse.InfoRes(text) ->
Expand Down Expand Up @@ -920,3 +930,5 @@ module CommandResponse =
dotnetnewgetDetails s detailedTemplate
| CoreResponse.ProjectScriptContext (projectFile, scriptLines, interpreterOptions) ->
projectScriptContext s projectFile scriptLines interpreterOptions
| CoreResponse.FileScriptContext (filePath, scriptLines, interpreterOptions) ->
fileScriptContext s filePath scriptLines interpreterOptions
22 changes: 18 additions & 4 deletions src/FsAutoComplete/FsAutoComplete.Lsp.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1449,7 +1449,7 @@ type FsharpLspServer(commands: Commands, lspClient: FSharpLspClient) =
}
)

member __.FSharpLineLense(p) = async {
member __.FSharpLineLense(p: ProjectParms) = async {
Debug.print "[LSP call] FSharpLineLense"
let fn = p.Project.GetFilePath()
let! res = commands.Declarations fn None (commands.TryGetFileVersion fn)
Expand Down Expand Up @@ -1497,7 +1497,7 @@ type FsharpLspServer(commands: Commands, lspClient: FSharpLspClient) =
return res
}

member __.FSharpCompile(p) = async {
member __.FSharpCompile(p: ProjectParms) = async {
Debug.print "[LSP call] FSharpCompile"
let fn = p.Project.GetFilePath()
let! res = commands.Compile fn
Expand Down Expand Up @@ -1546,7 +1546,7 @@ type FsharpLspServer(commands: Commands, lspClient: FSharpLspClient) =

}

member __.FSharpProject(p) = async {
member __.FSharpProject(p: ProjectParms) = async {
Debug.print "[LSP call] FSharpProject"
let fn = p.Project.GetFilePath()
let! res = commands.Project fn false ignore
Expand Down Expand Up @@ -1696,7 +1696,7 @@ type FsharpLspServer(commands: Commands, lspClient: FSharpLspClient) =

member __.ProjectScriptContext(r: ProjectScriptContextRequest) = async {
Debug.print "[LSP call] ProjectScriptContext"
match! commands.ProjectScriptContext(r.Project.GetFilePath()) with
match! commands.ProjectScriptContext(fileUriToLocalPath r.Project) with
| CoreResponse.InfoRes msg | CoreResponse.ErrorRes msg ->
return LspResult.internalError msg
| CoreResponse.ProjectScriptContext (projectFile, scriptLines, fsiOpts) ->
Expand All @@ -1707,6 +1707,19 @@ type FsharpLspServer(commands: Commands, lspClient: FSharpLspClient) =
return LspResult.notImplemented
}

member __.FileScriptContext(r: FileScriptContextRequest) = async {
Debug.print "[LSP call] ProjectScriptContext"
match! commands.FileScriptContext (fileUriToLocalPath r.File) with
| CoreResponse.InfoRes msg | CoreResponse.ErrorRes msg ->
return LspResult.internalError msg
| CoreResponse.FileScriptContext (filePath, scriptLines, fsiOpts) ->
return
{ Content = CommandResponse.fileScriptContext FsAutoComplete.JsonSerializer.writeJson filePath scriptLines fsiOpts }
|> LspResult.success
| _ ->
return LspResult.notImplemented
}

let startCore (commands: Commands) =
use input = Console.OpenStandardInput()
use output = Console.OpenStandardOutput()
Expand All @@ -1731,6 +1744,7 @@ let startCore (commands: Commands) =
|> Map.add "fake/listTargets" (requestHandling (fun s p -> s.FakeTargets(p) ))
|> Map.add "fake/runtimePath" (requestHandling (fun s p -> s.FakeRuntimePath(p) ))
|> Map.add "fsi/projectScriptContext" (requestHandling (fun s p -> s.ProjectScriptContext(p) ))
|> Map.add "fsi/fileScriptContext" (requestHandling (fun s p -> s.FileScriptContext(p) ))



Expand Down
4 changes: 3 additions & 1 deletion src/FsAutoComplete/LspHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,9 @@ type DocumentationForSymbolReuqest = {XmlSig: string; Assembly: string}

type FakeTargetsRequest = {FileName : string; FakeContext : FakeSupport.FakeContext; }

type ProjectScriptContextRequest = { Project: TextDocumentIdentifier }
type ProjectScriptContextRequest = { Project: DocumentUri }

type FileScriptContextRequest = { File: DocumentUri }

type LineLensConfig = {
Enabled: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module ProjectScriptTest.Domain

let doAThingWithAString (s: string) = String.replicate 50 s
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="Domain.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

Expand Down
51 changes: 35 additions & 16 deletions test/FsAutoComplete.Tests.Lsp/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ let dotnetnewTest =
))
]

let projectScriptContextTests =
let scriptContextTests =
let projectDir = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "ProjectScriptTest")
let projectFileUri = Path.Combine(projectDir, "ProjectScriptTest.fsproj") |> Uri |> string

Expand All @@ -662,20 +662,39 @@ let projectScriptContextTests =
)
let serverTest f () = f serverStart.Value

testList "ProjectScriptContext tests" [
testCase "Can retrieve script content for project" (serverTest (fun server ->
let res = server.ProjectScriptContext({ Project = { Uri = projectFileUri } }) |> Async.RunSynchronously
match res with
| Result.Error e -> failtestf "Request failed: %A" e
| Result.Ok { Content = content } ->
let payload = JsonSerializer.readJson<CommandResponse.ResponseMsg<CommandResponse.ProjectScriptContextResponse>>(content)
Expect.contains payload.Data.InterpreterOptions "--define:FOO" "Should have define from project file"

payload.Data.InterpreterOptions
|> Seq.tryFind (fun r -> r.Contains "StackExchange.Redis.dll")
|> Option.defaultWith (fun () -> failwith "Expected to find redis dependency in script options")
|> ignore
))
testList "Script context tests" [
testList "Project script context" [
testCase "Contains nuget references" (serverTest (fun server ->
let res = server.ProjectScriptContext({ Project = projectFileUri }) |> Async.RunSynchronously
match res with
| Result.Error e -> failtestf "Request failed: %A" e
| Result.Ok { Content = content } ->
let payload = JsonSerializer.readJson<CommandResponse.ResponseMsg<CommandResponse.ProjectScriptContextResponse>>(content)
Expect.contains payload.Data.InterpreterOptions "--define:FOO" "Should have define from project file"

payload.Data.InterpreterOptions
|> Seq.tryFind (fun r -> r.Contains "StackExchange.Redis.dll")
|> Option.defaultWith (fun () -> failwith "Expected to find redis dependency in script options")
|> ignore
))
]
testList "File script context" [
testCase "Contains files that come before target file" (serverTest (fun server ->
let targetFile = Path.Combine(projectDir, "Program.fs") |> Uri |> string
let res = server.FileScriptContext({ File = targetFile }) |> Async.RunSynchronously
match res with
| Result.Error e -> failtestf "Request failed: %A" e
| Result.Ok { Content = content } ->
let payload = JsonSerializer.readJson<CommandResponse.ResponseMsg<CommandResponse.FileScriptContextResponse>>(content)

let loads =
payload.Data.InterpreterOptions
|> List.filter (fun r -> r.StartsWith "--load:")

Expect.hasLength loads 2 "Should have loaded other files in the project (including auto-generated assemblyinfos)"
Expect.stringEnds (List.last loads) "Domain.fs" "Should have loaded the domain file"
))
]
]

///Global list of tests
Expand All @@ -691,5 +710,5 @@ let tests =
fsdnTest
uriTests
dotnetnewTest
projectScriptContextTests
scriptContextTests
]

0 comments on commit 6bc5a21

Please sign in to comment.