From d3e8726675b31e670add4f7b6a5b3b98c1834ff2 Mon Sep 17 00:00:00 2001 From: jpogran Date: Fri, 28 Sep 2018 09:44:02 -0400 Subject: [PATCH 1/2] (GH-56) Add DocumentSymbolProvider capability This commit adds the DocumentSymbolProvider and supporting code to the LanguageServer. It uses a procedural implementation to parse the output of the Puppet AST to generate symbols that VSCode understands. This lights up OutlineView, Breadcrumbs, and symbol search using the command pallete. --- lib/languageserver/constants.rb | 7 + lib/languageserver/document_symbol.rb | 79 +++++++++++ lib/languageserver/languageserver.rb | 2 +- lib/puppet-languageserver/message_router.rb | 15 ++ .../server_capabilities.rb | 3 +- .../puppet_parser_helper_spec.rb | 134 ++++++++++++++++++ 6 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 lib/languageserver/document_symbol.rb create mode 100644 spec/languageserver/unit/puppet-languageserver/puppet_parser_helper_spec.rb diff --git a/lib/languageserver/constants.rb b/lib/languageserver/constants.rb index fa77275c..72732cb7 100644 --- a/lib/languageserver/constants.rb +++ b/lib/languageserver/constants.rb @@ -40,6 +40,13 @@ module LanguageServer SYMBOLKIND_NUMBER = 16 SYMBOLKIND_BOOLEAN = 17 SYMBOLKIND_ARRAY = 18 + SYMBOLKIND_OBJECT = 19 + SYMBOLKIND_KEY = 20 + SYMBOLKIND_NULL = 21 + SYMBOLKIND_ENUMMEMBER = 22 + SYMBOLKIND_STRUCT = 23 + SYMBOLKIND_EVENT = 24 + SYMBOLKIND_OPERATOR = 25 TEXTDOCUMENTSYNCKIND_NONE = 0 TEXTDOCUMENTSYNCKIND_FULL = 1 diff --git a/lib/languageserver/document_symbol.rb b/lib/languageserver/document_symbol.rb new file mode 100644 index 00000000..069dadb9 --- /dev/null +++ b/lib/languageserver/document_symbol.rb @@ -0,0 +1,79 @@ +module LanguageServer +# /** +# * Represents programming constructs like variables, classes, interfaces etc. that appear in a document. Document symbols can be +# * hierarchical and they have two ranges: one that encloses its definition and one that points to its most interesting range, +# * e.g. the range of an identifier. +# */ +# export class DocumentSymbol { +# /** +# * The name of this symbol. +# */ +# name: string; +# /** +# * More detail for this symbol, e.g the signature of a function. +# */ +# detail?: string; +# /** +# * The kind of this symbol. +# */ +# kind: SymbolKind; +# /** +# * Indicates if this symbol is deprecated. +# */ +# deprecated?: boolean; +# /** +# * The range enclosing this symbol not including leading/trailing whitespace but everything else +# * like comments. This information is typically used to determine if the clients cursor is +# * inside the symbol to reveal in the symbol in the UI. +# */ +# range: Range; +# /** +# * The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. +# * Must be contained by the `range`. +# */ +# selectionRange: Range; +# /** +# * Children of this symbol, e.g. properties of a class. +# */ +# children?: DocumentSymbol[]; +# } + module DocumentSymbol + def self.create(options) + result = {} + raise('name is a required field for DocumentSymbol') if options['name'].nil? + raise('kind is a required field for DocumentSymbol') if options['kind'].nil? + raise('range is a required field for DocumentSymbol') if options['range'].nil? + raise('selectionRange is a required field for DocumentSymbol') if options['selectionRange'].nil? + + result['name'] = options['name'] + result['kind'] = options['kind'] + result['detail'] = options['detail'] unless options['detail'].nil? + result['deprecated'] = options['deprecated'] unless options['deprecated'].nil? + result['children'] = options['children'] unless options['children'].nil? + + result['range'] = { + 'start' => { + 'line' => options['range'][0], + 'character' => options['range'][1], + }, + 'end' => { + 'line' => options['range'][2], + 'character' => options['range'][3], + } + } + + result['selectionRange'] = { + 'start' => { + 'line' => options['selectionRange'][0], + 'character' => options['selectionRange'][1], + }, + 'end' => { + 'line' => options['selectionRange'][2], + 'character' => options['selectionRange'][3], + } + } + + result + end + end +end diff --git a/lib/languageserver/languageserver.rb b/lib/languageserver/languageserver.rb index 22db2209..cdcb84e1 100644 --- a/lib/languageserver/languageserver.rb +++ b/lib/languageserver/languageserver.rb @@ -1,4 +1,4 @@ -%w[constants diagnostic completion_list completion_item hover location puppet_version puppet_compilation puppet_fix_diagnostic_errors].each do |lib| +%w[constants diagnostic completion_list completion_item document_symbol hover location puppet_version puppet_compilation puppet_fix_diagnostic_errors].each do |lib| begin require "languageserver/#{lib}" rescue LoadError diff --git a/lib/puppet-languageserver/message_router.rb b/lib/puppet-languageserver/message_router.rb index 3f0fb7dd..e267c12b 100644 --- a/lib/puppet-languageserver/message_router.rb +++ b/lib/puppet-languageserver/message_router.rb @@ -149,6 +149,21 @@ def receive_request(request) request.reply_result(nil) end + when 'textDocument/documentSymbol' + file_uri = request.params['textDocument']['uri'] + content = documents.document(file_uri) + begin + case documents.document_type(file_uri) + when :manifest + r = PuppetLanguageServer::PuppetParserHelper.extract_document_symbols(content) + request.reply_result(r) + else + raise "Unable to provide definition on #{file_uri}" + end + rescue StandardError => exception + PuppetLanguageServer.log_message(:error, "(textDocument/documentSymbol) #{exception}") + request.reply_result(nil) + end else PuppetLanguageServer.log_message(:error, "Unknown RPC method #{request.rpc_method}") end diff --git a/lib/puppet-languageserver/server_capabilities.rb b/lib/puppet-languageserver/server_capabilities.rb index 3258c3b3..1989ee8e 100644 --- a/lib/puppet-languageserver/server_capabilities.rb +++ b/lib/puppet-languageserver/server_capabilities.rb @@ -10,7 +10,8 @@ def self.capabilities 'resolveProvider' => true, 'triggerCharacters' => ['>', '$', '[', '='] }, - 'definitionProvider' => true + 'definitionProvider' => true, + 'documentSymbolProvider' => true, } end end diff --git a/spec/languageserver/unit/puppet-languageserver/puppet_parser_helper_spec.rb b/spec/languageserver/unit/puppet-languageserver/puppet_parser_helper_spec.rb new file mode 100644 index 00000000..d0ab8eb7 --- /dev/null +++ b/spec/languageserver/unit/puppet-languageserver/puppet_parser_helper_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +RSpec::Matchers.define :be_document_symbol do |name, kind, start_line, start_char, end_line, end_char| + match do |actual| + actual['name'] == name && + actual['kind'] == kind && + actual['range']['start']['line'] == start_line && + actual['range']['start']['character'] == start_char && + actual['range']['end']['line'] == end_line && + actual['range']['end']['character'] == end_char + end + + failure_message do |actual| + "expected that symbol called '#{actual['name']}' of type '#{actual['kind']}' located at " + + "(#{actual['range']['start']['line']}, #{actual['range']['start']['character']}, " + + "#{actual['range']['end']['line']}, #{actual['range']['end']['character']}) would be " + + "a document symbol called '#{name}', of type '#{kind}' located at (#{start_line}, #{start_char}, #{end_line}, #{end_char})" + end + + description do + "be a document symbol called '#{name}' of type #{kind} located at #{start_line}, #{start_char}, #{end_line}, #{end_char}" + end +end + +describe 'PuppetLanguageServer::PuppetParserHelper' do + let(:subject) { PuppetLanguageServer::PuppetParserHelper } + + describe '#extract_document_symbols' do + it 'should find a class in the document root' do + content = <<-EOT + class foo { + } + EOT + result = subject.extract_document_symbols(content) + + expect(result.count).to eq(1) + expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 6, 0, 9) + end + + it 'should find a resource in the document root' do + pending('Not supported yet') + content = <<-EOT + user { 'alice': + } + EOT + result = subject.extract_document_symbols(content) + + expect(result.count).to eq(1) + expect(result[0]).to be_document_symbol('alice', LanguageServer::SYMBOLKIND_MODULE, 0, 8, 0, 13) + end + + it 'should find a single line class in the document root' do + pending('Not supported yet') + content = <<-EOT + class foo(String $var1 = 'value1', String $var2 = 'value2') { + } + EOT + result = subject.extract_document_symbols(content) + + expect(result.count).to eq(1) + expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 6, 0, 9) + expect(result[0]['children'].count).to eq(2) + # TODO: Check that the children are the properties var1 and var2 + end + + it 'should find a multi line class in the document root' do + pending('Not supported yet') + content = <<-EOT + class foo( + String $var1 = 'value1', + String $var2 = 'value2', + ) { + } + EOT + result = subject.extract_document_symbols(content) + + expect(result.count).to eq(1) + expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 6, 0, 9) + expect(result[0]['children'].count).to eq(2) + # TODO: Check that the children are the properties var1 and var2 + end + + it 'should find a simple resource in a class' do + content = <<-EOT + class foo { + user { 'alice': + } + } + EOT + result = subject.extract_document_symbols(content) + + expect(result.count).to eq(1) + expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 6, 0, 9) + expect(result[0]['children'].count).to eq(1) + expect(result[0]['children'][0]).to be_document_symbol('alice', LanguageServer::SYMBOLKIND_METHOD, 1, 8, 1, 13) + end + end + + # TODO: The method get_selection_range_array doesn't exist on the module + # describe 'should find line' do + # it 'should find line of first class' do + # content = "class wakka(\n $param1 = ''\n) {\n user { 'james':\n ensure => 'present'\n }\n}\n\nclass foo{\n}\n" + # line_offsets = [0, 13, 28, 32, 50, 74, 78, 80, 81, 92, 94] + # item_offset = 0; + # item_name = 'wakka' + + # test = PuppetLanguageServer::PuppetParserHelper.get_selection_range_array( + # content, + # line_offsets, + # item_offset, + # item_name + # ) + + # expect(test).to be_a(Array) + # expect(test).to eq([0,6,0,11]) + + # item_offset = 81; + # item_name = 'foo' + + # test = PuppetLanguageServer::PuppetParserHelper.get_selection_range_array( + # content, + # line_offsets, + # item_offset, + # item_name + # ) + + # expect(test).to be_a(Array) + # expect(test).to eq([9,6,9,9]) + + # # test_array = get_selection_range_array(content, line_offsets, item.offset, item.name) + + # end + # end +end From bd64c9601bcadca3b27981f580338bcc464e8a33 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Thu, 4 Oct 2018 17:11:30 +0800 Subject: [PATCH 2/2] (GH-56) Recursive Document Symbol Provider This commit builds on the procedural DocumentSymbolProvider and makes it a recursive one. This does not support the Puppet 4 AST, and returns an empty array. --- CHANGELOG.md | 2 + lib/languageserver/document_symbol.rb | 86 +++++----- .../manifest/document_symbol_provider.rb | 151 ++++++++++++++++++ lib/puppet-languageserver/message_router.rb | 4 +- lib/puppet-languageserver/providers.rb | 1 + .../server_capabilities.rb | 2 +- .../manifest/document_symbol_provider_spec.rb | 94 +++++++++++ .../puppet_parser_helper_spec.rb | 134 ---------------- 8 files changed, 294 insertions(+), 180 deletions(-) create mode 100644 lib/puppet-languageserver/manifest/document_symbol_provider.rb create mode 100644 spec/languageserver/unit/puppet-languageserver/manifest/document_symbol_provider_spec.rb delete mode 100644 spec/languageserver/unit/puppet-languageserver/puppet_parser_helper_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 51590a91..0447b8ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## Unreleased +- ([GH-56](https://github.com/lingua-pupuli/puppet-editor-services/issues/56)) Add DocumentSymbol Support + ## 0.14.0 - 2018-08-17 ### Fixed diff --git a/lib/languageserver/document_symbol.rb b/lib/languageserver/document_symbol.rb index 069dadb9..3c47a15c 100644 --- a/lib/languageserver/document_symbol.rb +++ b/lib/languageserver/document_symbol.rb @@ -1,42 +1,42 @@ module LanguageServer -# /** -# * Represents programming constructs like variables, classes, interfaces etc. that appear in a document. Document symbols can be -# * hierarchical and they have two ranges: one that encloses its definition and one that points to its most interesting range, -# * e.g. the range of an identifier. -# */ -# export class DocumentSymbol { -# /** -# * The name of this symbol. -# */ -# name: string; -# /** -# * More detail for this symbol, e.g the signature of a function. -# */ -# detail?: string; -# /** -# * The kind of this symbol. -# */ -# kind: SymbolKind; -# /** -# * Indicates if this symbol is deprecated. -# */ -# deprecated?: boolean; -# /** -# * The range enclosing this symbol not including leading/trailing whitespace but everything else -# * like comments. This information is typically used to determine if the clients cursor is -# * inside the symbol to reveal in the symbol in the UI. -# */ -# range: Range; -# /** -# * The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. -# * Must be contained by the `range`. -# */ -# selectionRange: Range; -# /** -# * Children of this symbol, e.g. properties of a class. -# */ -# children?: DocumentSymbol[]; -# } + # /** + # * Represents programming constructs like variables, classes, interfaces etc. that appear in a document. Document symbols can be + # * hierarchical and they have two ranges: one that encloses its definition and one that points to its most interesting range, + # * e.g. the range of an identifier. + # */ + # export class DocumentSymbol { + # /** + # * The name of this symbol. + # */ + # name: string; + # /** + # * More detail for this symbol, e.g the signature of a function. + # */ + # detail?: string; + # /** + # * The kind of this symbol. + # */ + # kind: SymbolKind; + # /** + # * Indicates if this symbol is deprecated. + # */ + # deprecated?: boolean; + # /** + # * The range enclosing this symbol not including leading/trailing whitespace but everything else + # * like comments. This information is typically used to determine if the clients cursor is + # * inside the symbol to reveal in the symbol in the UI. + # */ + # range: Range; + # /** + # * The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. + # * Must be contained by the `range`. + # */ + # selectionRange: Range; + # /** + # * Children of this symbol, e.g. properties of a class. + # */ + # children?: DocumentSymbol[]; + # } module DocumentSymbol def self.create(options) result = {} @@ -44,7 +44,7 @@ def self.create(options) raise('kind is a required field for DocumentSymbol') if options['kind'].nil? raise('range is a required field for DocumentSymbol') if options['range'].nil? raise('selectionRange is a required field for DocumentSymbol') if options['selectionRange'].nil? - + result['name'] = options['name'] result['kind'] = options['kind'] result['detail'] = options['detail'] unless options['detail'].nil? @@ -54,22 +54,22 @@ def self.create(options) result['range'] = { 'start' => { 'line' => options['range'][0], - 'character' => options['range'][1], + 'character' => options['range'][1] }, 'end' => { 'line' => options['range'][2], - 'character' => options['range'][3], + 'character' => options['range'][3] } } result['selectionRange'] = { 'start' => { 'line' => options['selectionRange'][0], - 'character' => options['selectionRange'][1], + 'character' => options['selectionRange'][1] }, 'end' => { 'line' => options['selectionRange'][2], - 'character' => options['selectionRange'][3], + 'character' => options['selectionRange'][3] } } diff --git a/lib/puppet-languageserver/manifest/document_symbol_provider.rb b/lib/puppet-languageserver/manifest/document_symbol_provider.rb new file mode 100644 index 00000000..243f503c --- /dev/null +++ b/lib/puppet-languageserver/manifest/document_symbol_provider.rb @@ -0,0 +1,151 @@ +module PuppetLanguageServer + module Manifest + module DocumentSymbolProvider + def self.extract_document_symbols(content) + parser = Puppet::Pops::Parser::Parser.new + result = parser.parse_string(content, '') + + if result.model.respond_to? :eAllContents + # We are unable to build a document symbol tree for Puppet 4 AST + return [] + end + symbols = [] + recurse_document_symbols(result.model, '', nil, symbols) # [] + + symbols + end + + def self.create_range_array(offset, length, locator) + start_line = locator.line_for_offset(offset) - 1 + start_char = locator.pos_on_line(offset) - 1 + end_line = locator.line_for_offset(offset + length) - 1 + end_char = locator.pos_on_line(offset + length) - 1 + + [start_line, start_char, end_line, end_char] + end + + def self.create_range_object(offset, length, locator) + result = create_range_array(offset, length, locator) + { + 'start' => { + 'line' => result[0], + 'character' => result[1] + }, + 'end' => { + 'line' => result[2], + 'character' => result[3] + } + } + end + + def self.locator_text(offset, length, locator) + locator.string.slice(offset, length) + end + + def self.recurse_document_symbols(object, path, parentsymbol, symbollist) + # POPS Object Model + # https://github.com/puppetlabs/puppet/blob/master/lib/puppet/pops/model/ast.pp + + # Path is just an internal path for debugging + # path = path + '/' + object.class.to_s[object.class.to_s.rindex('::')..-1] + + this_symbol = nil + + case object.class.to_s + # Puppet Resources + when 'Puppet::Pops::Model::ResourceExpression' + this_symbol = LanguageServer::DocumentSymbol.create( + 'name' => object.type_name.value, + 'kind' => LanguageServer::SYMBOLKIND_METHOD, + 'detail' => object.type_name.value, + 'range' => create_range_array(object.offset, object.length, object.locator), + 'selectionRange' => create_range_array(object.offset, object.length, object.locator), + 'children' => [] + ) + + when 'Puppet::Pops::Model::ResourceBody' + # We modify the parent symbol with the resource information, + # mainly we care about the resource title. + parentsymbol['name'] = parentsymbol['name'] + ': ' + locator_text(object.title.offset, object.title.length, object.title.locator) + parentsymbol['detail'] = parentsymbol['name'] + parentsymbol['selectionRange'] = create_range_object(object.title.offset, object.title.length, object.locator) + + when 'Puppet::Pops::Model::AttributeOperation' + attr_name = object.attribute_name + this_symbol = LanguageServer::DocumentSymbol.create( + 'name' => attr_name, + 'kind' => LanguageServer::SYMBOLKIND_VARIABLE, + 'detail' => attr_name, + 'range' => create_range_array(object.offset, object.length, object.locator), + 'selectionRange' => create_range_array(object.offset, attr_name.length, object.locator), + 'children' => [] + ) + + # Puppet Class + when 'Puppet::Pops::Model::HostClassDefinition' + this_symbol = LanguageServer::DocumentSymbol.create( + 'name' => object.name, + 'kind' => LanguageServer::SYMBOLKIND_CLASS, + 'detail' => object.name, + 'range' => create_range_array(object.offset, object.length, object.locator), + 'selectionRange' => create_range_array(object.offset, object.length, object.locator), + 'children' => [] + ) + # Load in the class parameters + object.parameters.each do |param| + param_symbol = LanguageServer::DocumentSymbol.create( + 'name' => '$' + param.name, + 'kind' => LanguageServer::SYMBOLKIND_PROPERTY, + 'detail' => '$' + param.name, + 'range' => create_range_array(param.offset, param.length, param.locator), + 'selectionRange' => create_range_array(param.offset, param.length, param.locator), + 'children' => [] + ) + this_symbol['children'].push(param_symbol) + end + + # Puppet Defined Type + when 'Puppet::Pops::Model::ResourceTypeDefinition' + this_symbol = LanguageServer::DocumentSymbol.create( + 'name' => object.name, + 'kind' => LanguageServer::SYMBOLKIND_CLASS, + 'detail' => object.name, + 'range' => create_range_array(object.offset, object.length, object.locator), + 'selectionRange' => create_range_array(object.offset, object.length, object.locator), + 'children' => [] + ) + # Load in the class parameters + object.parameters.each do |param| + param_symbol = LanguageServer::DocumentSymbol.create( + 'name' => '$' + param.name, + 'kind' => LanguageServer::SYMBOLKIND_FIELD, + 'detail' => '$' + param.name, + 'range' => create_range_array(param.offset, param.length, param.locator), + 'selectionRange' => create_range_array(param.offset, param.length, param.locator), + 'children' => [] + ) + this_symbol['children'].push(param_symbol) + end + + when 'Puppet::Pops::Model::AssignmentExpression' + this_symbol = LanguageServer::DocumentSymbol.create( + 'name' => '$' + object.left_expr.expr.value, + 'kind' => LanguageServer::SYMBOLKIND_VARIABLE, + 'detail' => '$' + object.left_expr.expr.value, + 'range' => create_range_array(object.left_expr.offset, object.left_expr.length, object.left_expr.locator), + 'selectionRange' => create_range_array(object.left_expr.offset, object.left_expr.length, object.left_expr.locator), + 'children' => [] + ) + + end + + object._pcore_contents do |item| + recurse_document_symbols(item, path, this_symbol.nil? ? parentsymbol : this_symbol, symbollist) + end + + return if this_symbol.nil? + parentsymbol.nil? ? symbollist.push(this_symbol) : parentsymbol['children'].push(this_symbol) + end + end + end +end diff --git a/lib/puppet-languageserver/message_router.rb b/lib/puppet-languageserver/message_router.rb index e267c12b..6d470429 100644 --- a/lib/puppet-languageserver/message_router.rb +++ b/lib/puppet-languageserver/message_router.rb @@ -155,8 +155,8 @@ def receive_request(request) begin case documents.document_type(file_uri) when :manifest - r = PuppetLanguageServer::PuppetParserHelper.extract_document_symbols(content) - request.reply_result(r) + result = PuppetLanguageServer::Manifest::DocumentSymbolProvider.extract_document_symbols(content) + request.reply_result(result) else raise "Unable to provide definition on #{file_uri}" end diff --git a/lib/puppet-languageserver/providers.rb b/lib/puppet-languageserver/providers.rb index af5ee1cf..9c88636c 100644 --- a/lib/puppet-languageserver/providers.rb +++ b/lib/puppet-languageserver/providers.rb @@ -2,6 +2,7 @@ epp/validation_provider manifest/completion_provider manifest/definition_provider + manifest/document_symbol_provider manifest/validation_provider manifest/hover_provider puppetfile/r10k/module/base diff --git a/lib/puppet-languageserver/server_capabilities.rb b/lib/puppet-languageserver/server_capabilities.rb index 1989ee8e..fa138bf7 100644 --- a/lib/puppet-languageserver/server_capabilities.rb +++ b/lib/puppet-languageserver/server_capabilities.rb @@ -11,7 +11,7 @@ def self.capabilities 'triggerCharacters' => ['>', '$', '[', '='] }, 'definitionProvider' => true, - 'documentSymbolProvider' => true, + 'documentSymbolProvider' => true } end end diff --git a/spec/languageserver/unit/puppet-languageserver/manifest/document_symbol_provider_spec.rb b/spec/languageserver/unit/puppet-languageserver/manifest/document_symbol_provider_spec.rb new file mode 100644 index 00000000..3bbf85a8 --- /dev/null +++ b/spec/languageserver/unit/puppet-languageserver/manifest/document_symbol_provider_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +RSpec::Matchers.define :be_document_symbol do |name, kind, start_line, start_char, end_line, end_char| + match do |actual| + actual['name'] == name && + actual['kind'] == kind && + actual['range']['start']['line'] == start_line && + actual['range']['start']['character'] == start_char && + actual['range']['end']['line'] == end_line && + actual['range']['end']['character'] == end_char + end + + failure_message do |actual| + "expected that symbol called '#{actual['name']}' of type '#{actual['kind']}' located at " + + "(#{actual['range']['start']['line']}, #{actual['range']['start']['character']}, " + + "#{actual['range']['end']['line']}, #{actual['range']['end']['character']}) would be " + + "a document symbol called '#{name}', of type '#{kind}' located at (#{start_line}, #{start_char}, #{end_line}, #{end_char})" + end + + description do + "be a document symbol called '#{name}' of type #{kind} located at #{start_line}, #{start_char}, #{end_line}, #{end_char}" + end +end + +describe 'PuppetLanguageServer::Manifest::DocumentSymbolProvider' do + let(:subject) { PuppetLanguageServer::Manifest::DocumentSymbolProvider } + + context 'with Puppet 4.0 and below', :if => Gem::Version.new(Puppet.version) < Gem::Version.new('5.0.0') do + describe '#extract_document_symbols' do + it 'should always return an empty array' do + content = <<-EOT + class foo { + user { 'alice': + } + } + EOT + result = subject.extract_document_symbols(content) + + expect(result).to eq([]) + end + end + end + + context 'with Puppet 5.0 and above', :if => Gem::Version.new(Puppet.version) >= Gem::Version.new('5.0.0') do + describe '#extract_document_symbols' do + it 'should find a class in the document root' do + content = "class foo {\n}" + result = subject.extract_document_symbols(content) + expect(result.count).to eq(1) + expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 0, 1, 1) + end + + it 'should find a resource in the document root' do + content = "user { 'alice':\n}" + result = subject.extract_document_symbols(content) + + expect(result.count).to eq(1) + expect(result[0]).to be_document_symbol("user: 'alice'", LanguageServer::SYMBOLKIND_METHOD, 0, 0, 1, 1) + end + + it 'should find a single line class in the document root' do + content = "class foo(String $var1 = 'value1', String $var2 = 'value2') {\n}" + result = subject.extract_document_symbols(content) + + expect(result.count).to eq(1) + expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 0, 1, 1) + expect(result[0]['children'].count).to eq(2) + expect(result[0]['children'][0]).to be_document_symbol('$var1', LanguageServer::SYMBOLKIND_PROPERTY, 0, 17, 0, 22) + expect(result[0]['children'][1]).to be_document_symbol('$var2', LanguageServer::SYMBOLKIND_PROPERTY, 0, 42, 0, 47) + end + + it 'should find a multi line class in the document root' do + content = "class foo(\n String $var1 = 'value1',\n String $var2 = 'value2',\n) {\n}" + result = subject.extract_document_symbols(content) + + expect(result.count).to eq(1) + expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 0, 4, 1) + expect(result[0]['children'].count).to eq(2) + expect(result[0]['children'][0]).to be_document_symbol('$var1', LanguageServer::SYMBOLKIND_PROPERTY, 1, 9, 1, 14) + expect(result[0]['children'][1]).to be_document_symbol('$var2', LanguageServer::SYMBOLKIND_PROPERTY, 2, 9, 2, 14) + end + + it 'should find a simple resource in a class' do + content = "class foo {\n user { 'alice':\n }\n}" + result = subject.extract_document_symbols(content) + + expect(result.count).to eq(1) + expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 0, 3, 1) + expect(result[0]['children'].count).to eq(1) + expect(result[0]['children'][0]).to be_document_symbol("user: 'alice'", LanguageServer::SYMBOLKIND_METHOD, 1, 2, 2, 3) + end + end + end +end diff --git a/spec/languageserver/unit/puppet-languageserver/puppet_parser_helper_spec.rb b/spec/languageserver/unit/puppet-languageserver/puppet_parser_helper_spec.rb deleted file mode 100644 index d0ab8eb7..00000000 --- a/spec/languageserver/unit/puppet-languageserver/puppet_parser_helper_spec.rb +++ /dev/null @@ -1,134 +0,0 @@ -require 'spec_helper' - -RSpec::Matchers.define :be_document_symbol do |name, kind, start_line, start_char, end_line, end_char| - match do |actual| - actual['name'] == name && - actual['kind'] == kind && - actual['range']['start']['line'] == start_line && - actual['range']['start']['character'] == start_char && - actual['range']['end']['line'] == end_line && - actual['range']['end']['character'] == end_char - end - - failure_message do |actual| - "expected that symbol called '#{actual['name']}' of type '#{actual['kind']}' located at " + - "(#{actual['range']['start']['line']}, #{actual['range']['start']['character']}, " + - "#{actual['range']['end']['line']}, #{actual['range']['end']['character']}) would be " + - "a document symbol called '#{name}', of type '#{kind}' located at (#{start_line}, #{start_char}, #{end_line}, #{end_char})" - end - - description do - "be a document symbol called '#{name}' of type #{kind} located at #{start_line}, #{start_char}, #{end_line}, #{end_char}" - end -end - -describe 'PuppetLanguageServer::PuppetParserHelper' do - let(:subject) { PuppetLanguageServer::PuppetParserHelper } - - describe '#extract_document_symbols' do - it 'should find a class in the document root' do - content = <<-EOT - class foo { - } - EOT - result = subject.extract_document_symbols(content) - - expect(result.count).to eq(1) - expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 6, 0, 9) - end - - it 'should find a resource in the document root' do - pending('Not supported yet') - content = <<-EOT - user { 'alice': - } - EOT - result = subject.extract_document_symbols(content) - - expect(result.count).to eq(1) - expect(result[0]).to be_document_symbol('alice', LanguageServer::SYMBOLKIND_MODULE, 0, 8, 0, 13) - end - - it 'should find a single line class in the document root' do - pending('Not supported yet') - content = <<-EOT - class foo(String $var1 = 'value1', String $var2 = 'value2') { - } - EOT - result = subject.extract_document_symbols(content) - - expect(result.count).to eq(1) - expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 6, 0, 9) - expect(result[0]['children'].count).to eq(2) - # TODO: Check that the children are the properties var1 and var2 - end - - it 'should find a multi line class in the document root' do - pending('Not supported yet') - content = <<-EOT - class foo( - String $var1 = 'value1', - String $var2 = 'value2', - ) { - } - EOT - result = subject.extract_document_symbols(content) - - expect(result.count).to eq(1) - expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 6, 0, 9) - expect(result[0]['children'].count).to eq(2) - # TODO: Check that the children are the properties var1 and var2 - end - - it 'should find a simple resource in a class' do - content = <<-EOT - class foo { - user { 'alice': - } - } - EOT - result = subject.extract_document_symbols(content) - - expect(result.count).to eq(1) - expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 6, 0, 9) - expect(result[0]['children'].count).to eq(1) - expect(result[0]['children'][0]).to be_document_symbol('alice', LanguageServer::SYMBOLKIND_METHOD, 1, 8, 1, 13) - end - end - - # TODO: The method get_selection_range_array doesn't exist on the module - # describe 'should find line' do - # it 'should find line of first class' do - # content = "class wakka(\n $param1 = ''\n) {\n user { 'james':\n ensure => 'present'\n }\n}\n\nclass foo{\n}\n" - # line_offsets = [0, 13, 28, 32, 50, 74, 78, 80, 81, 92, 94] - # item_offset = 0; - # item_name = 'wakka' - - # test = PuppetLanguageServer::PuppetParserHelper.get_selection_range_array( - # content, - # line_offsets, - # item_offset, - # item_name - # ) - - # expect(test).to be_a(Array) - # expect(test).to eq([0,6,0,11]) - - # item_offset = 81; - # item_name = 'foo' - - # test = PuppetLanguageServer::PuppetParserHelper.get_selection_range_array( - # content, - # line_offsets, - # item_offset, - # item_name - # ) - - # expect(test).to be_a(Array) - # expect(test).to eq([9,6,9,9]) - - # # test_array = get_selection_range_array(content, line_offsets, item.offset, item.name) - - # end - # end -end