@@ -3,19 +3,24 @@ 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
1519
20+
1621[<RequireQualifiedAccess>]
1722module Completion =
18- let private _logger = Logging.getLoggerByName " Completion"
23+ let private logger = Logging.getLoggerByName " Completion"
1924
2025 let private completionItemMemoryCache = new MemoryCache( new MemoryCacheOptions())
2126
@@ -180,13 +185,113 @@ module Completion =
180185 synopsis, documentationText
181186 | _, _ -> None, None
182187
183- let handle
188+ let codeActionContextToCompletionTrigger ( context : CompletionContext option ) =
189+ context
190+ |> Option.bind ( fun ctx ->
191+ match ctx.TriggerKind with
192+ | CompletionTriggerKind.Invoked
193+ | CompletionTriggerKind.TriggerForIncompleteCompletions ->
194+ Some Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke
195+ | CompletionTriggerKind.TriggerCharacter ->
196+ ctx.TriggerCharacter
197+ |> Option.map Seq.head
198+ |> Option.map Microsoft.CodeAnalysis.Completion.CompletionTrigger.CreateInsertionTrigger
199+ | _ -> None)
200+ |> Option.defaultValue Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke
201+
202+ let getCompletionsForRazorDocument
203+ ( p : CompletionParams )
184204 ( context : ServerRequestContext )
205+ : Async < option < Microsoft.CodeAnalysis.Completion.CompletionList * Document >> =
206+ async {
207+ match ! solutionGetRazorDocumentForUri context.Solution p.TextDocument.Uri with
208+ | None -> return None
209+ | Some( project, compilation, cshtmlTree) ->
210+ let! ct = Async.CancellationToken
211+ let! sourceText = cshtmlTree.GetTextAsync() |> Async.AwaitTask
212+
213+ let razorTextDocument =
214+ context.Solution.Projects
215+ |> Seq.collect ( fun p -> p.AdditionalDocuments)
216+ |> Seq.filter ( fun d -> Uri( d.FilePath, UriKind.Absolute) = Uri p.TextDocument.Uri)
217+ |> Seq.head
218+
219+ let! razorSourceText = razorTextDocument.GetTextAsync() |> Async.AwaitTask
220+
221+ let posInCshtml = Position.toRoslynPosition sourceText.Lines p.Position
222+ //logger.LogInformation("posInCshtml={posInCshtml=}", posInCshtml)
223+ let pos = p.Position
224+
225+ let root = cshtmlTree.GetRoot()
226+
227+ let mutable positionAndToken : ( int * SyntaxToken ) option = None
228+
229+ for t in root.DescendantTokens() do
230+ let cshtmlSpan = cshtmlTree.GetMappedLineSpan( t.Span)
231+
232+ if
233+ cshtmlSpan.StartLinePosition.Line = ( int pos.Line)
234+ && cshtmlSpan.EndLinePosition.Line = ( int pos.Line)
235+ && cshtmlSpan.StartLinePosition.Character <= ( int pos.Character)
236+ then
237+ let tokenStartCharacterOffset =
238+ ( int pos.Character - cshtmlSpan.StartLinePosition.Character)
239+
240+ positionAndToken <- Some( t.Span.Start + tokenStartCharacterOffset, t)
241+
242+ match positionAndToken with
243+ | None -> return None
244+ | Some( position, tokenForPosition) ->
245+
246+ let newSourceText =
247+ let cshtmlPosition = Position.toRoslynPosition razorSourceText.Lines p.Position
248+ let charInCshtml : char = razorSourceText[ cshtmlPosition - 1 ]
249+
250+ if charInCshtml = '.' && string tokenForPosition.Value <> " ." then
251+ // a hack to make <span>@Model.|</span> autocompletion to work:
252+ // - force a dot if present on .cscshtml but missing on .cs
253+ sourceText.WithChanges( new TextChange( new TextSpan( position - 1 , 0 ), " ." ))
254+ else
255+ sourceText
256+
257+ let cshtmlPath = Uri.toPath p.TextDocument.Uri
258+ let! doc = solutionTryAddDocument ( cshtmlPath + " .cs" ) ( newSourceText.ToString()) context.Solution
259+
260+ match doc with
261+ | None -> return None
262+ | Some doc ->
263+ let completionService =
264+ Microsoft.CodeAnalysis.Completion.CompletionService.GetService( doc)
265+ |> RoslynCompletionServiceWrapper
266+
267+ let completionOptions =
268+ RoslynCompletionOptions.Default()
269+ |> _. WithBool( " ShowItemsFromUnimportedNamespaces" , false )
270+ |> _. WithBool( " ShowNameSuggestions" , false )
271+
272+ let completionTrigger = p.Context |> codeActionContextToCompletionTrigger
273+
274+ let! roslynCompletions =
275+ completionService.GetCompletionsAsync(
276+ doc,
277+ position,
278+ completionOptions,
279+ completionTrigger,
280+ ct
281+ )
282+ |> Async.map Option.ofObj
283+
284+ return roslynCompletions |> Option.map ( fun rcl -> rcl, doc)
285+ }
286+
287+ let getCompletionsForCSharpDocument
185288 ( p : CompletionParams )
186- : Async < LspResult < U2 < CompletionItem array , CompletionList > option >> =
289+ ( context : ServerRequestContext )
290+ : Async < option < Microsoft.CodeAnalysis.Completion.CompletionList * Document >> =
187291 async {
188292 match context.GetDocument p.TextDocument.Uri with
189- | None -> return None |> LspResult.success
293+ | None -> return None
294+
190295 | Some doc ->
191296 let! ct = Async.CancellationToken
192297 let! sourceText = doc.GetTextAsync( ct) |> Async.AwaitTask
@@ -202,19 +307,7 @@ module Completion =
202307 |> _. WithBool( " ShowItemsFromUnimportedNamespaces" , false )
203308 |> _. WithBool( " ShowNameSuggestions" , false )
204309
205- let completionTrigger =
206- p.Context
207- |> Option.bind ( fun ctx ->
208- match ctx.TriggerKind with
209- | CompletionTriggerKind.Invoked
210- | CompletionTriggerKind.TriggerForIncompleteCompletions ->
211- Some Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke
212- | CompletionTriggerKind.TriggerCharacter ->
213- ctx.TriggerCharacter
214- |> Option.map Seq.head
215- |> Option.map Microsoft.CodeAnalysis.Completion.CompletionTrigger.CreateInsertionTrigger
216- | _ -> None)
217- |> Option.defaultValue Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke
310+ let completionTrigger = p.Context |> codeActionContextToCompletionTrigger
218311
219312 let shouldTriggerCompletion =
220313 p.Context
@@ -228,6 +321,23 @@ module Completion =
228321 else
229322 async.Return None
230323
324+ return roslynCompletions |> Option.map ( fun rcl -> rcl, doc)
325+ }
326+
327+ let handle
328+ ( context : ServerRequestContext )
329+ ( p : CompletionParams )
330+ : Async < LspResult < U2 < CompletionItem array , CompletionList > option >> =
331+ async {
332+ let getCompletions =
333+ if p.TextDocument.Uri.EndsWith( " .cshtml" ) then
334+ getCompletionsForRazorDocument
335+ else
336+ getCompletionsForCSharpDocument
337+
338+ match ! getCompletions p context with
339+ | None -> return None |> LspResult.success
340+ | Some( roslynCompletions, doc) ->
231341 let toLspCompletionItemsWithCacheInfo ( completions : Microsoft.CodeAnalysis.Completion.CompletionList ) =
232342 completions.ItemsList
233343 |> Seq.map ( fun item -> ( item, Guid.NewGuid() |> string))
@@ -244,22 +354,21 @@ module Completion =
244354 |> Array.ofSeq
245355
246356 let lspCompletionItemsWithCacheInfo =
247- roslynCompletions |> Option.map toLspCompletionItemsWithCacheInfo
357+ roslynCompletions |> toLspCompletionItemsWithCacheInfo
248358
249359 // cache roslyn completion items
250- for (_, cacheItemId, roslynDoc, roslynItem) in
251- ( lspCompletionItemsWithCacheInfo |> Option.defaultValue Array.empty) do
360+ for (_, cacheItemId, roslynDoc, roslynItem) in lspCompletionItemsWithCacheInfo do
252361 completionItemMemoryCacheSet cacheItemId roslynDoc roslynItem
253362
363+ let items =
364+ lspCompletionItemsWithCacheInfo |> Array.map ( fun ( item , _ , _ , _ ) -> item)
365+
254366 return
255- lspCompletionItemsWithCacheInfo
256- |> Option.map ( fun itemsWithCacheInfo ->
257- itemsWithCacheInfo |> Array.map ( fun ( item , _ , _ , _ ) -> item))
258- |> Option.map ( fun items ->
259- { IsIncomplete = true
260- Items = items
261- ItemDefaults = None })
262- |> Option.map U2.C2
367+ { IsIncomplete = true
368+ Items = items
369+ ItemDefaults = None }
370+ |> U2.C2
371+ |> Some
263372 |> LspResult.success
264373 }
265374
0 commit comments