Skip to content

Commit

Permalink
cancel outdated FCS requests on file version change (ionide#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
vasily-kirichenko authored and Krzysztof-Cieslak committed Oct 20, 2016
1 parent 7147250 commit e187913
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 114 deletions.
21 changes: 12 additions & 9 deletions src/FsAutoComplete.Core/Commands.fs
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,22 @@ type Commands (serialize : Serializer) =
member __.TryGetFileCheckerOptionsWithLines = state.TryGetFileCheckerOptionsWithLines
member __.Files = state.Files

member __.Parse file lines = async {
member __.Parse file lines version = async {
let colorizations = state.ColorizationOutput
let parse' fileName text options =
async {
let! _parseResults, checkResults = checker.ParseAndCheckFileInProject(fileName, 0, text, options)
let! result = checker.ParseAndCheckFileInProject(fileName, version, text, options)
return
match checkResults with
| FSharpCheckFileAnswer.Aborted -> [Response.info serialize "Parse aborted"]
| FSharpCheckFileAnswer.Succeeded results ->
if colorizations then
[ Response.errors serialize (results.Errors)
Response.colorizations serialize (results.GetExtraColorizationsAlternate()) ]
else [ Response.errors serialize (results.Errors) ]
match result with
| Failure e -> [Response.error serialize e]
| Success (_, checkResults) ->
match checkResults with
| FSharpCheckFileAnswer.Aborted -> [Response.info serialize "Parse aborted"]
| FSharpCheckFileAnswer.Succeeded results ->
if colorizations then
[ Response.errors serialize (results.Errors)
Response.colorizations serialize (results.GetExtraColorizationsAlternate()) ]
else [ Response.errors serialize (results.Errors) ]
}
let file = Path.GetFullPath file
let text = String.concat "\n" lines
Expand Down
75 changes: 73 additions & 2 deletions src/FsAutoComplete.Core/CompilerServiceInterface.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open System
open System.IO
open Microsoft.FSharp.Compiler.SourceCodeServices
open Utils
open System.Collections.Concurrent

type ParseAndCheckResults
(
Expand Down Expand Up @@ -105,14 +106,50 @@ type ParseAndCheckResults
member __.GetAST = parseResults.ParseTree
member __.GetCheckResults = checkResults

type private FileState =
| Checked
| NeedChecking
| BeingChecked
| Cancelled

type Version = int

type FSharpCompilerServiceChecker() =
let checker =
FSharpChecker.Create(
projectCacheSize = 200,
keepAllBackgroundResolutions = true,
keepAssemblyContents = true)

let files = ConcurrentDictionary<string, Version * FileState>()
do checker.BeforeBackgroundFileCheck.Add ignore

let isResultObsolete fileName =
match files.TryGetValue fileName with
| true, (_, Cancelled) -> true
| _ -> false

let fileChanged filePath version =
files.AddOrUpdate (filePath, (version, NeedChecking), (fun _ (oldVersion, oldState) ->
if version <> oldVersion then
(version,
match oldState with
| BeingChecked -> Cancelled
| Cancelled -> Cancelled
| NeedChecking -> NeedChecking
| Checked -> NeedChecking)
else oldVersion, oldState))
|> debug "[LanguageService] %s changed: set status to %A" filePath


let fixFileName path =
if (try Path.GetFullPath path |> ignore; true with _ -> false) then path
else
match Environment.OSVersion.Platform with
| PlatformID.Unix
| PlatformID.MacOSX -> Environment.GetEnvironmentVariable "HOME"
| _ -> Environment.ExpandEnvironmentVariables "%HOMEDRIVE%%HOMEPATH%"
</> Path.GetFileName path

let ensureCorrectFSharpCore (options: string[]) =
Environment.fsharpCoreOpt
Expand Down Expand Up @@ -187,8 +224,42 @@ type FSharpCompilerServiceChecker() =
return res |> Array.collect id
}

member __.ParseAndCheckFileInProject(fileName, version, source, options) =
checker.ParseAndCheckFileInProject(fileName, version, source, options)
member __.ParseAndCheckFileInProject(filePath, version, source, options) =
async {
debug "[LanguageService] ParseAndCheckFileInProject - enter"
fileChanged filePath version
let fixedFilePath = fixFileName filePath
let! res = Async.Catch (async {
try
// wait until the previous checking completed
while files.ContainsKey filePath &&
(match files.TryGetValue filePath with
| true, (v, Checked)
| true, (v, NeedChecking) ->
files.[filePath] <- (v, BeingChecked)
true
| _ -> false) do
do! Async.Sleep 20

debug "[LanguageService] Change state for %s to `BeingChecked`" filePath
debug "[LanguageService] Parse and typecheck source..."
return! checker.ParseAndCheckFileInProject
(fixedFilePath, version, source, options,
IsResultObsolete (fun _ -> isResultObsolete filePath), null)
finally
match files.TryGetValue filePath with
| true, (v, BeingChecked)
| true, (v, Cancelled) -> files.[filePath] <- (v, Checked)
| _ -> ()
})

debug "[LanguageService]: Check completed"
// Construct new typed parse result if the task succeeded
return
match res with
| Choice1Of2 x -> Success x
| Choice2Of2 e -> Failure e.Message
}

member __.TryGetRecentCheckResultsForFile(file, options, ?source) =
checker.TryGetRecentCheckResultsForFile(file, options, ?source=source)
Expand Down
5 changes: 4 additions & 1 deletion src/FsAutoComplete.Core/Utils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,7 @@ type System.Collections.Concurrent.ConcurrentDictionary<'key, 'value> with
| _ -> None

member x.ToSeq() =
x |> Seq.map (fun (KeyValue(k, v)) -> k, v)
x |> Seq.map (fun (KeyValue(k, v)) -> k, v)

let inline debug msg = Printf.kprintf System.Diagnostics.Debug.WriteLine msg
let inline fail msg = Printf.kprintf System.Diagnostics.Debug.Fail msg
4 changes: 2 additions & 2 deletions src/FsAutoComplete.Suave/FsAutoComplete.Suave.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ open FsAutoComplete.JsonSerializer

[<AutoOpen>]
module Contract =
type ParseRequest = { FileName : string; IsAsync : bool; Lines : string[]}
type ParseRequest = { FileName : string; IsAsync : bool; Lines : string[]; Version : int }
type ProjectRequest = { FileName : string;}
type DeclarationsRequest = {FileName : string}
type HelptextRequest = {Symbol : string}
Expand Down Expand Up @@ -70,7 +70,7 @@ let main argv =
Writers.setMimeType "application/json; charset=utf-8" >=>
POST >=>
choose [
path "/parse" >=> handler (fun (data : ParseRequest) -> commands.Parse data.FileName data.Lines)
path "/parse" >=> handler (fun (data : ParseRequest) -> commands.Parse data.FileName data.Lines data.Version)
//TODO: Add filewatcher
path "/project" >=> handler (fun (data : ProjectRequest) -> commands.Project data.FileName false ignore)
path "/parseProjects" >=> fun httpCtx ->
Expand Down
200 changes: 100 additions & 100 deletions src/FsAutoComplete/Program.fs
Original file line number Diff line number Diff line change
@@ -1,102 +1,102 @@
namespace FsAutoComplete

open System
open System.IO
open Microsoft.FSharp.Compiler
open JsonSerializer
open FsAutoComplete

module internal Main =
namespace FsAutoComplete

open System
open System.IO
open Microsoft.FSharp.Compiler
open JsonSerializer
open FsAutoComplete

module internal Main =
open System.Collections.Concurrent

module Response = CommandResponse
let originalFs = AbstractIL.Internal.Library.Shim.FileSystem
let commands = Commands writeJson
let fs = new FileSystem(originalFs, commands.Files.TryFind)
AbstractIL.Internal.Library.Shim.FileSystem <- fs
let commandQueue = new BlockingCollection<Command>(10)

let main () : int =
let mutable quit = false

while not quit do
async {
match commandQueue.Take() with
| Parse (file, kind, lines) ->
let! res = commands.Parse file lines
//Hack for tests
let r = match kind with
| Synchronous -> Response.info writeJson "Synchronous parsing started"
| Normal -> Response.info writeJson "Background parsing started"
return r :: res

| Project (file, verbose) ->
return! commands.Project file verbose (fun fullPath -> commandQueue.Add(Project (fullPath, verbose)))
| Declarations file -> return! commands.Declarations file
| HelpText sym -> return commands.Helptext sym
| PosCommand (cmd, file, lineStr, pos, _timeout, filter) ->
let file = Path.GetFullPath file
match commands.TryGetFileCheckerOptionsWithLines file with
| Failure s -> return [Response.error writeJson s]
| Success options ->
let projectOptions, lines = options
let ok = pos.Line <= lines.Length && pos.Line >= 1 &&
pos.Col <= lineStr.Length + 1 && pos.Col >= 1
if not ok then
return [Response.error writeJson "Position is out of range"]
else
// TODO: Should sometimes pass options.Source in here to force a reparse
// for completions e.g. `(some typed expr).$`
let tyResOpt = commands.TryGetRecentTypeCheckResultsForFile(file, projectOptions)
match tyResOpt with
| None -> return [ Response.info writeJson "Cached typecheck results not yet available"]
| Some tyRes ->
return!
match cmd with
| Completion -> commands.Completion tyRes pos lineStr filter
| ToolTip -> commands.ToolTip tyRes pos lineStr
| TypeSig -> commands.Typesig tyRes pos lineStr
| SymbolUse -> commands.SymbolUse tyRes pos lineStr
| FindDeclaration -> commands.FindDeclarations tyRes pos lineStr
| Methods -> commands.Methods tyRes pos lines
| SymbolUseProject -> commands.SymbolUseProject tyRes pos lineStr

| CompilerLocation -> return commands.CompilerLocation()
| Colorization enabled -> commands.Colorization enabled; return []
| Lint filename -> return! commands.Lint filename
| Error msg -> return commands.Error msg
| Quit ->
quit <- true
return []
}
|> Async.Catch
|> Async.RunSynchronously
|> function
| Choice1Of2 res -> res |> List.iter Console.WriteLine
| Choice2Of2 exn ->
exn
|> sprintf "Unexpected internal error. Please report at https://github.com/fsharp/FsAutoComplete/issues, attaching the exception information:\n%O"
|> Response.error writeJson
|> Console.WriteLine
0

[<EntryPoint>]
let entry args =
System.Threading.ThreadPool.SetMinThreads(8, 8) |> ignore
Console.InputEncoding <- Text.Encoding.UTF8
Console.OutputEncoding <- new Text.UTF8Encoding(false, false)
let extra = Options.p.Parse args
if extra.Count <> 0 then
printfn "Unrecognised arguments: %s" (String.concat "," extra)
1
else
try
async {
while true do
commandQueue.Add (CommandInput.parseCommand(Console.ReadLine()))
}
|> Async.Start

main()
finally
(!Debug.output).Close()
module Response = CommandResponse
let originalFs = AbstractIL.Internal.Library.Shim.FileSystem
let commands = Commands writeJson
let fs = new FileSystem(originalFs, commands.Files.TryFind)
AbstractIL.Internal.Library.Shim.FileSystem <- fs
let commandQueue = new BlockingCollection<Command>(10)

let main () : int =
let mutable quit = false

while not quit do
async {
match commandQueue.Take() with
| Parse (file, kind, lines) ->
let! res = commands.Parse file lines 0
//Hack for tests
let r = match kind with
| Synchronous -> Response.info writeJson "Synchronous parsing started"
| Normal -> Response.info writeJson "Background parsing started"
return r :: res

| Project (file, verbose) ->
return! commands.Project file verbose (fun fullPath -> commandQueue.Add(Project (fullPath, verbose)))
| Declarations file -> return! commands.Declarations file
| HelpText sym -> return commands.Helptext sym
| PosCommand (cmd, file, lineStr, pos, _timeout, filter) ->
let file = Path.GetFullPath file
match commands.TryGetFileCheckerOptionsWithLines file with
| Failure s -> return [Response.error writeJson s]
| Success options ->
let projectOptions, lines = options
let ok = pos.Line <= lines.Length && pos.Line >= 1 &&
pos.Col <= lineStr.Length + 1 && pos.Col >= 1
if not ok then
return [Response.error writeJson "Position is out of range"]
else
// TODO: Should sometimes pass options.Source in here to force a reparse
// for completions e.g. `(some typed expr).$`
let tyResOpt = commands.TryGetRecentTypeCheckResultsForFile(file, projectOptions)
match tyResOpt with
| None -> return [ Response.info writeJson "Cached typecheck results not yet available"]
| Some tyRes ->
return!
match cmd with
| Completion -> commands.Completion tyRes pos lineStr filter
| ToolTip -> commands.ToolTip tyRes pos lineStr
| TypeSig -> commands.Typesig tyRes pos lineStr
| SymbolUse -> commands.SymbolUse tyRes pos lineStr
| FindDeclaration -> commands.FindDeclarations tyRes pos lineStr
| Methods -> commands.Methods tyRes pos lines
| SymbolUseProject -> commands.SymbolUseProject tyRes pos lineStr

| CompilerLocation -> return commands.CompilerLocation()
| Colorization enabled -> commands.Colorization enabled; return []
| Lint filename -> return! commands.Lint filename
| Error msg -> return commands.Error msg
| Quit ->
quit <- true
return []
}
|> Async.Catch
|> Async.RunSynchronously
|> function
| Choice1Of2 res -> res |> List.iter Console.WriteLine
| Choice2Of2 exn ->
exn
|> sprintf "Unexpected internal error. Please report at https://github.com/fsharp/FsAutoComplete/issues, attaching the exception information:\n%O"
|> Response.error writeJson
|> Console.WriteLine
0

[<EntryPoint>]
let entry args =
System.Threading.ThreadPool.SetMinThreads(8, 8) |> ignore
Console.InputEncoding <- Text.Encoding.UTF8
Console.OutputEncoding <- new Text.UTF8Encoding(false, false)
let extra = Options.p.Parse args
if extra.Count <> 0 then
printfn "Unrecognised arguments: %s" (String.concat "," extra)
1
else
try
async {
while true do
commandQueue.Add (CommandInput.parseCommand(Console.ReadLine()))
}
|> Async.Start

main()
finally
(!Debug.output).Close()

0 comments on commit e187913

Please sign in to comment.