diff --git a/lib/lsp/lsp_base.rb b/lib/lsp/lsp_base.rb index dfe26213..7cff601e 100644 --- a/lib/lsp/lsp_base.rb +++ b/lib/lsp/lsp_base.rb @@ -16,7 +16,7 @@ def to_h item_value = send(name) if item_value.is_a?(Array) # Convert the items in the array .to_h - item_value = item_value.map { |item| item.to_h } + item_value = item_value.map { |item| item.respond_to?(:to_h) ? item.to_h : item } elsif !item_value.nil? && item_value.respond_to?(:to_h) item_value = item_value.to_h end diff --git a/lib/puppet-languageserver/manifest/signature_provider.rb b/lib/puppet-languageserver/manifest/signature_provider.rb new file mode 100644 index 00000000..92e44a0e --- /dev/null +++ b/lib/puppet-languageserver/manifest/signature_provider.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +module PuppetLanguageServer + module Manifest + module SignatureProvider + def self.signature_help(content, line_num, char_num, options = {}) + options = { + :tasks_mode => false + }.merge(options) + + result = PuppetLanguageServer::PuppetParserHelper.object_under_cursor(content, line_num, char_num, + :multiple_attempts => false, + :tasks_mode => options[:tasks_mode], + :remove_trigger_char => false) + response = LSP::SignatureHelp.new.from_h!('signatures' => [], 'activeSignature' => nil, 'activeParameter' => nil) + # We are in the root of the document so no signatures here. + return response if result.nil? + + item = result[:model] + path = result[:path] + locator = result[:locator] + + function_ast_object = nil + # Try and find the acutal function object within the AST + if item.class.to_s == 'Puppet::Pops::Model::CallNamedFunctionExpression' + function_ast_object = item + else + # Try and find the function with the AST tree + distance_up_ast = -1 + function_ast_object = path[distance_up_ast] + while !function_ast_object.nil? && function_ast_object.class.to_s != 'Puppet::Pops::Model::CallNamedFunctionExpression' + distance_up_ast -= 1 + function_ast_object = path[distance_up_ast] + end + raise "Unable to find suitable parent object for object of type #{item.class}" if function_ast_object.nil? + end + + function_name = function_ast_object.functor_expr.value + raise 'Could not determine the function name' if function_name.nil? + + # Convert line and char nums (base 0) to an absolute offset within the document + # result.line_offsets contains an array of the offsets on a per line basis e.g. + # [0, 14, 34, 36] means line number 2 starts at absolute offset 34 + # Once we know the line offset, we can simply add on the char_num to get the absolute offset + if function_ast_object.respond_to?(:locator) + line_offset = function_ast_object.locator.line_index[line_num] + else + line_offset = locator.line_index[line_num] + end + + abs_offset = line_offset + char_num + # We need to use offsets here in case functions span lines + param_number = param_number_from_ast(abs_offset, function_ast_object, locator) + raise 'Cursor is not within the function expression' if param_number.nil? + + func_info = PuppetLanguageServer::PuppetHelper.function(function_name) + raise "Function #{function_name} does not exist" if func_info.nil? + + func_info.signatures.each do |sig| + lsp_sig = LSP::SignatureInformation.new.from_h!( + 'label' => sig.key, + 'documentation' => sig.doc, + 'parameters' => [] + ) + + sig.parameters.each do |param| + lsp_sig.parameters << LSP::ParameterInformation.new.from_h!( + 'label' => param.signature_key_offset.nil? || param.signature_key_length.nil? ? param.name : [param.signature_key_offset, param.signature_key_offset + param.signature_key_length], + 'documentation' => param.doc + ) + end + + response.signatures << lsp_sig + end + + # Now figure out the first signature which could have the same or more than the number of arguments in the function call + func_arg_count = function_ast_object.arguments.count + signature_number = func_info.signatures.find_index { |sig| sig.parameters.count >= func_arg_count } + + # If we still don't know the signature number then assume it's the first one + signature_number = 0 if signature_number.nil? && func_info.signatures.count > 0 + + response.activeSignature = signature_number unless signature_number.nil? + response.activeParameter = param_number + + response + end + + def self.param_number_from_ast(char_offset, function_ast_object, locator) + # Figuring out which parameter the cursor is in is a little tricky. For example: + # + # func_two_param( $param1 , $param2 ) + # |------------------------------------| Locator for the entire function + # |-------------| Locator for the function expression + # |-----| Locator for function.arguments[0] child (contains it's children) + # |-----| Locator for function.arguments[1] child (contains it's children) + # + # Importantly, whitespace isn't included in the AST + + function_offset = function_ast_object.offset + function_length = function_ast_object.length + functor_expr_offset = function_ast_object.functor_expr.offset + functor_expr_length = function_ast_object.functor_expr.length + + # Shouldn't happen but, safety first! + return nil if function_offset.nil? || function_length.nil? || functor_expr_offset.nil? || functor_expr_length.nil? + + # Is the cursor on the function name or the opening bracket? then we are not in any parameters + return nil if char_offset <= functor_expr_offset + functor_expr_length + # Is the cursor on or beyond the closing bracket? then we are not in any parameters + return nil if char_offset >= function_offset + function_length + # Does the function even have arguments? then the cursor HAS to be in the first parameter + return 0 if function_ast_object.arguments.count.zero? + # Is the cursor within any of the function argument locations? if so, return the parameter number we're in + param_number = function_ast_object.arguments.find_index { |arg| char_offset >= arg.offset && char_offset <= arg.offset + arg.length } + return param_number unless param_number.nil? + + # So now we know the char_offset exists outside any of the locators. Check the extremities + # Is it before the first argument? if so then the parameter number has to be zero + return 0 if char_offset < function_ast_object.arguments[0].offset + + last_arg_index = function_ast_object.arguments.count - 1 + last_offset = function_ast_object.arguments[last_arg_index].offset + function_ast_object.arguments[last_arg_index].length + if char_offset > last_offset + # We know that the cursor is after the last argument, but before the closing bracket. + # Therefore the before_index is the last argument in the list, and the after_index is one more than that + before_index = last_arg_index + after_index = last_arg_index + 1 + before_offset = function_ast_object.arguments[before_index].offset + before_length = function_ast_object.arguments[before_index].length + # But we need to get the text after the last argument, to the closing bracket + locator_args = [ + last_offset, + function_offset + function_length - last_offset - 1 # Find the difference from the entire function length and the last offset and subtract 1 for the closing bracket as we don't need it + ] + else + # Now we now the char_offset exists between two existing locators. Determine the location by finding which argument is AFTER the cursor + after_index = function_ast_object.arguments.find_index { |arg| char_offset < arg.offset } + return nil if after_index.nil? || after_index.zero? # This should never happen but, you never know. + before_index = after_index - 1 + + # Now we know between which arguments (before_index and after_index) the char_offset lies + before_length = function_ast_object.arguments[before_index].length + before_offset = function_ast_object.arguments[before_index].offset + after_offset = function_ast_object.arguments[after_index].offset + + # Determine the text between the two arguments + locator_args = [ + before_offset + before_length, # From the end the begin_index + after_offset - before_offset - before_length # to the start of the after_index + ] + end + between_text = function_ast_object.respond_to?(:locator) ? function_ast_object.locator.extract_text(*locator_args) : locator.extract_text(*locator_args) + + # Now we have the text between the two arguments, determine where the comma is + comma_index = between_text.index(',') + # If there is no comma then it has to be the before_index i.e. + # ..., $param ) <--- the space before the closing bracket would have a between_text of ' ' + return before_index if comma_index.nil? + + # If the char_offset is after the comma then choose the after_index otherwise choose the before_index + char_offset > before_offset + before_length + comma_index ? after_index : before_index + end + private_class_method :param_number_from_ast + end + end +end diff --git a/lib/puppet-languageserver/message_router.rb b/lib/puppet-languageserver/message_router.rb index ffe36fbc..7aeb2882 100644 --- a/lib/puppet-languageserver/message_router.rb +++ b/lib/puppet-languageserver/message_router.rb @@ -179,6 +179,23 @@ def receive_request(request) request.reply_result(nil) end + when 'textDocument/signatureHelp' + file_uri = request.params['textDocument']['uri'] + line_num = request.params['position']['line'] + char_num = request.params['position']['character'] + content = documents.document(file_uri) + begin + case documents.document_type(file_uri) + when :manifest + request.reply_result(PuppetLanguageServer::Manifest::SignatureProvider.signature_help(content, line_num, char_num, :tasks_mode => PuppetLanguageServer::DocumentStore.module_plan_file?(file_uri))) + else + raise "Unable to provide signatures on #{file_uri}" + end + rescue StandardError => e + PuppetLanguageServer.log_message(:error, "(textDocument/signatureHelp) #{e}") + request.reply_result(nil) + end + when 'workspace/symbol' begin result = [] diff --git a/lib/puppet-languageserver/providers.rb b/lib/puppet-languageserver/providers.rb index 1fd99e1b..874b08d9 100644 --- a/lib/puppet-languageserver/providers.rb +++ b/lib/puppet-languageserver/providers.rb @@ -5,6 +5,7 @@ manifest/completion_provider manifest/definition_provider manifest/document_symbol_provider + manifest/signature_provider manifest/validation_provider manifest/hover_provider puppetfile/r10k/module/base diff --git a/lib/puppet-languageserver/puppet_parser_helper.rb b/lib/puppet-languageserver/puppet_parser_helper.rb index a31e076f..e0003c60 100644 --- a/lib/puppet-languageserver/puppet_parser_helper.rb +++ b/lib/puppet-languageserver/puppet_parser_helper.rb @@ -60,9 +60,10 @@ def self.get_line_at(content, line_offsets, line_num) def self.object_under_cursor(content, line_num, char_num, options) options = { - :multiple_attempts => false, - :disallowed_classes => [], - :tasks_mode => false + :multiple_attempts => false, + :disallowed_classes => [], + :tasks_mode => false, + :remove_trigger_char => true }.merge(options) # Use Puppet to generate the AST @@ -130,13 +131,14 @@ def self.object_under_cursor(content, line_num, char_num, options) # [0, 14, 34, 36] means line number 2 starts at absolute offset 34 # Once we know the line offset, we can simply add on the char_num to get the absolute offset # If during paring we modified the source we may need to change the cursor location - begin + if result.respond_to?(:line_offsets) line_offset = result.line_offsets[line_num] - rescue StandardError + else line_offset = result['locator'].line_index[line_num] end - # Typically we're completing after something was typed, so go back one char - abs_offset = line_offset + char_num + move_offset - 1 + abs_offset = line_offset + char_num + move_offset + # Typically we're completing after something was typed, so go back one char by default + abs_offset -= 1 if options[:remove_trigger_char] # Enumerate the AST looking for items that span the line/char we want. # Once we have all valid items, sort them by the smallest span. Typically the smallest span @@ -144,7 +146,8 @@ def self.object_under_cursor(content, line_num, char_num, options) # # TODO: Should probably walk the AST and only look for the deepest child, but integer sorting # is so much easier and faster. - model_path_struct = Struct.new(:model, :path) + model_path_locator_struct = Struct.new(:model, :path, :locator) + valid_models = [] if result.model.respond_to? :eAllContents valid_models = result.model.eAllContents.select do |item| @@ -156,7 +159,7 @@ def self.object_under_cursor(content, line_num, char_num, options) path = [] result.model._pcore_all_contents(path) do |item| if check_for_valid_item(item, abs_offset, options[:disallowed_classes]) # rubocop:disable Style/IfUnlessModifier Nicer to read like this - valid_models.push(model_path_struct.new(item, path.dup)) + valid_models.push(model_path_locator_struct.new(item, path.dup)) end end @@ -164,13 +167,14 @@ def self.object_under_cursor(content, line_num, char_num, options) end # nil means the root of the document return nil if valid_models.empty? - item = valid_models[0] + response = valid_models[0] - if item.respond_to? :eAllContents # rubocop:disable Style/IfUnlessModifier Nicer to read like this - item = model_path_struct.new(item, construct_path(item)) + if response.respond_to? :eAllContents # rubocop:disable Style/IfUnlessModifier Nicer to read like this + response = model_path_locator_struct.new(response, construct_path(response)) end - item + response.locator = result.model.locator + response end def self.construct_path(item) @@ -187,5 +191,20 @@ def self.construct_path(item) def self.check_for_valid_item(item, abs_offset, disallowed_classes) item.respond_to?(:offset) && !item.offset.nil? && !item.length.nil? && abs_offset >= item.offset && abs_offset <= item.offset + item.length && !disallowed_classes.include?(item.class) end + + # This method is only required during development or debugging. Visualising the AST tree can be difficult + # so this method just prints it to the console. + # def self.recurse_showast(item, abs_offset, disallowed_classes, depth = 0) + # output = " " * depth + # output += check_for_valid_item(item, abs_offset, disallowed_classes) ? 'X ' : ' ' + # output += "#{item.class.to_s} (#{item.object_id})" + # if item.respond_to?(:offset) + # output += " (Off-#{item.offset}:#{item.offset + item.length} Pos-#{item.line}:#{item.pos} Len-#{item.length}) ~#{item.locator.extract_text(item.offset, item.length)}~" + # end + # puts output + # item._pcore_contents do |child| + # recurse_showast(child, abs_offset, disallowed_classes, depth + 1) + # end + # end end end diff --git a/lib/puppet-languageserver/server_capabilities.rb b/lib/puppet-languageserver/server_capabilities.rb index d418d168..3ada953d 100644 --- a/lib/puppet-languageserver/server_capabilities.rb +++ b/lib/puppet-languageserver/server_capabilities.rb @@ -14,7 +14,10 @@ def self.capabilities }, 'definitionProvider' => true, 'documentSymbolProvider' => true, - 'workspaceSymbolProvider' => true + 'workspaceSymbolProvider' => true, + 'signatureHelpProvider' => { + 'triggerCharacters' => ['(', ','] + } } end diff --git a/spec/languageserver/integration/puppet-languageserver/manifest/signature_provider_spec.rb b/spec/languageserver/integration/puppet-languageserver/manifest/signature_provider_spec.rb new file mode 100644 index 00000000..f0094347 --- /dev/null +++ b/spec/languageserver/integration/puppet-languageserver/manifest/signature_provider_spec.rb @@ -0,0 +1,287 @@ +require 'spec_helper' + +def pretty_string(value) + value.nil? ? 'nil' : value.to_s +end + +describe 'signature_provider' do + let(:subject) { PuppetLanguageServer::Manifest::SignatureProvider } + + before(:all) do + wait_for_puppet_loading + end + + before(:each) do + # Prepopulate the Object Cache with workspace objects + # Functions + list = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new + list << PuppetLanguageServer::Sidecar::Protocol::PuppetFunction.new.from_h!({ + 'key' => 'func_two_param', + 'doc' => 'documentation for func_two_param', + 'function_version' => 4, + 'signatures' => [ + { + 'key' => 'func_two_param(Any $p1)', + 'doc' => 'first func_two_param signature', + 'return_types' => ['Any'], + 'parameters' => [ + { 'name' => 'p1', 'types' => ['Any'], 'doc' => 'p1 documentation' }, + ] + }, + { + 'key' => 'func_two_param(Any $p1, Any $p2)', + 'doc' => 'second func_two_param signature', + 'return_types' => ['Any'], + 'parameters' => [ + { 'name' => 'p1', 'types' => ['Any'], 'doc' => 'p1 documentation' }, + { 'name' => 'p2', 'types' => ['Any'], 'doc' => 'p2 documentation' }, + ] + }, + ] + }) + list << PuppetLanguageServer::Sidecar::Protocol::PuppetFunction.new.from_h!({ + 'key' => 'func_three_param', + 'doc' => 'documentation for func_three_param', + 'function_version' => 4, + 'signatures' => [ + { + 'key' => 'func_three_param(Any $p1, Any $p2, Any $p3)', + 'doc' => 'first func_three_param signature', + 'return_types' => ['Any'], + 'parameters' => [ + { 'name' => 'p1', 'types' => ['Any'], 'doc' => 'p1 documentation' }, + { 'name' => 'p2', 'types' => ['Any'], 'doc' => 'p2 documentation' }, + { 'name' => 'p3', 'types' => ['Any'], 'doc' => 'p3 documentation' } + ] + }, + ] + }) + PuppetLanguageServer::PuppetHelper.cache.import_sidecar_list!(list, :function, :workspace) + end + + after(:each) do + # Clear out the Object Cache of workspace objects + PuppetLanguageServer::PuppetHelper.cache.import_sidecar_list!([], :function, :workspace) + end + + describe '#signature_help' do + context "Given a simple valid manifest with a function with three parameters" do + let(:content) { "#something\nfunc_three_param( $param1 , $param2 ,\'param3\') \n#somethingelse" } + + [ + { :name => 'after the start bracket', :character => 17, :activeParameter => 0 }, + { :name => 'within the first parameter', :character => 20, :activeParameter => 0 }, + { :name => 'before first comma', :character => 26, :activeParameter => 0 }, + { :name => 'after first comma', :character => 28, :activeParameter => 1 }, + { :name => 'within the second parameter', :character => 34, :activeParameter => 1 }, + { :name => 'after second parameter', :character => 37, :activeParameter => 1 }, + { :name => 'after second comma', :character => 40, :activeParameter => 2 }, + { :name => 'within the third parameter', :character => 44, :activeParameter => 2 }, + { :name => 'before the end bracket', :character => 48, :activeParameter => 2 }, + ].each do |testcase| + describe "When the cursor is #{testcase[:name]}" do + it "should use the first signature" do + result = subject.signature_help(content, 1, testcase[:character], { :tasks_mode => true}) + + expect(result.activeSignature).to eq(0) + end + + it "should have an active parameter of #{pretty_string(testcase[:activeParameter])}" do + result = subject.signature_help(content, 1, testcase[:character], { :tasks_mode => true}) + + expect(result.activeParameter).to eq(testcase[:activeParameter]) + end + end + end + end + + context "Given a simple valid manifest with a function with two parameters" do + context "When supplying only one parameter" do + let(:content) { "#something\nfunc_two_param( $param1 ) \n#somethingelse" } + + [ + { :name => 'middle of first parameter', :character => 20, :activeParameter => 0, :activeSignature => 0 }, + ].each do |testcase| + describe "When the cursor is in #{testcase[:name]}" do + it "should have an active signature of #{pretty_string(testcase[:activeSignature])}" do + result = subject.signature_help(content, 1, testcase[:character], { :tasks_mode => true}) + + expect(result.activeSignature).to eq(testcase[:activeSignature]) + end + + it "should have an active parameter of #{pretty_string(testcase[:activeParameter])}" do + result = subject.signature_help(content, 1, testcase[:character], { :tasks_mode => true}) + + expect(result.activeParameter).to eq(testcase[:activeParameter]) + end + end + end + end + + context "When supplying two parameters" do + let(:content) { "#something\nfunc_two_param( $param1 , $param2 ) \n#somethingelse" } + + [ + { :name => 'within the first parameter', :character => 20, :activeParameter => 0, :activeSignature => 1 }, + { :name => 'within the second parameter', :character => 29, :activeParameter => 1, :activeSignature => 1 }, + ].each do |testcase| + describe "When the cursor is in #{testcase[:name]}" do + it "should have an active signature of #{pretty_string(testcase[:activeSignature])}" do + result = subject.signature_help(content, 1, testcase[:character], { :tasks_mode => true}) + + expect(result.activeSignature).to eq(testcase[:activeSignature]) + end + + it "should have an active parameter of #{pretty_string(testcase[:activeParameter])}" do + result = subject.signature_help(content, 1, testcase[:character], { :tasks_mode => true}) + + expect(result.activeParameter).to eq(testcase[:activeParameter]) + end + end + end + end + end + + context "Given a manifest part way through editing at the end" do + let(:content) { 'func_three_param( $param1 , ) ' } + + [ + { :name => 'after the start bracket', :character => 17, :activeParameter => 0 }, + { :name => 'within the first parameter', :character => 20, :activeParameter => 0 }, + { :name => 'before first comma', :character => 26, :activeParameter => 0 }, + { :name => 'after first comma', :character => 28, :activeParameter => 1 }, + { :name => 'before the end bracket', :character => 30, :activeParameter => 1 }, + ].each do |testcase| + describe "When the cursor is #{testcase[:name]}" do + it "should use the first signature" do + result = subject.signature_help(content, 0, testcase[:character], { :tasks_mode => true}) + + expect(result.activeSignature).to eq(0) + end + + it "should have an active parameter of #{pretty_string(testcase[:activeParameter])}" do + result = subject.signature_help(content, 0, testcase[:character], { :tasks_mode => true}) + + expect(result.activeParameter).to eq(testcase[:activeParameter]) + end + end + end + end + + context "Given a manifest with nested function calls, across multiple lines" do + let(:content) { "func_three_param( $param1 ,\n func_two_param( $nest1 , $nest2) ,\n $param3\n) " } + + describe "Within the first function" do + [ + { :name => 'after the start bracket', :line => 0, :character => 17, :activeParameter => 0, :activeSignature => 0 }, + { :name => 'within the first parameter', :line => 0, :character => 22, :activeParameter => 0, :activeSignature => 0 }, + { :name => 'before first comma', :line => 0, :character => 26, :activeParameter => 0, :activeSignature => 0 }, + { :name => 'after first comma', :line => 0, :character => 28, :activeParameter => 1, :activeSignature => 0 }, + { :name => 'before second comma', :line => 1, :character => 35, :activeParameter => 1, :activeSignature => 0 }, + { :name => 'after second comma', :line => 1, :character => 36, :activeParameter => 2, :activeSignature => 0 }, + { :name => 'within the third parameter', :line => 2, :character => 4, :activeParameter => 2, :activeSignature => 0 }, + { :name => 'before the end bracket', :line => 3, :character => 0, :activeParameter => 2, :activeSignature => 0 }, + ].each do |testcase| + describe "When the cursor is #{testcase[:name]}" do + it "should return signatures for the first function" do + result = subject.signature_help(content, testcase[:line], testcase[:character], { :tasks_mode => true}) + + expect(result.signatures.count).to be > 0 + expect(result.signatures[0].documentation).to match(/func_three_param/) + end + + it "should have an active signature of #{pretty_string(testcase[:activeSignature])}" do + result = subject.signature_help(content, testcase[:line], testcase[:character], { :tasks_mode => true}) + + expect(result.activeSignature).to eq(testcase[:activeSignature]) + end + + it "should have an active parameter of #{pretty_string(testcase[:activeParameter])}" do + result = subject.signature_help(content, testcase[:line], testcase[:character], { :tasks_mode => true}) + + expect(result.activeParameter).to eq(testcase[:activeParameter]) + end + end + end + end + + describe "Within the second function" do + [ + { :name => 'after the start bracket', :line => 1, :character => 17, :activeParameter => 0, :activeSignature => 1 }, + { :name => 'within the first parameter', :line => 1, :character => 19, :activeParameter => 0, :activeSignature => 1 }, + { :name => 'before first comma', :line => 1, :character => 24, :activeParameter => 0, :activeSignature => 1 }, + { :name => 'after first comma', :line => 1, :character => 26, :activeParameter => 1, :activeSignature => 1 }, + { :name => 'within the second parameter', :line => 1, :character => 30, :activeParameter => 1, :activeSignature => 1 }, + { :name => 'before the end bracket', :line => 1, :character => 33, :activeParameter => 1, :activeSignature => 1 }, + ].each do |testcase| + describe "When the cursor is #{testcase[:name]}" do + it "should return signatures for the second function" do + result = subject.signature_help(content, testcase[:line], testcase[:character], { :tasks_mode => true}) + + expect(result.signatures.count).to be > 0 + expect(result.signatures[0].documentation).to match(/func_two_param/) + end + + it "should have an active signature of #{pretty_string(testcase[:activeSignature])}" do + result = subject.signature_help(content, testcase[:line], testcase[:character], { :tasks_mode => true}) + + expect(result.activeSignature).to eq(testcase[:activeSignature]) + end + + it "should have an active parameter of #{pretty_string(testcase[:activeParameter])}" do + result = subject.signature_help(content, testcase[:line], testcase[:character], { :tasks_mode => true}) + + expect(result.activeParameter).to eq(testcase[:activeParameter]) + end + end + end + end + end + + context "Given an invalid manifest" do + [ + { :name => 'an empty first parameter', :manifest => 'func_three_param( , $param1 ) ' }, + { :name => 'a missing middle parameter', :manifest => 'func_three_param($param1 , , ) ' }, + ].each do |testcase| + describe "When the manifest has #{testcase[:name]}" do + it "should raise a runtime error" do + expect {subject.signature_help(testcase[:manifest], 0, 18, { :tasks_mode => true}) }.to raise_error(RuntimeError) + end + end + end + end + + context 'Given an invalid character location' do + describe 'With a single function in the manifest' do + let(:content) { "#something\nfunc_three_param( $param1 , $param2 ,\'param3\') \n#somethingelse" } + + [ + { :name => 'on function name', :character => 3 }, + { :name => 'before the start bracket', :character => 16 }, + { :name => 'after the end bracket', :character => 49 }, + ].each do |testcase| + describe "When the cursor is #{testcase[:name]}" do + it 'should raise a runtime error' do + expect {subject.signature_help(content, 1, testcase[:character], { :tasks_mode => true}) }.to raise_error(RuntimeError) + end + end + end + end + + describe 'With a nested function in the manifest' do + let(:content) { 'func_three_param( $param1 , func_two_param( $nest1 , $nest2) , $param3)' } + + [ + { :name => 'on outer function name', :character => 8 }, + { :name => 'on nested function name', :character => 36 }, + ].each do |testcase| + describe "When the cursor is #{testcase[:name]}" do + it 'should raise a runtime error' do + expect {subject.signature_help(content, 0, testcase[:character], { :tasks_mode => true}) }.to raise_error(RuntimeError) + end + end + end + end + end + end +end