@@ -3,20 +3,25 @@ namespace CSharpLanguageServer.Handlers
33open System
44open System.Reflection
55
6+ open Microsoft.CodeAnalysis
7+ open Microsoft.CodeAnalysis .Text
68open Microsoft.Extensions .Caching .Memory
79open Ionide.LanguageServerProtocol .Server
810open Ionide.LanguageServerProtocol .Types
911open Ionide.LanguageServerProtocol .JsonRpc
12+ open Microsoft.Extensions .Logging
1013
1114open CSharpLanguageServer.State
1215open CSharpLanguageServer.Util
1316open CSharpLanguageServer.Roslyn .Conversions
17+ open CSharpLanguageServer.Roslyn .Solution
1418open CSharpLanguageServer.Logging
1519open CSharpLanguageServer.Lsp .Workspace
1620
21+
1722[<RequireQualifiedAccess>]
1823module Completion =
19- let private _logger = Logging.getLoggerByName " Completion"
24+ let private logger = Logging.getLoggerByName " Completion"
2025
2126 let private completionItemMemoryCache = new MemoryCache( new MemoryCacheOptions())
2227
@@ -181,16 +186,118 @@ module Completion =
181186 synopsis, documentationText
182187 | _, _ -> None, None
183188
184- let handle
189+ let codeActionContextToCompletionTrigger ( context : CompletionContext option ) =
190+ context
191+ |> Option.bind ( fun ctx ->
192+ match ctx.TriggerKind with
193+ | CompletionTriggerKind.Invoked
194+ | CompletionTriggerKind.TriggerForIncompleteCompletions ->
195+ Some Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke
196+ | CompletionTriggerKind.TriggerCharacter ->
197+ ctx.TriggerCharacter
198+ |> Option.map Seq.head
199+ |> Option.map Microsoft.CodeAnalysis.Completion.CompletionTrigger.CreateInsertionTrigger
200+ | _ -> None)
201+ |> Option.defaultValue Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke
202+
203+ let getCompletionsForRazorDocument
204+ ( p : CompletionParams )
185205 ( context : ServerRequestContext )
206+ : Async < option < Microsoft.CodeAnalysis.Completion.CompletionList * Document >> =
207+ async {
208+ let wf = context.Workspace.SingletonFolder
209+
210+ match ! solutionGetRazorDocumentForUri wf.Solution.Value p.TextDocument.Uri with
211+ | None -> return None
212+ | Some( project, compilation, cshtmlTree) ->
213+ let! ct = Async.CancellationToken
214+ let! sourceText = cshtmlTree.GetTextAsync() |> Async.AwaitTask
215+
216+ let razorTextDocument =
217+ wf.Solution.Value
218+ |> _. Projects
219+ |> Seq.collect ( fun p -> p.AdditionalDocuments)
220+ |> Seq.filter ( fun d -> Uri( d.FilePath, UriKind.Absolute) = Uri p.TextDocument.Uri)
221+ |> Seq.head
222+
223+ let! razorSourceText = razorTextDocument.GetTextAsync() |> Async.AwaitTask
224+
225+ let posInCshtml = Position.toRoslynPosition sourceText.Lines p.Position
226+ //logger.LogInformation("posInCshtml={posInCshtml=}", posInCshtml)
227+ let pos = p.Position
228+
229+ let root = cshtmlTree.GetRoot()
230+
231+ let mutable positionAndToken : ( int * SyntaxToken ) option = None
232+
233+ for t in root.DescendantTokens() do
234+ let cshtmlSpan = cshtmlTree.GetMappedLineSpan( t.Span)
235+
236+ if
237+ cshtmlSpan.StartLinePosition.Line = ( int pos.Line)
238+ && cshtmlSpan.EndLinePosition.Line = ( int pos.Line)
239+ && cshtmlSpan.StartLinePosition.Character <= ( int pos.Character)
240+ then
241+ let tokenStartCharacterOffset =
242+ ( int pos.Character - cshtmlSpan.StartLinePosition.Character)
243+
244+ positionAndToken <- Some( t.Span.Start + tokenStartCharacterOffset, t)
245+
246+ match positionAndToken with
247+ | None -> return None
248+ | Some( position, tokenForPosition) ->
249+
250+ let newSourceText =
251+ let cshtmlPosition = Position.toRoslynPosition razorSourceText.Lines p.Position
252+ let charInCshtml : char = razorSourceText[ cshtmlPosition - 1 ]
253+
254+ if charInCshtml = '.' && string tokenForPosition.Value <> " ." then
255+ // a hack to make <span>@Model.|</span> autocompletion to work:
256+ // - force a dot if present on .cscshtml but missing on .cs
257+ sourceText.WithChanges( new TextChange( new TextSpan( position - 1 , 0 ), " ." ))
258+ else
259+ sourceText
260+
261+ let cshtmlPath = Uri.toPath p.TextDocument.Uri
262+ let! doc = solutionTryAddDocument ( cshtmlPath + " .cs" ) ( newSourceText.ToString()) wf.Solution.Value
263+
264+ match doc with
265+ | None -> return None
266+ | Some doc ->
267+ let completionService =
268+ Microsoft.CodeAnalysis.Completion.CompletionService.GetService( doc)
269+ |> RoslynCompletionServiceWrapper
270+
271+ let completionOptions =
272+ RoslynCompletionOptions.Default()
273+ |> _. WithBool( " ShowItemsFromUnimportedNamespaces" , false )
274+ |> _. WithBool( " ShowNameSuggestions" , false )
275+
276+ let completionTrigger = p.Context |> codeActionContextToCompletionTrigger
277+
278+ let! roslynCompletions =
279+ completionService.GetCompletionsAsync(
280+ doc,
281+ position,
282+ completionOptions,
283+ completionTrigger,
284+ ct
285+ )
286+ |> Async.map Option.ofObj
287+
288+ return roslynCompletions |> Option.map ( fun rcl -> rcl, doc)
289+ }
290+
291+ let getCompletionsForCSharpDocument
186292 ( p : CompletionParams )
187- : Async < LspResult < U2 < CompletionItem array , CompletionList > option >> =
293+ ( context : ServerRequestContext )
294+ : Async < option < Microsoft.CodeAnalysis.Completion.CompletionList * Document >> =
188295 async {
189296 let wf , docForUri =
190297 p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument
191298
192299 match docForUri with
193- | None -> return None |> LspResult.success
300+ | None -> return None
194301 | Some doc ->
195302 let! ct = Async.CancellationToken
196303 let! sourceText = doc.GetTextAsync( ct) |> Async.AwaitTask
@@ -206,19 +313,7 @@ module Completion =
206313 |> _. WithBool( " ShowItemsFromUnimportedNamespaces" , false )
207314 |> _. WithBool( " ShowNameSuggestions" , false )
208315
209- let completionTrigger =
210- p.Context
211- |> Option.bind ( fun ctx ->
212- match ctx.TriggerKind with
213- | CompletionTriggerKind.Invoked
214- | CompletionTriggerKind.TriggerForIncompleteCompletions ->
215- Some Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke
216- | CompletionTriggerKind.TriggerCharacter ->
217- ctx.TriggerCharacter
218- |> Option.map Seq.head
219- |> Option.map Microsoft.CodeAnalysis.Completion.CompletionTrigger.CreateInsertionTrigger
220- | _ -> None)
221- |> Option.defaultValue Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke
316+ let completionTrigger = p.Context |> codeActionContextToCompletionTrigger
222317
223318 let shouldTriggerCompletion =
224319 p.Context
@@ -232,6 +327,23 @@ module Completion =
232327 else
233328 async.Return None
234329
330+ return roslynCompletions |> Option.map ( fun rcl -> rcl, doc)
331+ }
332+
333+ let handle
334+ ( context : ServerRequestContext )
335+ ( p : CompletionParams )
336+ : Async < LspResult < U2 < CompletionItem array , CompletionList > option >> =
337+ async {
338+ let getCompletions =
339+ if p.TextDocument.Uri.EndsWith( " .cshtml" ) then
340+ getCompletionsForRazorDocument
341+ else
342+ getCompletionsForCSharpDocument
343+
344+ match ! getCompletions p context with
345+ | None -> return None |> LspResult.success
346+ | Some( roslynCompletions, doc) ->
235347 let toLspCompletionItemsWithCacheInfo ( completions : Microsoft.CodeAnalysis.Completion.CompletionList ) =
236348 completions.ItemsList
237349 |> Seq.map ( fun item -> ( item, Guid.NewGuid() |> string))
@@ -248,22 +360,21 @@ module Completion =
248360 |> Array.ofSeq
249361
250362 let lspCompletionItemsWithCacheInfo =
251- roslynCompletions |> Option.map toLspCompletionItemsWithCacheInfo
363+ roslynCompletions |> toLspCompletionItemsWithCacheInfo
252364
253365 // cache roslyn completion items
254- for (_, cacheItemId, roslynDoc, roslynItem) in
255- ( lspCompletionItemsWithCacheInfo |> Option.defaultValue Array.empty) do
366+ for (_, cacheItemId, roslynDoc, roslynItem) in lspCompletionItemsWithCacheInfo do
256367 completionItemMemoryCacheSet cacheItemId roslynDoc roslynItem
257368
369+ let items =
370+ lspCompletionItemsWithCacheInfo |> Array.map ( fun ( item , _ , _ , _ ) -> item)
371+
258372 return
259- lspCompletionItemsWithCacheInfo
260- |> Option.map ( fun itemsWithCacheInfo ->
261- itemsWithCacheInfo |> Array.map ( fun ( item , _ , _ , _ ) -> item))
262- |> Option.map ( fun items ->
263- { IsIncomplete = true
264- Items = items
265- ItemDefaults = None })
266- |> Option.map U2.C2
373+ { IsIncomplete = true
374+ Items = items
375+ ItemDefaults = None }
376+ |> U2.C2
377+ |> Some
267378 |> LspResult.success
268379 }
269380
0 commit comments