From 2885f9f80c82745da9c24b798f065b4b0c986dfb Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Wed, 20 Jan 2021 10:15:42 -0800 Subject: [PATCH] fix(exthost/#3009): Call '$release*' APIs to clean-up extension host cache (#3016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit __Issue:__ There is a memory leak in our completion items, signature help, and codelens via the extension host - may be responsible for the `SIGABRT` described in #3009 (as this can occur when the JS heap is out of memory). __Defect:__ The extension host adds a layer on top of the language server protocol, to serve as a cache for several language features - this helps performance when resolving items. However, this caching layer relies on the client to notify it when the items are no longer in use, so they can be cleaned up. Onivim was missing this `release` step. __Fix:__ Bake in the call to `release` in all the relevant subscriptions. This involves picking up the `cacheId` which wasn't wired up in some places (like codelens), setting up the `release*` API, and passing it back on conclusion of the subscription. To test this fix - I flipped the `Cache.enableDebugLogging` flag in the extension host and initiated several completions. Before this fix, it's obvious the caches were simply growing endlessly: ``` CompletionItem cache size — 1 CompletionItem cache size — 1 CompletionItem cache size — 2 CompletionItem cache size — 2 SignatureHelp cache size — 1 CompletionItem cache size — 3 CompletionItem cache size — 3 CompletionItem cache size — 4 CompletionItem cache size — 4 SignatureHelp cache size — 2 ... CompletionItem cache size — 15 CompletionItem cache size — 15 CompletionItem cache size — 16 CompletionItem cache size — 16 CompletionItem cache size — 17 CompletionItem cache size — 17 SignatureHelp cache size — 8 CompletionItem cache size — 18 CompletionItem cache size — 18 CompletionItem cache size — 19 CompletionItem cache size — 19 ``` After this fix, though, we can observe that the cache gets cleaned: ``` CompletionItem cache size — 1 CompletionItem cache size — 1 CompletionItem cache size — 0 CompletionItem cache size — 0 CompletionItem cache size — 1 CompletionItem cache size — 1 CompletionItem cache size — 0 CompletionItem cache size — 0 SignatureHelp cache size — 1 SignatureHelp cache size — 0 ... CompletionItem cache size — 0 CompletionItem cache size — 0 SignatureHelp cache size — 1 SignatureHelp cache size — 0 ``` Related #3009 Related #1058 --- CHANGES_CURRENT.md | 1 + src/Exthost/CodeLens.re | 23 ++- src/Exthost/Exthost.rei | 43 ++++- src/Exthost/Request.re | 27 ++- src/Exthost/SignatureHelp.re | 7 +- src/Exthost/SuggestResult.re | 7 +- src/Feature/LanguageSupport/CodeLens.re | 10 +- .../Feature_LanguageSupport.rei | 2 +- src/Service/Exthost/Service_Exthost.re | 179 +++++++++++++----- src/Service/Exthost/Service_Exthost.rei | 2 +- test/Exthost/LanguageFeaturesTest.re | 96 +++++----- 11 files changed, 280 insertions(+), 117 deletions(-) diff --git a/CHANGES_CURRENT.md b/CHANGES_CURRENT.md index 002edac072..602add5121 100644 --- a/CHANGES_CURRENT.md +++ b/CHANGES_CURRENT.md @@ -5,6 +5,7 @@ - #3008 - SCM: Fix index-out-of-bound exception when rendering diff markers - #3007 - Extensions: Show 'missing dependency' activation error to user - #3011 - Vim: Visual Block - Handle 'I' and 'A' in visual block mode (fixes #1633) +- #3016 - Extensions: Fix memory leak in extension host language features (fixes #3009) ### Performance diff --git a/src/Exthost/CodeLens.re b/src/Exthost/CodeLens.re index 3644f290e1..8e418f1e1f 100644 --- a/src/Exthost/CodeLens.re +++ b/src/Exthost/CodeLens.re @@ -1,5 +1,5 @@ [@deriving show] -type t = { +type lens = { cacheId: option(list(int)), range: OneBasedRange.t, command: option(Command.t), @@ -33,12 +33,29 @@ let decode = Decode.decode; let encode = Encode.encode; module List = { + [@deriving show] + type cacheId = int; + + [@deriving show] + type t = { + cacheId: option(cacheId), + lenses: list(lens), + }; + + let default = {cacheId: None, lenses: []}; + module Decode = { open Oni_Core.Json.Decode; - let simple = list(decode); + let simple = list(decode) |> map(lenses => {cacheId: None, lenses}); - let nested = field("lenses", simple); + let nested = + obj(({field, _}) => + { + lenses: field.required("lenses", list(decode)), + cacheId: field.optional("cacheId", int), + } + ); let decode = one_of([("nested", nested), ("simple", simple)]); }; diff --git a/src/Exthost/Exthost.rei b/src/Exthost/Exthost.rei index 8159fedc7a..a030c712a4 100644 --- a/src/Exthost/Exthost.rei +++ b/src/Exthost/Exthost.rei @@ -100,15 +100,26 @@ module OneBasedRange: { module CodeLens: { [@deriving show] - type t = { + type lens = { cacheId: option(list(int)), range: OneBasedRange.t, command: option(Command.t), }; - let decode: Json.decoder(t); + module List: { + [@deriving show] + type cacheId; - module List: {let decode: Json.decoder(list(t));}; + [@deriving show] + type t = { + cacheId: option(cacheId), + lenses: list(lens), + }; + + let default: t; + + let decode: Json.decoder(t); + }; }; module Location: { @@ -593,9 +604,11 @@ module SignatureHelp: { let decode: Json.decoder(t); }; + type cacheId; + module Response: { type t = { - id: int, + cacheId, signatures: list(Signature.t), activeSignature: int, activeParameter: int, @@ -606,10 +619,15 @@ module SignatureHelp: { }; module SuggestResult: { + [@deriving show]; + + type cacheId; + [@deriving show] type t = { completions: list(SuggestItem.t), isIncomplete: bool, + cacheId: option(cacheId), }; let empty: t; @@ -1758,11 +1776,14 @@ module Request: { module LanguageFeatures: { let provideCodeLenses: (~handle: int, ~resource: Uri.t, Client.t) => - Lwt.t(option(list(CodeLens.t))); + Lwt.t(option(CodeLens.List.t)); let resolveCodeLens: - (~handle: int, ~codeLens: CodeLens.t, Client.t) => - Lwt.t(option(CodeLens.t)); + (~handle: int, ~codeLens: CodeLens.lens, Client.t) => + Lwt.t(option(CodeLens.lens)); + + let releaseCodeLenses: + (~handle: int, ~cacheId: CodeLens.List.cacheId, Client.t) => unit; let provideCompletionItems: ( @@ -1778,6 +1799,9 @@ module Request: { (~handle: int, ~chainedCacheId: ChainedCacheId.t, Client.t) => Lwt.t(SuggestItem.t); + let releaseCompletionItems: + (~handle: int, ~cacheId: SuggestResult.cacheId, Client.t) => unit; + let provideDocumentHighlights: ( ~handle: int, @@ -1875,6 +1899,9 @@ module Request: { ) => Lwt.t(option(SignatureHelp.Response.t)); + let releaseSignatureHelp: + (~handle: int, ~cacheId: SignatureHelp.cacheId, Client.t) => unit; + let provideDocumentFormattingEdits: ( ~handle: int, @@ -1904,8 +1931,6 @@ module Request: { Client.t ) => Lwt.t(option(list(Edit.SingleEditOperation.t))); - - let releaseSignatureHelp: (~handle: int, ~id: int, Client.t) => unit; }; module SCM: { diff --git a/src/Exthost/Request.re b/src/Exthost/Request.re index d58f2d7208..1ba3a3e7c8 100644 --- a/src/Exthost/Request.re +++ b/src/Exthost/Request.re @@ -251,7 +251,7 @@ module LanguageFeatures = { ); }; - let resolveCodeLens = (~handle: int, ~codeLens: CodeLens.t, client) => { + let resolveCodeLens = (~handle: int, ~codeLens: CodeLens.lens, client) => { let decoder = Json.Decode.(nullable(CodeLens.decode)); Client.request( ~decoder, @@ -266,6 +266,17 @@ module LanguageFeatures = { client, ); }; + + let releaseCodeLenses = (~handle: int, ~cacheId, client) => { + Client.notify( + ~usesCancellationToken=false, + ~rpcName="ExtHostLanguageFeatures", + ~method="$releaseCodeLenses", + ~args=`List([`Int(handle), `Int(cacheId)]), + client, + ); + }; + let provideCompletionItems = ( ~handle: int, @@ -319,6 +330,16 @@ module LanguageFeatures = { ); }; + let releaseCompletionItems = (~handle: int, ~cacheId, client) => { + Client.notify( + ~usesCancellationToken=false, + ~rpcName="ExtHostLanguageFeatures", + ~method="$releaseCompletionItems", + ~args=`List([`Int(handle), `Int(cacheId)]), + client, + ); + }; + module Internal = { let provideDefinitionLink = (~handle, ~resource, ~position, method, client) => { @@ -448,7 +469,7 @@ module LanguageFeatures = { ); }; - let releaseSignatureHelp = (~handle, ~id, client) => + let releaseSignatureHelp = (~handle, ~cacheId, client) => Client.notify( ~rpcName="ExtHostLanguageFeatures", ~method="$releaseSignatureHelp", @@ -456,7 +477,7 @@ module LanguageFeatures = { `List( Json.Encode.[ handle |> encode_value(int), - id |> encode_value(int), + cacheId |> encode_value(int), ], ), client, diff --git a/src/Exthost/SignatureHelp.re b/src/Exthost/SignatureHelp.re index 5a6bedcbf1..256447a579 100644 --- a/src/Exthost/SignatureHelp.re +++ b/src/Exthost/SignatureHelp.re @@ -122,10 +122,13 @@ module Signature = { ); }; +[@deriving show] +type cacheId = int; + module Response = { [@deriving show] type t = { - id: int, + cacheId, signatures: list(Signature.t), activeSignature: int, activeParameter: int, @@ -135,7 +138,7 @@ module Response = { Json.Decode.( obj(({field, _}) => { - id: field.required("id", int), + cacheId: field.required("id", int), signatures: field.required("signatures", list(Signature.decode)), activeSignature: field.required("activeSignature", int), activeParameter: field.required("activeParameter", int), diff --git a/src/Exthost/SuggestResult.re b/src/Exthost/SuggestResult.re index de732cff85..f8ffc5043e 100644 --- a/src/Exthost/SuggestResult.re +++ b/src/Exthost/SuggestResult.re @@ -1,12 +1,16 @@ open Oni_Core; +[@deriving show] +type cacheId = int; + [@deriving show] type t = { completions: list(SuggestItem.t), isIncomplete: bool, + cacheId: option(cacheId), }; -let empty = {completions: [], isIncomplete: false}; +let empty = {completions: [], isIncomplete: false, cacheId: None}; module Dto = { let decode = { @@ -17,6 +21,7 @@ module Dto = { // https://github.com/onivim/vscode-exthost/blob/50bef147f7bbd250015361a4e3cad3305f65bc27/src/vs/workbench/api/common/extHost.protocol.ts#L1129 completions: field.required("b", list(SuggestItem.Dto.decode)), isIncomplete: field.withDefault("c", false, bool), + cacheId: field.optional("x", int), } ) ); diff --git a/src/Feature/LanguageSupport/CodeLens.re b/src/Feature/LanguageSupport/CodeLens.re index e461825b64..c0f6e19cb3 100644 --- a/src/Feature/LanguageSupport/CodeLens.re +++ b/src/Feature/LanguageSupport/CodeLens.re @@ -7,12 +7,12 @@ module Log = ( // MODEL -type codeLens = Exthost.CodeLens.t; +type codeLens = Exthost.CodeLens.lens; -let lineNumber = (lens: Exthost.CodeLens.t) => +let lineNumber = (lens: Exthost.CodeLens.lens) => Exthost.OneBasedRange.(lens.range.startLineNumber - 1); -let textFromExthost = (lens: Exthost.CodeLens.t) => { +let textFromExthost = (lens: Exthost.CodeLens.lens) => { Exthost.Command.( lens.command |> OptionEx.flatMap(command => command.label) @@ -21,7 +21,7 @@ let textFromExthost = (lens: Exthost.CodeLens.t) => { ); }; -let text = (lens: Exthost.CodeLens.t) => textFromExthost(lens); +let text = (lens: Exthost.CodeLens.lens) => textFromExthost(lens); type provider = { handle: int, @@ -55,7 +55,7 @@ type msg = bufferId: int, startLine: EditorCoreTypes.LineNumber.t, stopLine: EditorCoreTypes.LineNumber.t, - lenses: list(Exthost.CodeLens.t), + lenses: list(Exthost.CodeLens.lens), }); let register = (~handle: int, ~selector, ~maybeEventHandle, model) => { diff --git a/src/Feature/LanguageSupport/Feature_LanguageSupport.rei b/src/Feature/LanguageSupport/Feature_LanguageSupport.rei index 1d010929de..955adfa79a 100644 --- a/src/Feature/LanguageSupport/Feature_LanguageSupport.rei +++ b/src/Feature/LanguageSupport/Feature_LanguageSupport.rei @@ -32,7 +32,7 @@ module Msg: { }; module CodeLens: { - type t = Exthost.CodeLens.t; + type t = Exthost.CodeLens.lens; let lineNumber: t => int; let text: t => string; diff --git a/src/Service/Exthost/Service_Exthost.re b/src/Service/Exthost/Service_Exthost.re index b87a6ee55c..f02288f5bb 100644 --- a/src/Service/Exthost/Service_Exthost.re +++ b/src/Service/Exthost/Service_Exthost.re @@ -701,21 +701,36 @@ module Sub = { module CodeLensesSubscription = Isolinear.Sub.Make({ - type nonrec msg = result(list(Exthost.CodeLens.t), string); + type nonrec msg = result(list(Exthost.CodeLens.lens), string); type nonrec params = codeLensesParams; type state = { + maybeCacheId: ref(option(Exthost.CodeLens.List.cacheId)), isActive: ref(bool), - dispose: unit => unit, + disposeTimeout: unit => unit, }; let name = "Service_Exthost.CodeLensesSubscription"; let id = ({handle, buffer, startLine, eventTick, _}: params) => idForCodeLens(~handle, ~buffer, ~startLine, ~eventTick); + let cleanup = (~maybeCacheId, ~params) => { + maybeCacheId^ + |> Option.iter(cacheId => { + Exthost.Request.LanguageFeatures.releaseCodeLenses( + ~handle=params.handle, + ~cacheId, + params.client, + ); + + maybeCacheId := None; + }); + }; + let init = (~params, ~dispatch) => { let active = ref(true); - let dispose = + let maybeCacheId = ref(None); + let disposeTimeout = Revery.Tick.timeout( ~name, _ => { @@ -726,7 +741,7 @@ module Sub = { params.client, ); - let resolveLens = (codeLens: Exthost.CodeLens.t) => + let resolveLens = (codeLens: Exthost.CodeLens.lens) => if (! active^) { // If the subscription is over, don't both resolving... Lwt.return( @@ -747,36 +762,49 @@ module Sub = { let promise = Lwt.bind( init, - initResult => { - let unresolvedLenses = - initResult - |> Option.value(~default=[]) - |> List.filter((lens: Exthost.CodeLens.t) => { - Exthost.OneBasedRange.( - { - lens.range.startLineNumber >= params.startLine - && lens.range.startLineNumber <= params.stopLine; - } + (initResult: option(Exthost.CodeLens.List.t)) => { + initResult + |> Option.iter(({cacheId, _}: Exthost.CodeLens.List.t) => { + maybeCacheId := cacheId + }); + if (active^) { + let unresolvedLenses = + initResult + |> Option.map((lensResult: Exthost.CodeLens.List.t) => + lensResult.lenses ) - }); - - let resolvePromises = - unresolvedLenses |> List.map(resolveLens); - - let join: - ( - list(Exthost.CodeLens.t), - option(Exthost.CodeLens.t) - ) => - list(Exthost.CodeLens.t) = - (acc, cur) => { - switch (cur) { - | None => acc - | Some(lens) => [lens, ...acc] + |> Option.value(~default=[]) + |> List.filter((lens: Exthost.CodeLens.lens) => { + Exthost.OneBasedRange.( + { + lens.range.startLineNumber >= params.startLine + && lens.range.startLineNumber + <= params.stopLine; + } + ) + }); + + let resolvePromises = + unresolvedLenses |> List.map(resolveLens); + + let join: + ( + list(Exthost.CodeLens.lens), + option(Exthost.CodeLens.lens) + ) => + list(Exthost.CodeLens.lens) = + (acc, cur) => { + switch (cur) { + | None => acc + | Some(lens) => [lens, ...acc] + }; }; - }; - Utility.LwtEx.all(~initial=[], join, resolvePromises); + Utility.LwtEx.all(~initial=[], join, resolvePromises); + } else { + cleanup(~maybeCacheId, ~params); + Lwt.return([]); + }; }, ); @@ -790,14 +818,15 @@ module Sub = { }, Constants.lowPriorityDebounceTime, ); - {isActive: active, dispose}; + {isActive: active, disposeTimeout, maybeCacheId}; }; let update = (~params as _, ~state, ~dispatch as _) => state; - let dispose = (~params as _, ~state) => { + let dispose = (~params, ~state) => { + cleanup(~params, ~maybeCacheId=state.maybeCacheId); state.isActive := false; - state.dispose(); + state.disposeTimeout(); }; }); @@ -824,7 +853,10 @@ module Sub = { type nonrec msg = result(Exthost.SuggestResult.t, string); type nonrec params = completionParams; - type state = unit; + type state = { + isDisposed: ref(bool), + cacheId: ref(option(Exthost.SuggestResult.cacheId)), + }; let name = "Service_Exthost.CompletionSubscription"; let id = ({handle, buffer, position, _}: params) => @@ -835,7 +867,21 @@ module Sub = { "CompletionSubscription", ); + let cleanupCache = + (~params, ~cacheId: ref(option(Exthost.SuggestResult.cacheId))) => { + cacheId^ + |> Option.iter(cacheId => { + Exthost.Request.LanguageFeatures.releaseCompletionItems( + ~handle=params.handle, + ~cacheId, + params.client, + ) + }); + }; + let init = (~params, ~dispatch) => { + let isDisposed = ref(false); + let cacheId = ref(None); let promise = Exthost.Request.LanguageFeatures.provideCompletionItems( ~handle=params.handle, @@ -845,21 +891,30 @@ module Sub = { params.client, ); - Lwt.on_success(promise, suggestResult => - dispatch(Ok(suggestResult)) + Lwt.on_success( + promise, + suggestResult => { + cacheId := suggestResult.cacheId; + if (isDisposed^) { + cleanupCache(~params, ~cacheId); + } else { + dispatch(Ok(suggestResult)); + }; + }, ); Lwt.on_failure(promise, exn => dispatch(Error(Printexc.to_string(exn))) ); - (); + {isDisposed, cacheId}; }; let update = (~params as _, ~state, ~dispatch as _) => state; - let dispose = (~params as _, ~state as _) => { - (); + let dispose = (~params, ~state) => { + state.isDisposed := true; + cleanupCache(~params, ~cacheId=state.cacheId); }; }); let completionItems = @@ -926,13 +981,29 @@ module Sub = { result(option(Exthost.SignatureHelp.Response.t), string); type nonrec params = signatureHelpParams; - type state = unit; + type state = { + isActive: ref(bool), + cacheId: ref(option(Exthost.SignatureHelp.cacheId)), + }; + + let cleanup = (~params, ~cacheId) => { + cacheId^ + |> Option.iter(cacheId => { + Exthost.Request.LanguageFeatures.releaseSignatureHelp( + ~handle=params.handle, + ~cacheId, + params.client, + ) + }); + }; let name = "Service_Exthost.SignatureHelpSubscription"; let id = ({handle, buffer, position, _}: params) => idFromBufferPosition(~handle, ~buffer, ~position, "SignatureHelp"); let init = (~params, ~dispatch) => { + let isActive = ref(true); + let cacheId = ref(None); let promise = Exthost.Request.LanguageFeatures.provideSignatureHelp( ~handle=params.handle, @@ -942,21 +1013,37 @@ module Sub = { params.client, ); - Lwt.on_success(promise, suggestResult => - dispatch(Ok(suggestResult)) + Lwt.on_success( + promise, + (maybeSigHelpResult: option(Exthost.SignatureHelp.Response.t)) => { + maybeSigHelpResult + |> Option.iter(suggestResult => { + cacheId := + Some( + Exthost.SignatureHelp.Response.(suggestResult.cacheId), + ) + }); + + if (isActive^) { + dispatch(Ok(maybeSigHelpResult)); + } else { + cleanup(~params, ~cacheId); + }; + }, ); Lwt.on_failure(promise, exn => dispatch(Error(Printexc.to_string(exn))) ); - (); + {isActive, cacheId}; }; let update = (~params as _, ~state, ~dispatch as _) => state; - let dispose = (~params as _, ~state as _) => { - (); + let dispose = (~params, ~state) => { + state.isActive := false; + cleanup(~params, ~cacheId=state.cacheId); }; }); let signatureHelp = (~handle, ~context, ~buffer, ~position, ~toMsg, client) => { diff --git a/src/Service/Exthost/Service_Exthost.rei b/src/Service/Exthost/Service_Exthost.rei index 95e640fed7..f2c8d51c5b 100644 --- a/src/Service/Exthost/Service_Exthost.rei +++ b/src/Service/Exthost/Service_Exthost.rei @@ -135,7 +135,7 @@ module Sub: { ~buffer: Oni_Core.Buffer.t, ~startLine: EditorCoreTypes.LineNumber.t, ~stopLine: EditorCoreTypes.LineNumber.t, - ~toMsg: result(list(Exthost.CodeLens.t), string) => 'a, + ~toMsg: result(list(Exthost.CodeLens.lens), string) => 'a, Exthost.Client.t ) => Isolinear.Sub.t('a); diff --git a/test/Exthost/LanguageFeaturesTest.re b/test/Exthost/LanguageFeaturesTest.re index db73617a2e..dded74a5ab 100644 --- a/test/Exthost/LanguageFeaturesTest.re +++ b/test/Exthost/LanguageFeaturesTest.re @@ -82,40 +82,44 @@ describe("LanguageFeaturesTest", ({describe, _}) => { |> Test.withClientRequest( ~name="Get code lenses items", ~validate= - (codeLenses: option(list(Exthost.CodeLens.t))) => { - expect.equal( - codeLenses, - Some([ - CodeLens.{ - cacheId: Some([1, 0]), - range: range1, - command: - Some( - Exthost.Command.{ - label: - Some( - Exthost.Label.ofString("codelens: command1"), - ), - id: Some("codelens.command1"), - }, - ), - }, - CodeLens.{ - cacheId: Some([1, 1]), - range: range2, - command: - Some( - Exthost.Command.{ - label: - Some( - Exthost.Label.ofString("codelens: command2"), - ), - id: Some("codelens.command2"), - }, - ), - }, - ]), - ); + (codeLenses: option(Exthost.CodeLens.List.t)) => { + switch (codeLenses) { + | None => expect.equal(false, true) + | Some({lenses, _}) => + expect.equal( + lenses, + [ + CodeLens.{ + cacheId: Some([1, 0]), + range: range1, + command: + Some( + Exthost.Command.{ + label: + Some( + Exthost.Label.ofString("codelens: command1"), + ), + id: Some("codelens.command1"), + }, + ), + }, + CodeLens.{ + cacheId: Some([1, 1]), + range: range2, + command: + Some( + Exthost.Command.{ + label: + Some( + Exthost.Label.ofString("codelens: command2"), + ), + id: Some("codelens.command2"), + }, + ), + }, + ], + ) + }; true; }, getCodeLenses, @@ -160,7 +164,7 @@ describe("LanguageFeaturesTest", ({describe, _}) => { ~name="Get completion items", ~validate= (suggestResult: Exthost.SuggestResult.t) => { - let {completions, isIncomplete}: Exthost.SuggestResult.t = suggestResult; + let {completions, isIncomplete, _}: Exthost.SuggestResult.t = suggestResult; expect.int(List.length(completions)).toBe(2); expect.bool(isIncomplete).toBe(false); @@ -544,14 +548,15 @@ describe("LanguageFeaturesTest", ({describe, _}) => { |> Test.withClientRequest( ~name="Get signature help", ~validate= - (signatureHelp: option(Exthost.SignatureHelp.Response.t)) => { + (maybeSignatureHelp: option(Exthost.SignatureHelp.Response.t)) => { open Exthost.SignatureHelp; - expect.equal( - signatureHelp, - Some({ - id: 1, - signatures: [ + switch (maybeSignatureHelp) { + | None => failwith("No signature help returned") + | Some({signatures, activeSignature, activeParameter, _}) => + expect.equal( + signatures, + [ Signature.{ label: "signature 1", documentation: @@ -573,11 +578,10 @@ describe("LanguageFeaturesTest", ({describe, _}) => { ], }, ], - activeSignature: 0, - activeParameter: 0, - }), - ); - + ); + expect.equal(activeSignature, 0); + expect.equal(activeParameter, 0); + }; true; }, getSignatureHelp,