Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions lib/languageserver/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions lib/languageserver/document_symbol.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion lib/languageserver/languageserver.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
151 changes: 151 additions & 0 deletions lib/puppet-languageserver/manifest/document_symbol_provider.rb
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions lib/puppet-languageserver/message_router.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
result = PuppetLanguageServer::Manifest::DocumentSymbolProvider.extract_document_symbols(content)
request.reply_result(result)
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
Expand Down
1 change: 1 addition & 0 deletions lib/puppet-languageserver/providers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/puppet-languageserver/server_capabilities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ def self.capabilities
'resolveProvider' => true,
'triggerCharacters' => ['>', '$', '[', '=']
},
'definitionProvider' => true
'definitionProvider' => true,
'documentSymbolProvider' => true
}
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -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