diff --git a/src/fsharp/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.DesignTime.proj b/src/fsharp/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.DesignTime.proj
new file mode 100644
index 000000000..d02ae419b
--- /dev/null
+++ b/src/fsharp/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.DesignTime.proj
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+ $(MSBuildThisFileDirectory)FSharp.Compiler.LanguageServer.DesignTime.targets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/fsharp/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.DesignTime.targets b/src/fsharp/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.DesignTime.targets
new file mode 100644
index 000000000..ea8f3e286
--- /dev/null
+++ b/src/fsharp/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.DesignTime.targets
@@ -0,0 +1,52 @@
+
+
+
+
+ true
+ false
+ true
+ true
+ false
+ false
+ false
+ true
+ false
+ true
+ false
+
+
+
+
+
+
+ _ComputeTargetFrameworkItems
+ _PopulateTargetFrameworks
+
+
+
+
+ <_TargetFramework Include="$(TargetFramework)" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/fsharp/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.fsproj b/src/fsharp/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.fsproj
index fd6e517e5..0bb089914 100644
--- a/src/fsharp/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.fsproj
+++ b/src/fsharp/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.fsproj
@@ -23,6 +23,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/fsharp/FSharp.Compiler.LanguageServer/LspExternalAccess.fs b/src/fsharp/FSharp.Compiler.LanguageServer/LspExternalAccess.fs
index e6fa760d1..48e4b0b40 100644
--- a/src/fsharp/FSharp.Compiler.LanguageServer/LspExternalAccess.fs
+++ b/src/fsharp/FSharp.Compiler.LanguageServer/LspExternalAccess.fs
@@ -9,10 +9,18 @@ module FunctionNames =
[]
let OptionsSet = "options/set"
+ []
+ let TextDocumentPublishDiagnostics = "textDocument/publishDiagnostics"
+
type Options =
- { usePreviewTextHover: bool }
+ { usePreviewTextHover: bool
+ usePreviewDiagnostics: bool }
static member Default() =
- { usePreviewTextHover = false }
+ { usePreviewTextHover = false
+ usePreviewDiagnostics = false }
+ static member AllOn() =
+ { usePreviewTextHover = true
+ usePreviewDiagnostics = true }
module Extensions =
type JsonRpc with
diff --git a/src/fsharp/FSharp.Compiler.LanguageServer/LspTypes.fs b/src/fsharp/FSharp.Compiler.LanguageServer/LspTypes.fs
index 97479eef2..264e526fc 100644
--- a/src/fsharp/FSharp.Compiler.LanguageServer/LspTypes.fs
+++ b/src/fsharp/FSharp.Compiler.LanguageServer/LspTypes.fs
@@ -5,8 +5,9 @@ namespace FSharp.Compiler.LanguageServer
open Newtonsoft.Json.Linq
open Newtonsoft.Json
-// Interfaces as defined at https://microsoft.github.io/language-server-protocol/specification. The properties on
-// these types are camlCased to match the underlying JSON properties to avoid attributes on every field:
+// Interfaces as defined at https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/.
+// The properties on these types are camlCased to match the underlying JSON properties to avoid attributes on every
+// field:
// []
/// Represents a zero-based line and column of a text document.
@@ -32,7 +33,7 @@ type Diagnostic =
{ range: Range
severity: int option
code: string
- source: string option // "F#"
+ source: string option
message: string
relatedInformation: DiagnosticRelatedInformation[] option }
static member Error = 1
@@ -46,7 +47,7 @@ type PublishDiagnosticsParams =
type ClientCapabilities =
{ workspace: JToken option // TODO: WorkspaceClientCapabilities
- textDocument: JToken option // TODO: TextDocumentCapabilities
+ textDocument: JToken option // TODO: TextDocumentClientCapabilities, publishDiagnostics: { relatedInformation: bool option }
experimental: JToken option
supportsVisualStudioExtensions: bool option }
diff --git a/src/fsharp/FSharp.Compiler.LanguageServer/Methods.fs b/src/fsharp/FSharp.Compiler.LanguageServer/Methods.fs
index d1e614cb2..453b7f822 100644
--- a/src/fsharp/FSharp.Compiler.LanguageServer/Methods.fs
+++ b/src/fsharp/FSharp.Compiler.LanguageServer/Methods.fs
@@ -8,8 +8,10 @@ open System.Threading
open Newtonsoft.Json.Linq
open StreamJsonRpc
-// https://microsoft.github.io/language-server-protocol/specification
-type Methods(state: State) =
+// https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/
+type Methods() =
+
+ let state = State()
/// Helper to run Async<'T> with a CancellationToken.
let runAsync (cancellationToken: CancellationToken) (computation: Async<'T>) = Async.StartAsTask(computation, cancellationToken=cancellationToken)
@@ -29,8 +31,10 @@ type Methods(state: State) =
[] initializationOptions: JToken,
capabilities: ClientCapabilities,
[] trace: string,
- [] workspaceFolders: WorkspaceFolder[]
+ [] workspaceFolders: WorkspaceFolder[],
+ [] cancellationToken: CancellationToken
) =
+ state.Initialize rootPath rootUri (fun projectOptions -> TextDocument.PublishDiagnostics(state, projectOptions) |> Async.Start)
{ InitializeResult.capabilities = ServerCapabilities.DefaultCapabilities() }
[]
@@ -63,5 +67,6 @@ type Methods(state: State) =
(
options: Options
) =
- sprintf "got options %A" options |> Console.Error.WriteLine
+ eprintfn "got options %A" options
state.Options <- options
+ state.InvalidateAllProjects()
diff --git a/src/fsharp/FSharp.Compiler.LanguageServer/Server.fs b/src/fsharp/FSharp.Compiler.LanguageServer/Server.fs
index 071ad6b22..28d5e49a5 100644
--- a/src/fsharp/FSharp.Compiler.LanguageServer/Server.fs
+++ b/src/fsharp/FSharp.Compiler.LanguageServer/Server.fs
@@ -12,17 +12,17 @@ type Server(sendingStream: Stream, receivingStream: Stream) =
let converter = JsonOptionConverter() // special handler to convert between `Option<'T>` and `obj/null`.
do formatter.JsonSerializer.Converters.Add(converter)
let handler = new HeaderDelimitedMessageHandler(sendingStream, receivingStream, formatter)
- let state = State()
- let methods = Methods(state)
+ let methods = Methods()
let rpc = new JsonRpc(handler, methods)
+ do methods.State.JsonRpc <- Some rpc
member __.StartListening() =
rpc.StartListening()
member __.WaitForExitAsync() =
async {
- do! Async.AwaitEvent (state.Shutdown)
- do! Async.AwaitEvent (state.Exit)
+ do! Async.AwaitEvent (methods.State.Shutdown)
+ do! Async.AwaitEvent (methods.State.Exit)
}
interface IDisposable with
diff --git a/src/fsharp/FSharp.Compiler.LanguageServer/State.fs b/src/fsharp/FSharp.Compiler.LanguageServer/State.fs
index 5ca2d3f84..0812bb9a7 100644
--- a/src/fsharp/FSharp.Compiler.LanguageServer/State.fs
+++ b/src/fsharp/FSharp.Compiler.LanguageServer/State.fs
@@ -2,11 +2,209 @@
namespace FSharp.Compiler.LanguageServer
+open System
+open System.Collections.Concurrent
+open System.Collections.Generic
+open System.Diagnostics
+open System.IO
+open System.Text.RegularExpressions
+open FSharp.Compiler.SourceCodeServices
+open StreamJsonRpc
+
+module internal Solution =
+ // easy unit testing
+ let getProjectPaths (solutionContent: string) (solutionDir: string) =
+ // This looks scary, but is much more lightweight than carrying along MSBuild just to have it parse the solution file.
+ //
+ // A valid line in .sln looks like:
+ // Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ConsoleApp2", "ConsoleApp2\ConsoleApp2.fsproj", "{60A4BE67-7E03-4200-AD38-B0E5E8E049C1}"
+ // and we're hoping to extract this: ------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ //
+ // therefore:
+ // ^Project text 'Project' at the start of the line
+ // .* any number of characters
+ // \"" double quote character (it's doubled up to escape from the raw string literal here)
+ // ( start of capture group
+ // [^\""] not a quote
+ // * many of those
+ // \.fsproj literal string ".fsproj"
+ // ) end of capture group
+ // \"" double quote
+ let pattern = Regex(@"^Project.*\""([^\""]*\.fsproj)\""")
+ let lines = solutionContent.Split('\n')
+ let relativeProjects =
+ lines
+ |> Array.map pattern.Match
+ |> Array.filter (fun m -> m.Success)
+ |> Array.map (fun m -> m.Groups.[1].Value)
+ // .sln files by convention uses backslashes, which might not be appropriate at runtime
+ |> Array.map (fun p -> p.Replace('\\', Path.DirectorySeparatorChar))
+ let projects =
+ relativeProjects
+ |> Array.map (fun p -> if Path.IsPathRooted(p) then p else Path.Combine(solutionDir, p))
+ projects
+
type State() =
+ let checker = FSharpChecker.Create()
+
+ let sourceFileToProjectMap = ConcurrentDictionary()
+
let shutdownEvent = new Event<_>()
let exitEvent = new Event<_>()
let cancelEvent = new Event<_>()
+ let projectInvalidatedEvent = new Event<_>()
+
+ let fileChanged (args: FileSystemEventArgs) =
+ match sourceFileToProjectMap.TryGetValue args.FullPath with
+ | true, projectOptions -> projectInvalidatedEvent.Trigger(projectOptions)
+ | false, _ -> ()
+ let fileRenamed (args: RenamedEventArgs) =
+ match sourceFileToProjectMap.TryGetValue args.FullPath with
+ | true, projectOptions -> projectInvalidatedEvent.Trigger(projectOptions)
+ | false, _ -> ()
+ let fileWatcher = new FileSystemWatcher()
+ do fileWatcher.IncludeSubdirectories <- true
+ do fileWatcher.Changed.Add(fileChanged)
+ do fileWatcher.Created.Add(fileChanged)
+ do fileWatcher.Deleted.Add(fileChanged)
+ do fileWatcher.Renamed.Add(fileRenamed)
+
+ let execProcess (name: string) (args: string) =
+ let startInfo = ProcessStartInfo(name, args)
+ eprintfn "executing: %s %s" name args
+ startInfo.CreateNoWindow <- true
+ startInfo.RedirectStandardOutput <- true
+ startInfo.UseShellExecute <- false
+ let lines = List()
+ use proc = new Process()
+ proc.StartInfo <- startInfo
+ proc.OutputDataReceived.Add(fun args -> lines.Add(args.Data))
+ proc.Start() |> ignore
+ proc.BeginOutputReadLine()
+ proc.WaitForExit()
+ lines.ToArray()
+
+ let linesWithPrefixClean (prefix: string) (lines: string[]) =
+ lines
+ |> Array.filter (isNull >> not)
+ |> Array.map (fun line -> line.TrimStart(' '))
+ |> Array.filter (fun line -> line.StartsWith(prefix))
+ |> Array.map (fun line -> line.Substring(prefix.Length))
+
+ let getProjectOptions (rootDir: string) =
+ if isNull rootDir then [||]
+ else
+ fileWatcher.Path <- rootDir
+ fileWatcher.EnableRaisingEvents <- true
+
+ /// This function is meant to be temporary. Until we figure out what a language server for a project
+ /// system looks like, we have to guess based on the files we find in the root.
+ let getProjectOptions (projectPath: string) =
+ let projectDir = Path.GetDirectoryName(projectPath)
+ let normalizePath (path: string) =
+ if Path.IsPathRooted(path) then path
+ else Path.Combine(projectDir, path)
+
+ // To avoid essentially re-creating a copy of MSBuild alongside this tool, we instead fake a design-
+ // time build with this project. The output of building this helper project is text that's easily
+ // parsable. See the helper project for more information.
+ let reporterProject = Path.Combine(Path.GetDirectoryName(typeof.Assembly.Location), "FSharp.Compiler.LanguageServer.DesignTime.proj")
+ let detectedTfmSentinel = "DetectedTargetFramework="
+ let detectedCommandLineArgSentinel = "DetectedCommandLineArg="
+
+ let execTfmReporter =
+ sprintf "build \"%s\" \"/p:ProjectFile=%s\"" reporterProject projectPath
+ |> execProcess "dotnet"
+
+ let execArgReporter (tfm: string) =
+ sprintf "build \"%s\" \"/p:ProjectFile=%s\" \"/p:TargetFramework=%s\"" reporterProject projectPath tfm
+ |> execProcess "dotnet"
+
+ // find the target frameworks
+ let targetFrameworks =
+ execTfmReporter
+ |> linesWithPrefixClean detectedTfmSentinel
+
+ let getArgs (tfm: string) =
+ execArgReporter tfm
+ |> linesWithPrefixClean detectedCommandLineArgSentinel
+
+ let tfmAndArgs =
+ targetFrameworks
+ |> Array.map (fun tfm -> tfm, getArgs tfm)
+
+ let separateArgs (args: string[]) =
+ args
+ |> Array.partition (fun a -> a.StartsWith("-"))
+ |> (fun (options, files) ->
+ let normalizedFiles = files |> Array.map normalizePath
+ options, normalizedFiles)
+
+ // TODO: for now we're only concerned with the first TFM
+ let _tfm, args = Array.head tfmAndArgs
+
+ let otherOptions, sourceFiles = separateArgs args
+
+ let projectOptions: FSharpProjectOptions =
+ { ProjectFileName = projectPath
+ ProjectId = None
+ SourceFiles = sourceFiles
+ OtherOptions = otherOptions
+ ReferencedProjects = [||] // TODO: populate from @(ProjectReference)
+ IsIncompleteTypeCheckEnvironment = false
+ UseScriptResolutionRules = false
+ LoadTime = DateTime.Now
+ UnresolvedReferences = None
+ OriginalLoadReferences = []
+ ExtraProjectInfo = None
+ Stamp = None }
+ projectOptions
+ let topLevelProjects = Directory.GetFiles(rootDir, "*.fsproj")
+ let watchableProjectPaths =
+ match topLevelProjects with
+ | [||] ->
+ match Directory.GetFiles(rootDir, "*.sln") with
+ // TODO: what to do with multiple .sln or a combo of .sln/.fsproj?
+ | [| singleSolution |] ->
+ let content = File.ReadAllText(singleSolution)
+ let solutionDir = Path.GetDirectoryName(singleSolution)
+ Solution.getProjectPaths content solutionDir
+ | _ -> [||]
+ | _ -> topLevelProjects
+ let watchableProjectOptions =
+ watchableProjectPaths
+ |> Array.map getProjectOptions
+
+ // associate source files with project options
+ let watchFile file projectOptions =
+ sourceFileToProjectMap.AddOrUpdate(file, projectOptions, fun _ _ -> projectOptions)
+
+ for projectOptions in watchableProjectOptions do
+ // watch .fsproj
+ watchFile projectOptions.ProjectFileName projectOptions |> ignore
+ // TODO: watch .deps.json
+ for sourceFile in projectOptions.SourceFiles do
+ let sourceFileFullPath =
+ if Path.IsPathRooted(sourceFile) then sourceFile
+ else
+ let projectDir = Path.GetDirectoryName(projectOptions.ProjectFileName)
+ Path.Combine(projectDir, sourceFile)
+ watchFile sourceFileFullPath projectOptions |> ignore
+
+ watchableProjectOptions
+
+ member __.Checker = checker
+
+ /// Initialize the LSP at the specified location. According to the spec, `rootUri` is to be preferred over `rootPath`.
+ member __.Initialize (rootPath: string) (rootUri: DocumentUri) (computeDiagnostics: FSharpProjectOptions -> unit) =
+ let rootDir =
+ if not (isNull rootUri) then Uri(rootUri).LocalPath
+ else rootPath
+ let projectOptions = getProjectOptions rootDir
+ projectInvalidatedEvent.Publish.Add computeDiagnostics // compute diagnostics on project invalidation
+ for projectOption in projectOptions do
+ computeDiagnostics projectOption // compute initial set of diagnostics
[]
member __.Shutdown = shutdownEvent.Publish
@@ -17,10 +215,19 @@ type State() =
[]
member __.Cancel = cancelEvent.Publish
+ []
+ member __.ProjectInvalidated = projectInvalidatedEvent.Publish
+
member __.DoShutdown() = shutdownEvent.Trigger()
member __.DoExit() = exitEvent.Trigger()
member __.DoCancel() = cancelEvent.Trigger()
+ member __.InvalidateAllProjects() =
+ for projectOptions in sourceFileToProjectMap.Values do
+ projectInvalidatedEvent.Trigger(projectOptions)
+
member val Options = Options.Default() with get, set
+
+ member val JsonRpc: JsonRpc option = None with get, set
diff --git a/src/fsharp/FSharp.Compiler.LanguageServer/TextDocument.fs b/src/fsharp/FSharp.Compiler.LanguageServer/TextDocument.fs
index 0c7379650..489b55ebc 100644
--- a/src/fsharp/FSharp.Compiler.LanguageServer/TextDocument.fs
+++ b/src/fsharp/FSharp.Compiler.LanguageServer/TextDocument.fs
@@ -2,13 +2,15 @@
namespace FSharp.Compiler.LanguageServer
-open System
+open System.Threading
module TextDocument =
+ let mutable publishDiagnosticsCancellationTokenSource = new CancellationTokenSource()
+
let Hover (state: State) (textDocument: TextDocumentIdentifier) (position: Position) =
async {
- Console.Error.WriteLine("hover at " + position.line.ToString() + "," + position.character.ToString())
+ eprintfn "hover at %d, %d" position.line position.character
if not state.Options.usePreviewTextHover then return None
else
let startCol, endCol =
@@ -21,10 +23,53 @@ module TextDocument =
}
}
- let PublishDiagnostics(state: State) =
+ let PublishDiagnostics(state: State, projectOptions: FSharp.Compiler.SourceCodeServices.FSharpProjectOptions) =
+ // TODO: honor TextDocumentClientCapabilities.publishDiagnostics.relatedInformation
+ // cancel any existing request to publish diagnostics
+ publishDiagnosticsCancellationTokenSource.Cancel()
+ publishDiagnosticsCancellationTokenSource <- new CancellationTokenSource()
async {
- return {
- PublishDiagnosticsParams.uri = ""
- diagnostics = [||]
- }
+ if not state.Options.usePreviewDiagnostics then return ()
+ else
+ eprintfn "starting diagnostics computation"
+ match state.JsonRpc with
+ | None -> eprintfn "state.JsonRpc was null; should not be?"
+ | Some jsonRpc ->
+ let! results = state.Checker.ParseAndCheckProject projectOptions
+ let diagnostics = results.Errors
+ let diagnosticsPerFile =
+ diagnostics
+ |> Array.fold (fun state t ->
+ let existing = Map.tryFind t.FileName state |> Option.defaultValue []
+ Map.add t.FileName (t :: existing) state) Map.empty
+ for sourceFile in projectOptions.SourceFiles do
+ let diagnostics =
+ Map.tryFind sourceFile diagnosticsPerFile
+ |> Option.defaultValue []
+ |> List.map (fun d ->
+ // F# errors count lines starting at 1, but LSP starts at 0
+ let range: Range =
+ { start = { line = d.StartLineAlternate - 1; character = d.StartColumn }
+ ``end`` = { line = d.EndLineAlternate - 1; character = d.EndColumn } }
+ let severity =
+ match d.Severity with
+ | FSharp.Compiler.SourceCodeServices.FSharpErrorSeverity.Warning -> Diagnostic.Warning
+ | FSharp.Compiler.SourceCodeServices.FSharpErrorSeverity.Error -> Diagnostic.Error
+ let res: Diagnostic =
+ { range = range
+ severity = Some severity
+ code = "FS" + d.ErrorNumber.ToString("0000")
+ source = Some d.FileName
+ message = d.Message
+ relatedInformation = None }
+ res)
+ |> List.toArray
+ let args: PublishDiagnosticsParams =
+ { uri = System.Uri(sourceFile).AbsoluteUri
+ diagnostics = diagnostics }
+
+ // fire each notification separately
+ jsonRpc.NotifyAsync(TextDocumentPublishDiagnostics, args) |> Async.AwaitTask |> Async.Start
}
+ |> (fun computation -> Async.StartAsTask(computation, cancellationToken=publishDiagnosticsCancellationTokenSource.Token))
+ |> Async.AwaitTask
diff --git a/tests/FSharp.Compiler.LanguageServer.UnitTests/DiagnosticsTests.fs b/tests/FSharp.Compiler.LanguageServer.UnitTests/DiagnosticsTests.fs
new file mode 100644
index 000000000..aaadff258
--- /dev/null
+++ b/tests/FSharp.Compiler.LanguageServer.UnitTests/DiagnosticsTests.fs
@@ -0,0 +1,154 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace FSharp.Compiler.LanguageServer.UnitTests
+
+open System
+open System.IO
+open System.Linq
+open System.Threading.Tasks
+open FSharp.Compiler.LanguageServer
+open Nerdbank.Streams
+open NUnit.Framework
+
+[]
+type DiagnosticsTests() =
+
+ let createTestableProject (tfm: string) (sourceFiles: (string * string) list) =
+ let testDir = new TemporaryDirectory()
+ let directoryBuildText = ""
+ File.WriteAllText(Path.Combine(testDir.Directory, "Directory.Build.props"), directoryBuildText)
+ File.WriteAllText(Path.Combine(testDir.Directory, "Directory.Build.targets"), directoryBuildText)
+ for name, contents in sourceFiles do
+ File.WriteAllText(Path.Combine(testDir.Directory, name), contents)
+ let compileItems =
+ sourceFiles
+ |> List.map fst
+ |> List.map (sprintf " ")
+ |> List.fold (fun content line -> content + "\n" + line) ""
+ let replacements =
+ [ "{{COMPILE}}", compileItems
+ "{{TARGETFRAMEWORK}}", tfm ]
+ let projectTemplate =
+ @"
+
+
+ Library
+ {{TARGETFRAMEWORK}}
+
+
+{{COMPILE}}
+
+"
+ let projectFile =
+ replacements
+ |> List.fold (fun (content: string) (find, replace) -> content.Replace(find, replace)) projectTemplate
+ File.WriteAllText(Path.Combine(testDir.Directory, "test.fsproj"), projectFile)
+ testDir
+
+ let createRpcClient (tempDir: TemporaryDirectory) =
+ let clientStream, serverStream = FullDuplexStream.CreatePair().ToTuple()
+ let server = new Server(serverStream, serverStream)
+ server.StartListening()
+ let client = new TestClient(tempDir, clientStream, clientStream, server)
+ client
+
+ let createClientTest (tfm: string) (sourceFiles: (string * string) list) =
+ let testDir = createTestableProject tfm sourceFiles
+ let client = createRpcClient testDir
+ client
+
+ let getDiagnostics (content: string) =
+ async {
+ use client = createClientTest "netstandard2.0" [ "lib.fs", content ]
+ let! diagnostics = client.WaitForDiagnosticsAsync client.Initialize ["lib.fs"]
+ return diagnostics.["lib.fs"]
+ }
+
+ []
+ member __.``No diagnostics for correct code``() =
+ async {
+ let! diagnostics = getDiagnostics @"
+namespace Test
+
+module Numbers =
+ let one: int = 1
+"
+ Assert.AreEqual(0, diagnostics.Length)
+ } |> Async.StartAsTask :> Task
+
+ []
+ member __.``Diagnostics for incorrect code``() =
+ async {
+ let! diagnostics = getDiagnostics @"
+namespace Test
+
+module Numbers =
+ let one: int = false
+"
+ let diag = diagnostics.Single()
+ Assert.AreEqual("FS0001", diag.code)
+ Assert.AreEqual(Some 1, diag.severity)
+ Assert.AreEqual(4, diag.range.start.line)
+ Assert.AreEqual(19, diag.range.start.character)
+ Assert.AreEqual(4, diag.range.``end``.line)
+ Assert.AreEqual(24, diag.range.``end``.character)
+ Assert.AreEqual("This expression was expected to have type\n 'int' \nbut here has type\n 'bool'", diag.message.Trim())
+ Assert.IsTrue(diag.source.Value.EndsWith("lib.fs"))
+ } |> Async.StartAsTask :> Task
+
+ []
+ member __.``Diagnostics added for updated incorrect code``() =
+ async {
+ let correct = @"
+namespace Test
+
+module Numbers =
+ let one: int = 1
+"
+ let incorrect = @"
+namespace Test
+
+module Numbers =
+ let one: int = false
+"
+
+ // verify initial state
+ use client = createClientTest "netstandard2.0" [ "lib.fs", correct ]
+ let! diagnostics = client.WaitForDiagnosticsAsync client.Initialize ["lib.fs"]
+ Assert.AreEqual(0, diagnostics.["lib.fs"].Length)
+
+ // touch file with incorrect data
+ let touch () = File.WriteAllText(Path.Combine(client.RootPath, "lib.fs"), incorrect)
+ let! diagnostics = client.WaitForDiagnostics touch ["lib.fs"]
+ let diag = diagnostics.["lib.fs"].Single()
+ Assert.AreEqual("FS0001", diag.code)
+ } |> Async.StartAsTask :> Task
+
+ []
+ member __.``Diagnostics removed for updated correct code``() =
+ async {
+ let incorrect = @"
+namespace Test
+
+module Numbers =
+ let one: int = false
+"
+ let correct = @"
+namespace Test
+
+module Numbers =
+ let one: int = 1
+"
+
+ // verify initial state
+ use client = createClientTest "netstandard2.0" [ "lib.fs", incorrect ]
+ let! diagnostics = client.WaitForDiagnosticsAsync client.Initialize ["lib.fs"]
+ let diag = diagnostics.["lib.fs"].Single()
+ Assert.AreEqual("FS0001", diag.code)
+
+ // touch file with correct data
+ let touch () = File.WriteAllText(Path.Combine(client.RootPath, "lib.fs"), correct)
+ let! diagnostics = client.WaitForDiagnostics touch ["lib.fs"]
+ let libActualContents = File.ReadAllText(Path.Combine(client.RootPath, "lib.fs"))
+ Assert.AreEqual(0, diagnostics.["lib.fs"].Length, "Actual on-disk contents of lib.fs:\n" + libActualContents)
+ } |> Async.StartAsTask :> Task
diff --git a/tests/FSharp.Compiler.LanguageServer.UnitTests/FSharp.Compiler.LanguageServer.UnitTests.fsproj b/tests/FSharp.Compiler.LanguageServer.UnitTests/FSharp.Compiler.LanguageServer.UnitTests.fsproj
index c457b9e56..4e061f557 100644
--- a/tests/FSharp.Compiler.LanguageServer.UnitTests/FSharp.Compiler.LanguageServer.UnitTests.fsproj
+++ b/tests/FSharp.Compiler.LanguageServer.UnitTests/FSharp.Compiler.LanguageServer.UnitTests.fsproj
@@ -11,8 +11,12 @@
+
+
+
+
diff --git a/tests/FSharp.Compiler.LanguageServer.UnitTests/MiscTests.fs b/tests/FSharp.Compiler.LanguageServer.UnitTests/MiscTests.fs
new file mode 100644
index 000000000..02104be2e
--- /dev/null
+++ b/tests/FSharp.Compiler.LanguageServer.UnitTests/MiscTests.fs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace FSharp.Compiler.LanguageServer.UnitTests
+
+open System.IO
+open FSharp.Compiler.LanguageServer
+open NUnit.Framework
+
+[]
+type MiscTests() =
+
+ []
+ member __.``Find F# projects in a .sln file``() =
+ let slnContent = @"
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29201.188
+MinimumVisualStudioVersion = 10.0.40219.1
+Project(""{F2A71F9B-5D33-465A-A702-920D77279786}"") = ""ConsoleApp1"", ""ConsoleApp1\ConsoleApp1.fsproj"", ""{60A4BE67-7E03-4200-AD38-B0E5E8E049C1}""
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {60A4BE67-7E03-4200-AD38-B0E5E8E049C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {60A4BE67-7E03-4200-AD38-B0E5E8E049C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {60A4BE67-7E03-4200-AD38-B0E5E8E049C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {60A4BE67-7E03-4200-AD38-B0E5E8E049C1}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {80902CFC-54E6-4485-AC17-4516930C8B2B}
+ EndGlobalSection
+EndGlobal
+"
+ let testDir = @"C:\Dir\With\Solution" // don't care about the potentially improper directory separators here, it's really just a dumb string
+ let foundProjects = Solution.getProjectPaths slnContent testDir
+ let expected = Path.Combine(testDir, "ConsoleApp1", "ConsoleApp1.fsproj") // proper directory separator characters will be used at runtime
+ Assert.AreEqual([| expected |], foundProjects)
diff --git a/tests/FSharp.Compiler.LanguageServer.UnitTests/ProtocolTests.fs b/tests/FSharp.Compiler.LanguageServer.UnitTests/ProtocolTests.fs
index 4d0a789b9..e9de31049 100644
--- a/tests/FSharp.Compiler.LanguageServer.UnitTests/ProtocolTests.fs
+++ b/tests/FSharp.Compiler.LanguageServer.UnitTests/ProtocolTests.fs
@@ -31,21 +31,17 @@ type ProtocolTests() =
client.StartListening()
// initialize
- let capabilities =
- { ClientCapabilities.workspace = None
+ let capabilities: ClientCapabilities =
+ { workspace = None
textDocument = None
experimental = None
supportsVisualStudioExtensions = None }
let! result =
- client.InvokeAsync(
- "initialize", // method
- 0, // processId
- "rootPath",
- "rootUri",
- null, // initializationOptions
- capabilities, // client capabilities
- "none") // trace
- |> Async.AwaitTask
+ client.InvokeWithParameterObjectAsync(
+ "initialize",
+ {| processId = Process.GetCurrentProcess().Id
+ capabilities = capabilities |}
+ ) |> Async.AwaitTask
Assert.True(result.capabilities.hoverProvider)
do! client.NotifyAsync("initialized") |> Async.AwaitTask
diff --git a/tests/FSharp.Compiler.LanguageServer.UnitTests/TemporaryDirectory.fs b/tests/FSharp.Compiler.LanguageServer.UnitTests/TemporaryDirectory.fs
new file mode 100644
index 000000000..ef3953d78
--- /dev/null
+++ b/tests/FSharp.Compiler.LanguageServer.UnitTests/TemporaryDirectory.fs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace FSharp.Compiler.LanguageServer.UnitTests
+
+open System
+open System.IO
+
+type TemporaryDirectory() =
+
+ let directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())
+ do Directory.CreateDirectory(directory) |> ignore
+
+ member __.Directory = directory
+
+ interface IDisposable with
+ member __.Dispose() =
+ try
+ Directory.Delete(directory, true)
+ with
+ | _ -> ()
diff --git a/tests/FSharp.Compiler.LanguageServer.UnitTests/TestClient.fs b/tests/FSharp.Compiler.LanguageServer.UnitTests/TestClient.fs
new file mode 100644
index 000000000..0baaaf40e
--- /dev/null
+++ b/tests/FSharp.Compiler.LanguageServer.UnitTests/TestClient.fs
@@ -0,0 +1,121 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace FSharp.Compiler.LanguageServer.UnitTests
+
+open System
+open System.Collections.Generic
+open System.Diagnostics
+open System.IO
+open System.Threading
+open FSharp.Compiler.LanguageServer
+open Newtonsoft.Json.Linq
+open StreamJsonRpc
+
+type TestClient(tempDir: TemporaryDirectory, sendingStream: Stream, receivingStream: Stream, server: Server) =
+
+ let rootPath = tempDir.Directory
+ let rootPath = if rootPath.EndsWith(Path.DirectorySeparatorChar.ToString()) then rootPath else rootPath + Path.DirectorySeparatorChar.ToString()
+ let diagnosticsEvent = Event<_>()
+
+ let formatter = JsonMessageFormatter()
+ let converter = JsonOptionConverter() // special handler to convert between `Option<'T>` and `obj/null`.
+ do formatter.JsonSerializer.Converters.Add(converter)
+ let handler = new HeaderDelimitedMessageHandler(sendingStream, receivingStream, formatter)
+ let client = new JsonRpc(handler)
+ let handler (functionName: string) (args: JToken): JToken =
+ match functionName with
+ | TextDocumentPublishDiagnostics ->
+ let args = args.ToObject(formatter.JsonSerializer)
+ let fullPath = Uri(args.uri).LocalPath
+ let shortPath = if fullPath.StartsWith(rootPath) then fullPath.Substring(rootPath.Length) else fullPath
+ diagnosticsEvent.Trigger((shortPath, args.diagnostics))
+ null
+ | _ -> null
+ let addHandler (name: string) =
+ client.AddLocalRpcMethod(name, new Func(handler name))
+ do addHandler TextDocumentPublishDiagnostics
+ do client.StartListening()
+
+ member __.RootPath = rootPath
+
+ member __.Server = server
+
+ []
+ member __.PublishDiagnostics = diagnosticsEvent.Publish
+
+ member __.Initialize () =
+ async {
+ do! client.NotifyWithParameterObjectAsync(OptionsSet, {| options = Options.AllOn() |}) |> Async.AwaitTask
+ let capabilities: ClientCapabilities =
+ { workspace = None
+ textDocument = None
+ experimental = None
+ supportsVisualStudioExtensions = None }
+ let! _result =
+ client.InvokeWithParameterObjectAsync(
+ "initialize", // method
+ {| processId = Process.GetCurrentProcess().Id
+ rootPath = rootPath
+ capabilities = capabilities |}
+ ) |> Async.AwaitTask
+ return ()
+ }
+
+ member this.WaitForDiagnostics (triggerAction: unit -> unit) (fileNames: string list) =
+ async {
+ // prepare file diagnostic triggers
+ let diagnosticTriggers = Dictionary()
+ fileNames |> List.iter (fun f -> diagnosticTriggers.[f] <- new ManualResetEvent(false))
+
+ // prepare callback handler
+ let diagnosticsMap = Dictionary()
+ let handler (fileName: string, diagnostics: Diagnostic[]) =
+ diagnosticsMap.[fileName] <- diagnostics
+ // auto-generated files (e.g., AssemblyInfo.fs) won't be in the trigger map
+ if diagnosticTriggers.ContainsKey(fileName) then
+ diagnosticTriggers.[fileName].Set() |> ignore
+
+ // subscribe to the event
+ let wrappedHandler = new Handler(fun _sender args -> handler args)
+ this.PublishDiagnostics.AddHandler(wrappedHandler)
+ triggerAction ()
+
+ // wait for all triggers to hit
+ let! results =
+ diagnosticTriggers
+ |> Seq.map (fun entry ->
+ async {
+ let! result = Async.AwaitWaitHandle(entry.Value, millisecondsTimeout = int (TimeSpan.FromSeconds(10.0).TotalMilliseconds))
+ return if result then None
+ else
+ let filePath = Path.Combine(rootPath, entry.Key)
+ let actualContents = File.ReadAllText(filePath)
+ Some <| sprintf "No diagnostics received for file '%s'. Contents:\n%s\n" entry.Key actualContents
+ })
+ |> Async.Parallel
+ let results = results |> Array.choose (fun x -> x)
+ if results.Length > 0 then
+ let combinedErrors = String.Join("-----\n", results)
+ let allDiagnosticsEvents =
+ diagnosticsMap
+ |> Seq.map (fun entry ->
+ sprintf "File '%s' reported %d diagnostics." entry.Key entry.Value.Length)
+ |> (fun s -> String.Join("\n", s))
+ failwith <| sprintf "Error waiting for diagnostics:\n%s\n-----\n%s" combinedErrors allDiagnosticsEvents
+
+ // clean up event
+ this.PublishDiagnostics.RemoveHandler(wrappedHandler)
+
+ // done
+ return diagnosticsMap
+ }
+
+ member this.WaitForDiagnosticsAsync (triggerAction: unit -> Async) (fileNames: string list) =
+ this.WaitForDiagnostics (fun () -> triggerAction () |> Async.RunSynchronously) fileNames
+
+ interface IDisposable with
+ member __.Dispose() =
+ try
+ (tempDir :> IDisposable).Dispose()
+ with
+ | _ -> ()
diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs
index 273e0bad4..0d92ce047 100644
--- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs
+++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs
@@ -33,7 +33,10 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () =
let getProjectInfoManager(document: Document) =
document.Project.Solution.Workspace.Services.GetService().FSharpProjectOptionsManager
-
+
+ let getSettings(document: Document) =
+ document.Project.Solution.Workspace.Services.GetService()
+
static let errorInfoEqualityComparer =
{ new IEqualityComparer with
member __.Equals (x, y) =
@@ -110,6 +113,10 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () =
interface IFSharpDocumentDiagnosticAnalyzer with
member this.AnalyzeSyntaxAsync(document: Document, cancellationToken: CancellationToken): Task> =
+ // if using LSP, just bail early
+ let settings = getSettings document
+ if settings.Advanced.UsePreviewDiagnostics then Task.FromResult(ImmutableArray.Empty)
+ else
let projectInfoManager = getProjectInfoManager document
asyncMaybe {
let! parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document, cancellationToken)
@@ -123,6 +130,10 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () =
|> RoslynHelpers.StartAsyncAsTask cancellationToken
member this.AnalyzeSemanticsAsync(document: Document, cancellationToken: CancellationToken): Task> =
+ // if using LSP, just bail early
+ let settings = getSettings document
+ if settings.Advanced.UsePreviewDiagnostics then Task.FromResult(ImmutableArray.Empty)
+ else
let projectInfoManager = getProjectInfoManager document
asyncMaybe {
let! parsingOptions, _, projectOptions = projectInfoManager.TryGetOptionsForDocumentOrProject(document, cancellationToken)
diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpLanguageClient.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpLanguageClient.fs
index 176ba2cc5..7a987d665 100644
--- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpLanguageClient.fs
+++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpLanguageClient.fs
@@ -34,7 +34,8 @@ type FSharpContentDefinition() =
type internal FSharpLanguageClient
[]
(
- lspService: LspService
+ lspService: LspService,
+ settings: EditorOptions
) =
inherit LanguageClient()
override __.Name = "F# Language Service"
@@ -63,4 +64,7 @@ type internal FSharpLanguageClient
member __.CustomMessageTarget = null
member __.MiddleLayer = null
member __.AttachForCustomMessageAsync(rpc: JsonRpc) =
- lspService.SetJsonRpc(rpc) |> Async.StartAsTask :> Task
+ async {
+ do! lspService.SetJsonRpc(rpc)
+ do! lspService.SetOptions(settings.Advanced.AsLspOptions())
+ } |> Async.StartAsTask :> Task
diff --git a/vsintegration/src/FSharp.Editor/Options/EditorOptions.fs b/vsintegration/src/FSharp.Editor/Options/EditorOptions.fs
index 3d71e2522..af88f02d1 100644
--- a/vsintegration/src/FSharp.Editor/Options/EditorOptions.fs
+++ b/vsintegration/src/FSharp.Editor/Options/EditorOptions.fs
@@ -93,11 +93,16 @@ type CodeLensOptions =
type AdvancedOptions =
{ IsBlockStructureEnabled: bool
IsOutliningEnabled: bool
- UsePreviewTextHover: bool }
+ UsePreviewTextHover: bool
+ UsePreviewDiagnostics: bool }
static member Default =
{ IsBlockStructureEnabled = true
IsOutliningEnabled = true
- UsePreviewTextHover = false }
+ UsePreviewTextHover = false
+ UsePreviewDiagnostics = false }
+ member this.AsLspOptions(): Options =
+ { usePreviewTextHover = this.UsePreviewTextHover
+ usePreviewDiagnostics = this.UsePreviewDiagnostics }
[]
type FormattingOptions =
@@ -203,8 +208,7 @@ module internal OptionsUI =
async {
let lspService = this.GetService()
let settings = this.GetService()
- let options =
- { Options.usePreviewTextHover = settings.Advanced.UsePreviewTextHover }
+ let options = settings.Advanced.AsLspOptions()
do! lspService.SetOptions options
} |> Async.Start
diff --git a/vsintegration/src/FSharp.UIResources/AdvancedOptionsControl.xaml b/vsintegration/src/FSharp.UIResources/AdvancedOptionsControl.xaml
index f8dd82f7d..57ca75df7 100644
--- a/vsintegration/src/FSharp.UIResources/AdvancedOptionsControl.xaml
+++ b/vsintegration/src/FSharp.UIResources/AdvancedOptionsControl.xaml
@@ -27,8 +27,12 @@
diff --git a/vsintegration/src/FSharp.UIResources/Strings.Designer.cs b/vsintegration/src/FSharp.UIResources/Strings.Designer.cs
index 661fffa56..c03ff05d7 100644
--- a/vsintegration/src/FSharp.UIResources/Strings.Designer.cs
+++ b/vsintegration/src/FSharp.UIResources/Strings.Designer.cs
@@ -150,6 +150,15 @@ namespace Microsoft.VisualStudio.FSharp.UIResources {
}
}
+ ///
+ /// Looks up a localized string similar to Diagnostics.
+ ///
+ public static string Diagnostics {
+ get {
+ return ResourceManager.GetString("Diagnostics", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to D_ot underline.
///
diff --git a/vsintegration/src/FSharp.UIResources/Strings.resx b/vsintegration/src/FSharp.UIResources/Strings.resx
index f4066cf34..037596b71 100644
--- a/vsintegration/src/FSharp.UIResources/Strings.resx
+++ b/vsintegration/src/FSharp.UIResources/Strings.resx
@@ -237,4 +237,7 @@
Text hover
+
+ Diagnostics
+
\ No newline at end of file
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.cs.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.cs.xlf
index 973cdbd6e..0d2794979 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.cs.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.cs.xlf
@@ -42,6 +42,11 @@
Seznamy dokončení
+
+
+ Diagnostics
+
+
Výkon
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.de.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.de.xlf
index c2defd7c4..fc1d36391 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.de.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.de.xlf
@@ -42,6 +42,11 @@
Vervollständigungslisten
+
+
+ Diagnostics
+
+
Leistung
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.es.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.es.xlf
index 2b0507a43..24649ecad 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.es.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.es.xlf
@@ -42,6 +42,11 @@
Listas de finalización
+
+
+ Diagnostics
+
+
Rendimiento
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.fr.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.fr.xlf
index 423d9c97c..d0f77ba36 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.fr.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.fr.xlf
@@ -42,6 +42,11 @@
Listes de saisie semi-automatique
+
+
+ Diagnostics
+
+
Performances
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.it.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.it.xlf
index a577a1e31..a5555f973 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.it.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.it.xlf
@@ -42,6 +42,11 @@
Elenchi di completamento
+
+
+ Diagnostics
+
+
Prestazioni
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.ja.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.ja.xlf
index de08a5885..847e29436 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.ja.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.ja.xlf
@@ -42,6 +42,11 @@
入力候補一覧
+
+
+ Diagnostics
+
+
パフォーマンス
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.ko.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.ko.xlf
index 0651ce96f..55927919d 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.ko.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.ko.xlf
@@ -42,6 +42,11 @@
완성 목록
+
+
+ Diagnostics
+
+
성능
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.pl.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.pl.xlf
index 1c6085f85..db4a13948 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.pl.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.pl.xlf
@@ -42,6 +42,11 @@
Listy uzupełniania
+
+
+ Diagnostics
+
+
Wydajność
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.pt-BR.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.pt-BR.xlf
index 2fca2b08f..5385f6147 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.pt-BR.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.pt-BR.xlf
@@ -42,6 +42,11 @@
Listas de Conclusão
+
+
+ Diagnostics
+
+
Desempenho
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.ru.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.ru.xlf
index de8534643..ba6871df2 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.ru.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.ru.xlf
@@ -42,6 +42,11 @@
Списки завершения
+
+
+ Diagnostics
+
+
Производительность
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.tr.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.tr.xlf
index dd44a1a1b..9caf1fa8b 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.tr.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.tr.xlf
@@ -42,6 +42,11 @@
Tamamlama Listeleri
+
+
+ Diagnostics
+
+
Performans
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hans.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hans.xlf
index ba5c37bf7..cff797b88 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hans.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hans.xlf
@@ -42,6 +42,11 @@
完成列表
+
+
+ Diagnostics
+
+
性能
diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hant.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hant.xlf
index db714fc7b..a3267c876 100644
--- a/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hant.xlf
+++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hant.xlf
@@ -42,6 +42,11 @@
完成清單
+
+
+ Diagnostics
+
+
效能