Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/CSharpLanguageServer/CSharpLanguageServer.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<ChangelogFile>CHANGELOG.md</ChangelogFile>
<Nullable>enable</Nullable>
<MSBuildTreatWarningsAsErrors>true</MSBuildTreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/CSharpLanguageServer/Conversions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ module Location =
|> Option.bind (fun filePath -> if File.Exists filePath then Some filePath else None)
|> Option.map (fun filePath -> toLspLocation filePath (loc.GetLineSpan().Span))

//Console.Error.WriteLine("loc={0}; mapped={1}; source={2}", loc, mappedSourceLocation, sourceLocation)

mappedSourceLocation |> Option.orElse sourceLocation

| _ -> None
Expand Down
139 changes: 124 additions & 15 deletions src/CSharpLanguageServer/Handlers/Completion.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@ namespace CSharpLanguageServer.Handlers
open System
open System.Reflection

open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.Text
open Microsoft.Extensions.Caching.Memory
open Ionide.LanguageServerProtocol.Server
open Ionide.LanguageServerProtocol.Types
open Ionide.LanguageServerProtocol.JsonRpc
open Microsoft.Extensions.Logging

open CSharpLanguageServer.State
open CSharpLanguageServer.Util
open CSharpLanguageServer.Conversions
open CSharpLanguageServer.Logging
open CSharpLanguageServer.RoslynHelpers

[<RequireQualifiedAccess>]
module Completion =
let private _logger = Logging.getLoggerByName "Completion"
let private logger = Logging.getLoggerByName "Completion"

let private completionItemMemoryCache = new MemoryCache(new MemoryCacheOptions())

Expand Down Expand Up @@ -180,13 +184,99 @@ module Completion =
synopsis, documentationText
| _, _ -> None, None

let handle
let getCompletionsForRazorDocument
(p: CompletionParams)
(context: ServerRequestContext)
: Async<option<Microsoft.CodeAnalysis.Completion.CompletionList * Document>> =
async {
match! getRazorDocumentForUri context.Solution p.TextDocument.Uri with
| None -> return None
| Some(project, compilation, cshtmlTree) ->
let! ct = Async.CancellationToken
let! sourceText = cshtmlTree.GetTextAsync() |> Async.AwaitTask

let razorTextDocument =
context.Solution.Projects
|> Seq.collect (fun p -> p.AdditionalDocuments)
|> Seq.filter (fun d -> Uri(d.FilePath, UriKind.Absolute) = Uri p.TextDocument.Uri)
|> Seq.head

let! razorSourceText = razorTextDocument.GetTextAsync() |> Async.AwaitTask

let posInCshtml = Position.toRoslynPosition sourceText.Lines p.Position
//logger.LogInformation("posInCshtml={posInCshtml=}", posInCshtml)
let pos = p.Position

let root = cshtmlTree.GetRoot()

let mutable positionAndToken: (int * SyntaxToken) option = None

for t in root.DescendantTokens() do
let cshtmlSpan = cshtmlTree.GetMappedLineSpan(t.Span)

if
cshtmlSpan.StartLinePosition.Line = (int pos.Line)
&& cshtmlSpan.EndLinePosition.Line = (int pos.Line)
&& cshtmlSpan.StartLinePosition.Character <= (int pos.Character)
then
let tokenStartCharacterOffset =
(int pos.Character - cshtmlSpan.StartLinePosition.Character)

positionAndToken <- Some(t.Span.Start + tokenStartCharacterOffset, t)

match positionAndToken with
| None -> return None
| Some(position, tokenForPosition) ->

let newSourceText =
let cshtmlPosition = Position.toRoslynPosition razorSourceText.Lines p.Position
let charInCshtml: char = razorSourceText[cshtmlPosition - 1]

if charInCshtml = '.' && string tokenForPosition.Value <> "." then
// a hack to make <span>@Model.|</span> autocompletion to work:
// - force a dot if present on .cscshtml but missing on .cs
sourceText.WithChanges(new TextChange(new TextSpan(position - 1, 0), "."))
else
sourceText

let cshtmlPath = Uri.toPath p.TextDocument.Uri
let! doc = tryAddDocument logger context.Solution (cshtmlPath + ".cs") (newSourceText.ToString())

match doc with
| None -> return None
| Some doc ->
let completionService =
Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)
|> RoslynCompletionServiceWrapper

let completionOptions =
RoslynCompletionOptions.Default()
|> _.WithBool("ShowItemsFromUnimportedNamespaces", false)
|> _.WithBool("ShowNameSuggestions", false)

let completionTrigger = CompletionContext.toCompletionTrigger p.Context

let! roslynCompletions =
completionService.GetCompletionsAsync(
doc,
position,
completionOptions,
completionTrigger,
ct
)
|> Async.map Option.ofObj

return roslynCompletions |> Option.map (fun rcl -> rcl, doc)
}

let getCompletionsForCSharpDocument
(p: CompletionParams)
: Async<LspResult<U2<CompletionItem array, CompletionList> option>> =
(context: ServerRequestContext)
: Async<option<Microsoft.CodeAnalysis.Completion.CompletionList * Document>> =
async {
match context.GetDocument p.TextDocument.Uri with
| None -> return None |> LspResult.success
| None -> return None

| Some doc ->
let! ct = Async.CancellationToken
let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask
Expand Down Expand Up @@ -216,6 +306,23 @@ module Completion =
else
async.Return None

return roslynCompletions |> Option.map (fun rcl -> rcl, doc)
}

let handle
(context: ServerRequestContext)
(p: CompletionParams)
: Async<LspResult<U2<CompletionItem array, CompletionList> option>> =
async {
let getCompletions =
if p.TextDocument.Uri.EndsWith(".cshtml") then
getCompletionsForRazorDocument
else
getCompletionsForCSharpDocument

match! getCompletions p context with
| None -> return None |> LspResult.success
| Some(roslynCompletions, doc) ->
let toLspCompletionItemsWithCacheInfo (completions: Microsoft.CodeAnalysis.Completion.CompletionList) =
completions.ItemsList
|> Seq.map (fun item -> (item, Guid.NewGuid() |> string))
Expand All @@ -232,33 +339,35 @@ module Completion =
|> Array.ofSeq

let lspCompletionItemsWithCacheInfo =
roslynCompletions |> Option.map toLspCompletionItemsWithCacheInfo
roslynCompletions |> toLspCompletionItemsWithCacheInfo

// cache roslyn completion items
for (_, cacheItemId, roslynDoc, roslynItem) in
(lspCompletionItemsWithCacheInfo |> Option.defaultValue Array.empty) do
for (_, cacheItemId, roslynDoc, roslynItem) in lspCompletionItemsWithCacheInfo do
completionItemMemoryCacheSet cacheItemId roslynDoc roslynItem

let items =
lspCompletionItemsWithCacheInfo |> Array.map (fun (item, _, _, _) -> item)

return
lspCompletionItemsWithCacheInfo
|> Option.map (fun itemsWithCacheInfo ->
itemsWithCacheInfo |> Array.map (fun (item, _, _, _) -> item))
|> Option.map (fun items ->
{ IsIncomplete = true
Items = items
ItemDefaults = None })
|> Option.map U2.C2
{ IsIncomplete = true
Items = items
ItemDefaults = None }
|> U2.C2
|> Some
|> LspResult.success
}

let resolve (_context: ServerRequestContext) (item: CompletionItem) : AsyncLspResult<CompletionItem> = async {

let roslynDocAndItemMaybe =
item.Data
|> Option.bind deserialize<string option>
|> Option.bind completionItemMemoryCacheGet

match roslynDocAndItemMaybe with
| Some(doc, roslynCompletionItem) ->
logger.LogInformation("resolve, doc={0}, item={1}", doc, roslynCompletionItem)

let completionService =
Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)
|> nonNull "Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)"
Expand Down
28 changes: 10 additions & 18 deletions src/CSharpLanguageServer/Handlers/Diagnostic.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ open CSharpLanguageServer.Types

[<RequireQualifiedAccess>]
module Diagnostic =
let provider
(clientCapabilities: ClientCapabilities)
: U2<DiagnosticOptions, DiagnosticRegistrationOptions> option =
let provider (_cc: ClientCapabilities) : U2<DiagnosticOptions, DiagnosticRegistrationOptions> option =
let registrationOptions: DiagnosticRegistrationOptions =
{ DocumentSelector = Some defaultDocumentSelector
WorkDoneProgress = None
Expand All @@ -35,24 +33,18 @@ module Diagnostic =
Items = [||]
RelatedDocuments = None }

match context.GetDocument p.TextDocument.Uri with
| None -> return emptyReport |> U2.C1 |> LspResult.success
let! semanticModel = context.GetSemanticModel p.TextDocument.Uri

| Some doc ->
let! ct = Async.CancellationToken
let! semanticModelMaybe = doc.GetSemanticModelAsync(ct) |> Async.AwaitTask

match semanticModelMaybe |> Option.ofObj with
let diagnostics =
match semanticModel with
| None -> [||]
| Some semanticModel ->
let diagnostics =
semanticModel.GetDiagnostics()
|> Seq.map Diagnostic.fromRoslynDiagnostic
|> Seq.map fst
|> Array.ofSeq

return { emptyReport with Items = diagnostics } |> U2.C1 |> LspResult.success
semanticModel.GetDiagnostics()
|> Seq.map Diagnostic.fromRoslynDiagnostic
|> Seq.map fst
|> Array.ofSeq

| None -> return emptyReport |> U2.C1 |> LspResult.success
return { emptyReport with Items = diagnostics } |> U2.C1 |> LspResult.success
}

let private getWorkspaceDiagnosticReports
Expand Down
66 changes: 33 additions & 33 deletions src/CSharpLanguageServer/Handlers/DocumentHighlight.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,53 +12,53 @@ open CSharpLanguageServer.Conversions

[<RequireQualifiedAccess>]
module DocumentHighlight =
let provider (_: ClientCapabilities) : U2<bool, DocumentHighlightOptions> option = Some(U2.C1 true)
let provider (_cc: ClientCapabilities) : U2<bool, DocumentHighlightOptions> option = Some(U2.C1 true)

let private shouldHighlight (symbol: ISymbol) =
match symbol with
| :? INamespaceSymbol -> false
| _ -> true

let handle
(context: ServerRequestContext)
(p: DocumentHighlightParams)
: AsyncLspResult<DocumentHighlight[] option> =
async {
let! ct = Async.CancellationToken
let filePath = Uri.toPath p.TextDocument.Uri
// We only need to find references in the file (not the whole workspace), so we don't use
// context.FindSymbol & context.FindReferences here.
let private getHighlights symbol (project: Project) (docMaybe: Document option) (filePath: string) = async {
let! ct = Async.CancellationToken

// We only need to find references in the file (not the whole workspace), so we don't use
// context.FindSymbol & context.FindReferences here.
let getHighlights (symbol: ISymbol) (doc: Document) = async {
let docSet = ImmutableHashSet.Create(doc)
let docSet: ImmutableHashSet<Document> option =
docMaybe |> Option.map (fun doc -> ImmutableHashSet.Create(doc))

let! refs =
SymbolFinder.FindReferencesAsync(symbol, doc.Project.Solution, docSet, cancellationToken = ct)
|> Async.AwaitTask
let! refs =
SymbolFinder.FindReferencesAsync(symbol, project.Solution, docSet |> Option.toObj, cancellationToken = ct)
|> Async.AwaitTask

let! def =
SymbolFinder.FindSourceDefinitionAsync(symbol, doc.Project.Solution, cancellationToken = ct)
|> Async.AwaitTask
let! def =
SymbolFinder.FindSourceDefinitionAsync(symbol, project.Solution, cancellationToken = ct)
|> Async.AwaitTask

let locations =
refs
|> Seq.collect (fun r -> r.Locations)
|> Seq.map (fun rl -> rl.Location)
|> Seq.filter (fun l -> l.IsInSource && l.GetMappedLineSpan().Path = filePath)
|> Seq.append (def |> Option.ofObj |> Option.toList |> Seq.collect (fun sym -> sym.Locations))
let locations =
refs
|> Seq.collect (fun r -> r.Locations)
|> Seq.map (fun rl -> rl.Location)
|> Seq.filter (fun l -> l.IsInSource && l.GetMappedLineSpan().Path = filePath)
|> Seq.append (def |> Option.ofObj |> Option.toList |> Seq.collect (fun sym -> sym.Locations))

return
locations
|> Seq.choose Location.fromRoslynLocation
|> Seq.map (fun l ->
{ Range = l.Range
Kind = Some DocumentHighlightKind.Read })
}
return
locations
|> Seq.choose Location.fromRoslynLocation
|> Seq.map (fun l ->
{ Range = l.Range
Kind = Some DocumentHighlightKind.Read })
}

let handle
(context: ServerRequestContext)
(p: DocumentHighlightParams)
: AsyncLspResult<DocumentHighlight[] option> =
async {
match! context.FindSymbol' p.TextDocument.Uri p.Position with
| Some(symbol, _, Some doc) ->
| Some(symbol, project, docMaybe) ->
if shouldHighlight symbol then
let! highlights = getHighlights symbol doc
let! highlights = getHighlights symbol project docMaybe (Uri.toPath p.TextDocument.Uri)
return highlights |> Seq.toArray |> Some |> LspResult.success
else
return None |> LspResult.success
Expand Down
Loading