add workspace and diagnostics to lsp (#7006)

This commit is contained in:
Brett V. Forsgren 2019-09-27 09:35:22 -07:00 коммит произвёл GitHub
Родитель f49548692d
Коммит 8121dd5cd6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
34 изменённых файлов: 855 добавлений и 41 удалений

Просмотреть файл

@ -0,0 +1,49 @@
<Project DefaultTargets="Build">
<!--
Usage: dotnet build <this-file> /p:ProjectFile=<path-to-project>
Returns: The project's supported target frameworks, one per line, in the format of:
DetectedTargetFramework=<tfm-1>
DetectedTargetFramework=<tfm-2>
...
Usage: dotnet build <this-file> /p:ProjectFile=<path-to-project> /p:TargetFramework=<tfm>
Returns: The project's command line arguments, one per line, as they would be passed to fsc.exe in the format of:
DetectedCommandLineArg=<arg-1>
DetectedCommandLineArg=<arg-2>
...
To avoid recreating a distribution of MSBuild just to get some evaluated project values, we instead inject a special
targets file into the specified project which then prints the values with a well-known prefix that's easy to parse.
The benefits of doing it this way are:
- We don't have to re-create a copy of MSBuild. That would be difficult, large, and change frequently.
- This works on any OS and any TFM.
- We use the exact version of MSBuild and the compiler that the user has instead of dealing with the potential
mismatch between what this tool would include vs. what they actually have.
The downsides of this method are:
- We're dependent upon the continued existence of some well-known Targets and ItemGroups, but the ones we depend on
have been stable for some time.
- An external process invoke is slow, but this is only done once at LSP instantiation.
-->
<PropertyGroup>
<!-- See this file for details on the magic. -->
<InjetedTargetsFile>$(MSBuildThisFileDirectory)FSharp.Compiler.LanguageServer.DesignTime.targets</InjetedTargetsFile>
</PropertyGroup>
<Target Name="Build">
<Error Text="Property `ProjectFile` must be specified." Condition="'$(ProjectFile)' == ''" />
<!-- report TFMs if none specified -->
<MSBuild Projects="$(ProjectFile)" Targets="ReportTargetFrameworks" Properties="CustomAfterMicrosoftCommonCrossTargetingTargets=$(InjetedTargetsFile);CustomAfterMicrosoftCommonTargets=$(InjetedTargetsFile)" Condition="'$(TargetFramework)' == ''" />
<!-- report command line arguments when TFM is given -->
<MSBuild Projects="$(ProjectFile)" Targets="Restore" Condition="'$(TargetFramework)' != ''" />
<MSBuild Projects="$(ProjectFile)" Targets="ReportCommandLineArgs" Properties="DesignTimeBuild=true;CustomAfterMicrosoftCommonTargets=$(InjetedTargetsFile);TargetFramework=$(TargetFramework)" Condition="'$(TargetFramework)' != ''" />
</Target>
</Project>

Просмотреть файл

@ -0,0 +1,52 @@
<Project>
<!--
Accurately reporting the command line arguments requires that the target Restore is run _without_ these values having
been set. The project file helper associated with this .targets file calls Restore before calling into the target
that prints the command line arguments.
-->
<PropertyGroup Condition="'$(DesignTimeBuild)' == 'true'">
<ProvideCommandLineArgs>true</ProvideCommandLineArgs>
<BuildProjectReferences>false</BuildProjectReferences>
<SkipCompilerExecution>true</SkipCompilerExecution>
<DisableRarCache>true</DisableRarCache>
<AutoGenerateBindingRedirects>false</AutoGenerateBindingRedirects>
<CopyBuildOutputToOutputDirectory>false</CopyBuildOutputToOutputDirectory>
<CopyOutputSymbolsToOutputDirectory>false</CopyOutputSymbolsToOutputDirectory>
<SkipCopyBuildProduct>true</SkipCopyBuildProduct>
<AddModules>false</AddModules>
<UseCommonOutputDirectory>true</UseCommonOutputDirectory>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
</PropertyGroup>
<!-- displays the target frameworks of a cross-targeted project -->
<Target Name="_ComputeDependsOn">
<!--
In a multi-targeted project, the _ComputeTargetFrameworkItems populates the ItemGroup _TargetFramework with the
appropriate values. If the project contains just a single value for $(TargetFramework) then the target
_ComputeTargetFrameworkItems doesn't exist, so instead the helper target _PopulateTargetFrameworks is run which
populates the same group.
-->
<PropertyGroup>
<WorkerDependsOn Condition="'$(TargetFramework)' == ''">_ComputeTargetFrameworkItems</WorkerDependsOn>
<WorkerDependsOn Condition="'$(TargetFramework)' != ''">_PopulateTargetFrameworks</WorkerDependsOn>
</PropertyGroup>
</Target>
<Target Name="_PopulateTargetFrameworks">
<ItemGroup>
<_TargetFramework Include="$(TargetFramework)" />
</ItemGroup>
</Target>
<Target Name="_ReportTargetFrameworksWorker" DependsOnTargets="_ComputeDependsOn;$(WorkerDependsOn)">
<Message Text="DetectedTargetFramework=%(_TargetFramework.Identity)" Importance="High" />
</Target>
<Target Name="ReportTargetFrameworks" DependsOnTargets="_ComputeDependsOn;_ReportTargetFrameworksWorker">
</Target>
<!-- displays the command line arguments passed to fsc.exe -->
<Target Name="ReportCommandLineArgs"
DependsOnTargets="Build">
<Message Text="DetectedCommandLineArg=%(FscCommandLineArgs.Identity)" Importance="High" />
</Target>
</Project>

Просмотреть файл

@ -23,6 +23,15 @@
<Compile Include="Program.fs" /> <Compile Include="Program.fs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="FSharp.Compiler.LanguageServer.DesignTime.proj" CopyToOutputDirectory="PreserveNewest" />
<None Include="FSharp.Compiler.LanguageServer.DesignTime.targets" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="FSharp.Compiler.LanguageServer.UnitTests" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)..\FSharp.Compiler.Private\FSharp.Compiler.Private.fsproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)..\FSharp.Compiler.Private\FSharp.Compiler.Private.fsproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)..\FSharp.Core\FSharp.Core.fsproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)..\FSharp.Core\FSharp.Core.fsproj" />

Просмотреть файл

@ -9,10 +9,18 @@ module FunctionNames =
[<Literal>] [<Literal>]
let OptionsSet = "options/set" let OptionsSet = "options/set"
[<Literal>]
let TextDocumentPublishDiagnostics = "textDocument/publishDiagnostics"
type Options = type Options =
{ usePreviewTextHover: bool } { usePreviewTextHover: bool
usePreviewDiagnostics: bool }
static member Default() = static member Default() =
{ usePreviewTextHover = false } { usePreviewTextHover = false
usePreviewDiagnostics = false }
static member AllOn() =
{ usePreviewTextHover = true
usePreviewDiagnostics = true }
module Extensions = module Extensions =
type JsonRpc with type JsonRpc with

Просмотреть файл

@ -5,8 +5,9 @@ namespace FSharp.Compiler.LanguageServer
open Newtonsoft.Json.Linq open Newtonsoft.Json.Linq
open Newtonsoft.Json open Newtonsoft.Json
// Interfaces as defined at https://microsoft.github.io/language-server-protocol/specification. The properties on // Interfaces as defined at https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/.
// these types are camlCased to match the underlying JSON properties to avoid attributes on every field: // The properties on these types are camlCased to match the underlying JSON properties to avoid attributes on every
// field:
// [<JsonProperty("camlCased")>] // [<JsonProperty("camlCased")>]
/// Represents a zero-based line and column of a text document. /// Represents a zero-based line and column of a text document.
@ -32,7 +33,7 @@ type Diagnostic =
{ range: Range { range: Range
severity: int option severity: int option
code: string code: string
source: string option // "F#" source: string option
message: string message: string
relatedInformation: DiagnosticRelatedInformation[] option } relatedInformation: DiagnosticRelatedInformation[] option }
static member Error = 1 static member Error = 1
@ -46,7 +47,7 @@ type PublishDiagnosticsParams =
type ClientCapabilities = type ClientCapabilities =
{ workspace: JToken option // TODO: WorkspaceClientCapabilities { workspace: JToken option // TODO: WorkspaceClientCapabilities
textDocument: JToken option // TODO: TextDocumentCapabilities textDocument: JToken option // TODO: TextDocumentClientCapabilities, publishDiagnostics: { relatedInformation: bool option }
experimental: JToken option experimental: JToken option
supportsVisualStudioExtensions: bool option } supportsVisualStudioExtensions: bool option }

Просмотреть файл

@ -8,8 +8,10 @@ open System.Threading
open Newtonsoft.Json.Linq open Newtonsoft.Json.Linq
open StreamJsonRpc open StreamJsonRpc
// https://microsoft.github.io/language-server-protocol/specification // https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/
type Methods(state: State) = type Methods() =
let state = State()
/// Helper to run Async<'T> with a CancellationToken. /// Helper to run Async<'T> with a CancellationToken.
let runAsync (cancellationToken: CancellationToken) (computation: Async<'T>) = Async.StartAsTask(computation, cancellationToken=cancellationToken) let runAsync (cancellationToken: CancellationToken) (computation: Async<'T>) = Async.StartAsTask(computation, cancellationToken=cancellationToken)
@ -29,8 +31,10 @@ type Methods(state: State) =
[<Optional; DefaultParameterValue(null: JToken)>] initializationOptions: JToken, [<Optional; DefaultParameterValue(null: JToken)>] initializationOptions: JToken,
capabilities: ClientCapabilities, capabilities: ClientCapabilities,
[<Optional; DefaultParameterValue(null: string)>] trace: string, [<Optional; DefaultParameterValue(null: string)>] trace: string,
[<Optional; DefaultParameterValue(null: WorkspaceFolder[])>] workspaceFolders: WorkspaceFolder[] [<Optional; DefaultParameterValue(null: WorkspaceFolder[])>] workspaceFolders: WorkspaceFolder[],
[<Optional; DefaultParameterValue(CancellationToken())>] cancellationToken: CancellationToken
) = ) =
state.Initialize rootPath rootUri (fun projectOptions -> TextDocument.PublishDiagnostics(state, projectOptions) |> Async.Start)
{ InitializeResult.capabilities = ServerCapabilities.DefaultCapabilities() } { InitializeResult.capabilities = ServerCapabilities.DefaultCapabilities() }
[<JsonRpcMethod("initialized")>] [<JsonRpcMethod("initialized")>]
@ -63,5 +67,6 @@ type Methods(state: State) =
( (
options: Options options: Options
) = ) =
sprintf "got options %A" options |> Console.Error.WriteLine eprintfn "got options %A" options
state.Options <- options state.Options <- options
state.InvalidateAllProjects()

Просмотреть файл

@ -12,17 +12,17 @@ type Server(sendingStream: Stream, receivingStream: Stream) =
let converter = JsonOptionConverter() // special handler to convert between `Option<'T>` and `obj/null`. let converter = JsonOptionConverter() // special handler to convert between `Option<'T>` and `obj/null`.
do formatter.JsonSerializer.Converters.Add(converter) do formatter.JsonSerializer.Converters.Add(converter)
let handler = new HeaderDelimitedMessageHandler(sendingStream, receivingStream, formatter) let handler = new HeaderDelimitedMessageHandler(sendingStream, receivingStream, formatter)
let state = State() let methods = Methods()
let methods = Methods(state)
let rpc = new JsonRpc(handler, methods) let rpc = new JsonRpc(handler, methods)
do methods.State.JsonRpc <- Some rpc
member __.StartListening() = member __.StartListening() =
rpc.StartListening() rpc.StartListening()
member __.WaitForExitAsync() = member __.WaitForExitAsync() =
async { async {
do! Async.AwaitEvent (state.Shutdown) do! Async.AwaitEvent (methods.State.Shutdown)
do! Async.AwaitEvent (state.Exit) do! Async.AwaitEvent (methods.State.Exit)
} }
interface IDisposable with interface IDisposable with

Просмотреть файл

@ -2,11 +2,209 @@
namespace FSharp.Compiler.LanguageServer 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() = type State() =
let checker = FSharpChecker.Create()
let sourceFileToProjectMap = ConcurrentDictionary<string, FSharpProjectOptions>()
let shutdownEvent = new Event<_>() let shutdownEvent = new Event<_>()
let exitEvent = new Event<_>() let exitEvent = new Event<_>()
let cancelEvent = 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<string>()
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<State>.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
[<CLIEvent>] [<CLIEvent>]
member __.Shutdown = shutdownEvent.Publish member __.Shutdown = shutdownEvent.Publish
@ -17,10 +215,19 @@ type State() =
[<CLIEvent>] [<CLIEvent>]
member __.Cancel = cancelEvent.Publish member __.Cancel = cancelEvent.Publish
[<CLIEvent>]
member __.ProjectInvalidated = projectInvalidatedEvent.Publish
member __.DoShutdown() = shutdownEvent.Trigger() member __.DoShutdown() = shutdownEvent.Trigger()
member __.DoExit() = exitEvent.Trigger() member __.DoExit() = exitEvent.Trigger()
member __.DoCancel() = cancelEvent.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 Options = Options.Default() with get, set
member val JsonRpc: JsonRpc option = None with get, set

Просмотреть файл

@ -2,13 +2,15 @@
namespace FSharp.Compiler.LanguageServer namespace FSharp.Compiler.LanguageServer
open System open System.Threading
module TextDocument = module TextDocument =
let mutable publishDiagnosticsCancellationTokenSource = new CancellationTokenSource()
let Hover (state: State) (textDocument: TextDocumentIdentifier) (position: Position) = let Hover (state: State) (textDocument: TextDocumentIdentifier) (position: Position) =
async { 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 if not state.Options.usePreviewTextHover then return None
else else
let startCol, endCol = 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 { async {
return { if not state.Options.usePreviewDiagnostics then return ()
PublishDiagnosticsParams.uri = "" else
diagnostics = [||] 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

Просмотреть файл

@ -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
[<TestFixture>]
type DiagnosticsTests() =
let createTestableProject (tfm: string) (sourceFiles: (string * string) list) =
let testDir = new TemporaryDirectory()
let directoryBuildText = "<Project />"
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 " <Compile Include=\"%s\" />")
|> List.fold (fun content line -> content + "\n" + line) ""
let replacements =
[ "{{COMPILE}}", compileItems
"{{TARGETFRAMEWORK}}", tfm ]
let projectTemplate =
@"
<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>{{TARGETFRAMEWORK}}</TargetFramework>
</PropertyGroup>
<ItemGroup>
{{COMPILE}}
</ItemGroup>
</Project>"
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"]
}
[<Test>]
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
[<Test>]
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
[<Test; Ignore("FileSystemWatcher isn't 100% reliable.")>]
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
[<Test; Ignore("FileSystemWatcher isn't 100% reliable.")>]
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

Просмотреть файл

@ -11,8 +11,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="TemporaryDirectory.fs" />
<Compile Include="TestClient.fs" />
<Compile Include="ProtocolTests.fs" /> <Compile Include="ProtocolTests.fs" />
<Compile Include="SerializationTests.fs" /> <Compile Include="SerializationTests.fs" />
<Compile Include="DiagnosticsTests.fs" />
<Compile Include="MiscTests.fs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

Просмотреть файл

@ -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
[<TestFixture>]
type MiscTests() =
[<Test>]
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)

Просмотреть файл

@ -31,21 +31,17 @@ type ProtocolTests() =
client.StartListening() client.StartListening()
// initialize // initialize
let capabilities = let capabilities: ClientCapabilities =
{ ClientCapabilities.workspace = None { workspace = None
textDocument = None textDocument = None
experimental = None experimental = None
supportsVisualStudioExtensions = None } supportsVisualStudioExtensions = None }
let! result = let! result =
client.InvokeAsync<InitializeResult>( client.InvokeWithParameterObjectAsync<InitializeResult>(
"initialize", // method "initialize",
0, // processId {| processId = Process.GetCurrentProcess().Id
"rootPath", capabilities = capabilities |}
"rootUri", ) |> Async.AwaitTask
null, // initializationOptions
capabilities, // client capabilities
"none") // trace
|> Async.AwaitTask
Assert.True(result.capabilities.hoverProvider) Assert.True(result.capabilities.hoverProvider)
do! client.NotifyAsync("initialized") |> Async.AwaitTask do! client.NotifyAsync("initialized") |> Async.AwaitTask

Просмотреть файл

@ -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
| _ -> ()

Просмотреть файл

@ -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<PublishDiagnosticsParams>(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<JToken, JToken>(handler name))
do addHandler TextDocumentPublishDiagnostics
do client.StartListening()
member __.RootPath = rootPath
member __.Server = server
[<CLIEvent>]
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<InitializeResult>(
"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<string, ManualResetEvent>()
fileNames |> List.iter (fun f -> diagnosticTriggers.[f] <- new ManualResetEvent(false))
// prepare callback handler
let diagnosticsMap = Dictionary<string, Diagnostic[]>()
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<string * Diagnostic[]>(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<unit>) (fileNames: string list) =
this.WaitForDiagnostics (fun () -> triggerAction () |> Async.RunSynchronously) fileNames
interface IDisposable with
member __.Dispose() =
try
(tempDir :> IDisposable).Dispose()
with
| _ -> ()

Просмотреть файл

@ -34,6 +34,9 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () =
let getProjectInfoManager(document: Document) = let getProjectInfoManager(document: Document) =
document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().FSharpProjectOptionsManager document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().FSharpProjectOptionsManager
let getSettings(document: Document) =
document.Project.Solution.Workspace.Services.GetService<EditorOptions>()
static let errorInfoEqualityComparer = static let errorInfoEqualityComparer =
{ new IEqualityComparer<FSharpErrorInfo> with { new IEqualityComparer<FSharpErrorInfo> with
member __.Equals (x, y) = member __.Equals (x, y) =
@ -110,6 +113,10 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () =
interface IFSharpDocumentDiagnosticAnalyzer with interface IFSharpDocumentDiagnosticAnalyzer with
member this.AnalyzeSyntaxAsync(document: Document, cancellationToken: CancellationToken): Task<ImmutableArray<Diagnostic>> = member this.AnalyzeSyntaxAsync(document: Document, cancellationToken: CancellationToken): Task<ImmutableArray<Diagnostic>> =
// if using LSP, just bail early
let settings = getSettings document
if settings.Advanced.UsePreviewDiagnostics then Task.FromResult(ImmutableArray<Diagnostic>.Empty)
else
let projectInfoManager = getProjectInfoManager document let projectInfoManager = getProjectInfoManager document
asyncMaybe { asyncMaybe {
let! parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document, cancellationToken) let! parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document, cancellationToken)
@ -123,6 +130,10 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () =
|> RoslynHelpers.StartAsyncAsTask cancellationToken |> RoslynHelpers.StartAsyncAsTask cancellationToken
member this.AnalyzeSemanticsAsync(document: Document, cancellationToken: CancellationToken): Task<ImmutableArray<Diagnostic>> = member this.AnalyzeSemanticsAsync(document: Document, cancellationToken: CancellationToken): Task<ImmutableArray<Diagnostic>> =
// if using LSP, just bail early
let settings = getSettings document
if settings.Advanced.UsePreviewDiagnostics then Task.FromResult(ImmutableArray<Diagnostic>.Empty)
else
let projectInfoManager = getProjectInfoManager document let projectInfoManager = getProjectInfoManager document
asyncMaybe { asyncMaybe {
let! parsingOptions, _, projectOptions = projectInfoManager.TryGetOptionsForDocumentOrProject(document, cancellationToken) let! parsingOptions, _, projectOptions = projectInfoManager.TryGetOptionsForDocumentOrProject(document, cancellationToken)

Просмотреть файл

@ -34,7 +34,8 @@ type FSharpContentDefinition() =
type internal FSharpLanguageClient type internal FSharpLanguageClient
[<ImportingConstructor>] [<ImportingConstructor>]
( (
lspService: LspService lspService: LspService,
settings: EditorOptions
) = ) =
inherit LanguageClient() inherit LanguageClient()
override __.Name = "F# Language Service" override __.Name = "F# Language Service"
@ -63,4 +64,7 @@ type internal FSharpLanguageClient
member __.CustomMessageTarget = null member __.CustomMessageTarget = null
member __.MiddleLayer = null member __.MiddleLayer = null
member __.AttachForCustomMessageAsync(rpc: JsonRpc) = member __.AttachForCustomMessageAsync(rpc: JsonRpc) =
lspService.SetJsonRpc(rpc) |> Async.StartAsTask :> Task async {
do! lspService.SetJsonRpc(rpc)
do! lspService.SetOptions(settings.Advanced.AsLspOptions())
} |> Async.StartAsTask :> Task

Просмотреть файл

@ -93,11 +93,16 @@ type CodeLensOptions =
type AdvancedOptions = type AdvancedOptions =
{ IsBlockStructureEnabled: bool { IsBlockStructureEnabled: bool
IsOutliningEnabled: bool IsOutliningEnabled: bool
UsePreviewTextHover: bool } UsePreviewTextHover: bool
UsePreviewDiagnostics: bool }
static member Default = static member Default =
{ IsBlockStructureEnabled = true { IsBlockStructureEnabled = true
IsOutliningEnabled = true IsOutliningEnabled = true
UsePreviewTextHover = false } UsePreviewTextHover = false
UsePreviewDiagnostics = false }
member this.AsLspOptions(): Options =
{ usePreviewTextHover = this.UsePreviewTextHover
usePreviewDiagnostics = this.UsePreviewDiagnostics }
[<CLIMutable>] [<CLIMutable>]
type FormattingOptions = type FormattingOptions =
@ -203,8 +208,7 @@ module internal OptionsUI =
async { async {
let lspService = this.GetService<LspService>() let lspService = this.GetService<LspService>()
let settings = this.GetService<EditorOptions>() let settings = this.GetService<EditorOptions>()
let options = let options = settings.Advanced.AsLspOptions()
{ Options.usePreviewTextHover = settings.Advanced.UsePreviewTextHover }
do! lspService.SetOptions options do! lspService.SetOptions options
} |> Async.Start } |> Async.Start

Просмотреть файл

@ -27,8 +27,12 @@
<!-- this group box should be disabled for branch `release/dev16.2`, enabled otherwise --> <!-- this group box should be disabled for branch `release/dev16.2`, enabled otherwise -->
<!-- <!--
<GroupBox Header="{x:Static local:Strings.Use_out_of_process_language_server}"> <GroupBox Header="{x:Static local:Strings.Use_out_of_process_language_server}">
<StackPanel>
<CheckBox x:Name="usePreviewTextHover" IsChecked="{Binding UsePreviewTextHover}" <CheckBox x:Name="usePreviewTextHover" IsChecked="{Binding UsePreviewTextHover}"
Content="{x:Static local:Strings.Text_hover}" /> Content="{x:Static local:Strings.Text_hover}" />
<CheckBox x:Name="usePreviewDiagnostics" IsChecked="{Binding UsePreviewDiagnostics}"
Content="{x:Static local:Strings.Diagnostics}" />
</StackPanel>
</GroupBox> </GroupBox>
--> -->
</StackPanel> </StackPanel>

Просмотреть файл

@ -150,6 +150,15 @@ namespace Microsoft.VisualStudio.FSharp.UIResources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Diagnostics.
/// </summary>
public static string Diagnostics {
get {
return ResourceManager.GetString("Diagnostics", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to D_ot underline. /// Looks up a localized string similar to D_ot underline.
/// </summary> /// </summary>

Просмотреть файл

@ -237,4 +237,7 @@
<data name="Text_hover" xml:space="preserve"> <data name="Text_hover" xml:space="preserve">
<value>Text hover</value> <value>Text hover</value>
</data> </data>
<data name="Diagnostics" xml:space="preserve">
<value>Diagnostics</value>
</data>
</root> </root>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">Seznamy dokončení</target> <target state="translated">Seznamy dokončení</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">Výkon</target> <target state="translated">Výkon</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">Vervollständigungslisten</target> <target state="translated">Vervollständigungslisten</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">Leistung</target> <target state="translated">Leistung</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">Listas de finalización</target> <target state="translated">Listas de finalización</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">Rendimiento</target> <target state="translated">Rendimiento</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">Listes de saisie semi-automatique</target> <target state="translated">Listes de saisie semi-automatique</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">Performances</target> <target state="translated">Performances</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">Elenchi di completamento</target> <target state="translated">Elenchi di completamento</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">Prestazioni</target> <target state="translated">Prestazioni</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">入力候補一覧</target> <target state="translated">入力候補一覧</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">パフォーマンス</target> <target state="translated">パフォーマンス</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">완성 목록</target> <target state="translated">완성 목록</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">성능</target> <target state="translated">성능</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">Listy uzupełniania</target> <target state="translated">Listy uzupełniania</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">Wydajność</target> <target state="translated">Wydajność</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">Listas de Conclusão</target> <target state="translated">Listas de Conclusão</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">Desempenho</target> <target state="translated">Desempenho</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">Списки завершения</target> <target state="translated">Списки завершения</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">Производительность</target> <target state="translated">Производительность</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">Tamamlama Listeleri</target> <target state="translated">Tamamlama Listeleri</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">Performans</target> <target state="translated">Performans</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">完成列表</target> <target state="translated">完成列表</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">性能</target> <target state="translated">性能</target>

Просмотреть файл

@ -42,6 +42,11 @@
<target state="translated">完成清單</target> <target state="translated">完成清單</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Diagnostics">
<source>Diagnostics</source>
<target state="new">Diagnostics</target>
<note />
</trans-unit>
<trans-unit id="Language_Service_Performance"> <trans-unit id="Language_Service_Performance">
<source>Performance</source> <source>Performance</source>
<target state="translated">效能</target> <target state="translated">效能</target>