Skip to content

Commit 83e794d

Browse files
authored
Merge pull request #59 from jpogran/GH-56-outlineview
(GH-56) OutLineView
2 parents 1677942 + bd64c96 commit 83e794d

File tree

9 files changed

+352
-2
lines changed

9 files changed

+352
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
66

77
## Unreleased
88

9+
- ([GH-56](https://github.com/lingua-pupuli/puppet-editor-services/issues/56)) Add DocumentSymbol Support
10+
911
## 0.14.0 - 2018-08-17
1012

1113
### Fixed

lib/languageserver/constants.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ module LanguageServer
4040
SYMBOLKIND_NUMBER = 16
4141
SYMBOLKIND_BOOLEAN = 17
4242
SYMBOLKIND_ARRAY = 18
43+
SYMBOLKIND_OBJECT = 19
44+
SYMBOLKIND_KEY = 20
45+
SYMBOLKIND_NULL = 21
46+
SYMBOLKIND_ENUMMEMBER = 22
47+
SYMBOLKIND_STRUCT = 23
48+
SYMBOLKIND_EVENT = 24
49+
SYMBOLKIND_OPERATOR = 25
4350

4451
TEXTDOCUMENTSYNCKIND_NONE = 0
4552
TEXTDOCUMENTSYNCKIND_FULL = 1
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
module LanguageServer
2+
# /**
3+
# * Represents programming constructs like variables, classes, interfaces etc. that appear in a document. Document symbols can be
4+
# * hierarchical and they have two ranges: one that encloses its definition and one that points to its most interesting range,
5+
# * e.g. the range of an identifier.
6+
# */
7+
# export class DocumentSymbol {
8+
# /**
9+
# * The name of this symbol.
10+
# */
11+
# name: string;
12+
# /**
13+
# * More detail for this symbol, e.g the signature of a function.
14+
# */
15+
# detail?: string;
16+
# /**
17+
# * The kind of this symbol.
18+
# */
19+
# kind: SymbolKind;
20+
# /**
21+
# * Indicates if this symbol is deprecated.
22+
# */
23+
# deprecated?: boolean;
24+
# /**
25+
# * The range enclosing this symbol not including leading/trailing whitespace but everything else
26+
# * like comments. This information is typically used to determine if the clients cursor is
27+
# * inside the symbol to reveal in the symbol in the UI.
28+
# */
29+
# range: Range;
30+
# /**
31+
# * The range that should be selected and revealed when this symbol is being picked, e.g the name of a function.
32+
# * Must be contained by the `range`.
33+
# */
34+
# selectionRange: Range;
35+
# /**
36+
# * Children of this symbol, e.g. properties of a class.
37+
# */
38+
# children?: DocumentSymbol[];
39+
# }
40+
module DocumentSymbol
41+
def self.create(options)
42+
result = {}
43+
raise('name is a required field for DocumentSymbol') if options['name'].nil?
44+
raise('kind is a required field for DocumentSymbol') if options['kind'].nil?
45+
raise('range is a required field for DocumentSymbol') if options['range'].nil?
46+
raise('selectionRange is a required field for DocumentSymbol') if options['selectionRange'].nil?
47+
48+
result['name'] = options['name']
49+
result['kind'] = options['kind']
50+
result['detail'] = options['detail'] unless options['detail'].nil?
51+
result['deprecated'] = options['deprecated'] unless options['deprecated'].nil?
52+
result['children'] = options['children'] unless options['children'].nil?
53+
54+
result['range'] = {
55+
'start' => {
56+
'line' => options['range'][0],
57+
'character' => options['range'][1]
58+
},
59+
'end' => {
60+
'line' => options['range'][2],
61+
'character' => options['range'][3]
62+
}
63+
}
64+
65+
result['selectionRange'] = {
66+
'start' => {
67+
'line' => options['selectionRange'][0],
68+
'character' => options['selectionRange'][1]
69+
},
70+
'end' => {
71+
'line' => options['selectionRange'][2],
72+
'character' => options['selectionRange'][3]
73+
}
74+
}
75+
76+
result
77+
end
78+
end
79+
end

lib/languageserver/languageserver.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
%w[constants diagnostic completion_list completion_item hover location puppet_version puppet_compilation puppet_fix_diagnostic_errors].each do |lib|
1+
%w[constants diagnostic completion_list completion_item document_symbol hover location puppet_version puppet_compilation puppet_fix_diagnostic_errors].each do |lib|
22
begin
33
require "languageserver/#{lib}"
44
rescue LoadError
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
module PuppetLanguageServer
2+
module Manifest
3+
module DocumentSymbolProvider
4+
def self.extract_document_symbols(content)
5+
parser = Puppet::Pops::Parser::Parser.new
6+
result = parser.parse_string(content, '')
7+
8+
if result.model.respond_to? :eAllContents
9+
# We are unable to build a document symbol tree for Puppet 4 AST
10+
return []
11+
end
12+
symbols = []
13+
recurse_document_symbols(result.model, '', nil, symbols) # []
14+
15+
symbols
16+
end
17+
18+
def self.create_range_array(offset, length, locator)
19+
start_line = locator.line_for_offset(offset) - 1
20+
start_char = locator.pos_on_line(offset) - 1
21+
end_line = locator.line_for_offset(offset + length) - 1
22+
end_char = locator.pos_on_line(offset + length) - 1
23+
24+
[start_line, start_char, end_line, end_char]
25+
end
26+
27+
def self.create_range_object(offset, length, locator)
28+
result = create_range_array(offset, length, locator)
29+
{
30+
'start' => {
31+
'line' => result[0],
32+
'character' => result[1]
33+
},
34+
'end' => {
35+
'line' => result[2],
36+
'character' => result[3]
37+
}
38+
}
39+
end
40+
41+
def self.locator_text(offset, length, locator)
42+
locator.string.slice(offset, length)
43+
end
44+
45+
def self.recurse_document_symbols(object, path, parentsymbol, symbollist)
46+
# POPS Object Model
47+
# https://github.com/puppetlabs/puppet/blob/master/lib/puppet/pops/model/ast.pp
48+
49+
# Path is just an internal path for debugging
50+
# path = path + '/' + object.class.to_s[object.class.to_s.rindex('::')..-1]
51+
52+
this_symbol = nil
53+
54+
case object.class.to_s
55+
# Puppet Resources
56+
when 'Puppet::Pops::Model::ResourceExpression'
57+
this_symbol = LanguageServer::DocumentSymbol.create(
58+
'name' => object.type_name.value,
59+
'kind' => LanguageServer::SYMBOLKIND_METHOD,
60+
'detail' => object.type_name.value,
61+
'range' => create_range_array(object.offset, object.length, object.locator),
62+
'selectionRange' => create_range_array(object.offset, object.length, object.locator),
63+
'children' => []
64+
)
65+
66+
when 'Puppet::Pops::Model::ResourceBody'
67+
# We modify the parent symbol with the resource information,
68+
# mainly we care about the resource title.
69+
parentsymbol['name'] = parentsymbol['name'] + ': ' + locator_text(object.title.offset, object.title.length, object.title.locator)
70+
parentsymbol['detail'] = parentsymbol['name']
71+
parentsymbol['selectionRange'] = create_range_object(object.title.offset, object.title.length, object.locator)
72+
73+
when 'Puppet::Pops::Model::AttributeOperation'
74+
attr_name = object.attribute_name
75+
this_symbol = LanguageServer::DocumentSymbol.create(
76+
'name' => attr_name,
77+
'kind' => LanguageServer::SYMBOLKIND_VARIABLE,
78+
'detail' => attr_name,
79+
'range' => create_range_array(object.offset, object.length, object.locator),
80+
'selectionRange' => create_range_array(object.offset, attr_name.length, object.locator),
81+
'children' => []
82+
)
83+
84+
# Puppet Class
85+
when 'Puppet::Pops::Model::HostClassDefinition'
86+
this_symbol = LanguageServer::DocumentSymbol.create(
87+
'name' => object.name,
88+
'kind' => LanguageServer::SYMBOLKIND_CLASS,
89+
'detail' => object.name,
90+
'range' => create_range_array(object.offset, object.length, object.locator),
91+
'selectionRange' => create_range_array(object.offset, object.length, object.locator),
92+
'children' => []
93+
)
94+
# Load in the class parameters
95+
object.parameters.each do |param|
96+
param_symbol = LanguageServer::DocumentSymbol.create(
97+
'name' => '$' + param.name,
98+
'kind' => LanguageServer::SYMBOLKIND_PROPERTY,
99+
'detail' => '$' + param.name,
100+
'range' => create_range_array(param.offset, param.length, param.locator),
101+
'selectionRange' => create_range_array(param.offset, param.length, param.locator),
102+
'children' => []
103+
)
104+
this_symbol['children'].push(param_symbol)
105+
end
106+
107+
# Puppet Defined Type
108+
when 'Puppet::Pops::Model::ResourceTypeDefinition'
109+
this_symbol = LanguageServer::DocumentSymbol.create(
110+
'name' => object.name,
111+
'kind' => LanguageServer::SYMBOLKIND_CLASS,
112+
'detail' => object.name,
113+
'range' => create_range_array(object.offset, object.length, object.locator),
114+
'selectionRange' => create_range_array(object.offset, object.length, object.locator),
115+
'children' => []
116+
)
117+
# Load in the class parameters
118+
object.parameters.each do |param|
119+
param_symbol = LanguageServer::DocumentSymbol.create(
120+
'name' => '$' + param.name,
121+
'kind' => LanguageServer::SYMBOLKIND_FIELD,
122+
'detail' => '$' + param.name,
123+
'range' => create_range_array(param.offset, param.length, param.locator),
124+
'selectionRange' => create_range_array(param.offset, param.length, param.locator),
125+
'children' => []
126+
)
127+
this_symbol['children'].push(param_symbol)
128+
end
129+
130+
when 'Puppet::Pops::Model::AssignmentExpression'
131+
this_symbol = LanguageServer::DocumentSymbol.create(
132+
'name' => '$' + object.left_expr.expr.value,
133+
'kind' => LanguageServer::SYMBOLKIND_VARIABLE,
134+
'detail' => '$' + object.left_expr.expr.value,
135+
'range' => create_range_array(object.left_expr.offset, object.left_expr.length, object.left_expr.locator),
136+
'selectionRange' => create_range_array(object.left_expr.offset, object.left_expr.length, object.left_expr.locator),
137+
'children' => []
138+
)
139+
140+
end
141+
142+
object._pcore_contents do |item|
143+
recurse_document_symbols(item, path, this_symbol.nil? ? parentsymbol : this_symbol, symbollist)
144+
end
145+
146+
return if this_symbol.nil?
147+
parentsymbol.nil? ? symbollist.push(this_symbol) : parentsymbol['children'].push(this_symbol)
148+
end
149+
end
150+
end
151+
end

lib/puppet-languageserver/message_router.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,21 @@ def receive_request(request)
149149
request.reply_result(nil)
150150
end
151151

152+
when 'textDocument/documentSymbol'
153+
file_uri = request.params['textDocument']['uri']
154+
content = documents.document(file_uri)
155+
begin
156+
case documents.document_type(file_uri)
157+
when :manifest
158+
result = PuppetLanguageServer::Manifest::DocumentSymbolProvider.extract_document_symbols(content)
159+
request.reply_result(result)
160+
else
161+
raise "Unable to provide definition on #{file_uri}"
162+
end
163+
rescue StandardError => exception
164+
PuppetLanguageServer.log_message(:error, "(textDocument/documentSymbol) #{exception}")
165+
request.reply_result(nil)
166+
end
152167
else
153168
PuppetLanguageServer.log_message(:error, "Unknown RPC method #{request.rpc_method}")
154169
end

lib/puppet-languageserver/providers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
epp/validation_provider
33
manifest/completion_provider
44
manifest/definition_provider
5+
manifest/document_symbol_provider
56
manifest/validation_provider
67
manifest/hover_provider
78
puppetfile/r10k/module/base

lib/puppet-languageserver/server_capabilities.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ def self.capabilities
1010
'resolveProvider' => true,
1111
'triggerCharacters' => ['>', '$', '[', '=']
1212
},
13-
'definitionProvider' => true
13+
'definitionProvider' => true,
14+
'documentSymbolProvider' => true
1415
}
1516
end
1617
end
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
require 'spec_helper'
2+
3+
RSpec::Matchers.define :be_document_symbol do |name, kind, start_line, start_char, end_line, end_char|
4+
match do |actual|
5+
actual['name'] == name &&
6+
actual['kind'] == kind &&
7+
actual['range']['start']['line'] == start_line &&
8+
actual['range']['start']['character'] == start_char &&
9+
actual['range']['end']['line'] == end_line &&
10+
actual['range']['end']['character'] == end_char
11+
end
12+
13+
failure_message do |actual|
14+
"expected that symbol called '#{actual['name']}' of type '#{actual['kind']}' located at " +
15+
"(#{actual['range']['start']['line']}, #{actual['range']['start']['character']}, " +
16+
"#{actual['range']['end']['line']}, #{actual['range']['end']['character']}) would be " +
17+
"a document symbol called '#{name}', of type '#{kind}' located at (#{start_line}, #{start_char}, #{end_line}, #{end_char})"
18+
end
19+
20+
description do
21+
"be a document symbol called '#{name}' of type #{kind} located at #{start_line}, #{start_char}, #{end_line}, #{end_char}"
22+
end
23+
end
24+
25+
describe 'PuppetLanguageServer::Manifest::DocumentSymbolProvider' do
26+
let(:subject) { PuppetLanguageServer::Manifest::DocumentSymbolProvider }
27+
28+
context 'with Puppet 4.0 and below', :if => Gem::Version.new(Puppet.version) < Gem::Version.new('5.0.0') do
29+
describe '#extract_document_symbols' do
30+
it 'should always return an empty array' do
31+
content = <<-EOT
32+
class foo {
33+
user { 'alice':
34+
}
35+
}
36+
EOT
37+
result = subject.extract_document_symbols(content)
38+
39+
expect(result).to eq([])
40+
end
41+
end
42+
end
43+
44+
context 'with Puppet 5.0 and above', :if => Gem::Version.new(Puppet.version) >= Gem::Version.new('5.0.0') do
45+
describe '#extract_document_symbols' do
46+
it 'should find a class in the document root' do
47+
content = "class foo {\n}"
48+
result = subject.extract_document_symbols(content)
49+
expect(result.count).to eq(1)
50+
expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 0, 1, 1)
51+
end
52+
53+
it 'should find a resource in the document root' do
54+
content = "user { 'alice':\n}"
55+
result = subject.extract_document_symbols(content)
56+
57+
expect(result.count).to eq(1)
58+
expect(result[0]).to be_document_symbol("user: 'alice'", LanguageServer::SYMBOLKIND_METHOD, 0, 0, 1, 1)
59+
end
60+
61+
it 'should find a single line class in the document root' do
62+
content = "class foo(String $var1 = 'value1', String $var2 = 'value2') {\n}"
63+
result = subject.extract_document_symbols(content)
64+
65+
expect(result.count).to eq(1)
66+
expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 0, 1, 1)
67+
expect(result[0]['children'].count).to eq(2)
68+
expect(result[0]['children'][0]).to be_document_symbol('$var1', LanguageServer::SYMBOLKIND_PROPERTY, 0, 17, 0, 22)
69+
expect(result[0]['children'][1]).to be_document_symbol('$var2', LanguageServer::SYMBOLKIND_PROPERTY, 0, 42, 0, 47)
70+
end
71+
72+
it 'should find a multi line class in the document root' do
73+
content = "class foo(\n String $var1 = 'value1',\n String $var2 = 'value2',\n) {\n}"
74+
result = subject.extract_document_symbols(content)
75+
76+
expect(result.count).to eq(1)
77+
expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 0, 4, 1)
78+
expect(result[0]['children'].count).to eq(2)
79+
expect(result[0]['children'][0]).to be_document_symbol('$var1', LanguageServer::SYMBOLKIND_PROPERTY, 1, 9, 1, 14)
80+
expect(result[0]['children'][1]).to be_document_symbol('$var2', LanguageServer::SYMBOLKIND_PROPERTY, 2, 9, 2, 14)
81+
end
82+
83+
it 'should find a simple resource in a class' do
84+
content = "class foo {\n user { 'alice':\n }\n}"
85+
result = subject.extract_document_symbols(content)
86+
87+
expect(result.count).to eq(1)
88+
expect(result[0]).to be_document_symbol('foo', LanguageServer::SYMBOLKIND_CLASS, 0, 0, 3, 1)
89+
expect(result[0]['children'].count).to eq(1)
90+
expect(result[0]['children'][0]).to be_document_symbol("user: 'alice'", LanguageServer::SYMBOLKIND_METHOD, 1, 2, 2, 3)
91+
end
92+
end
93+
end
94+
end

0 commit comments

Comments
 (0)