hind 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edc5339a6fe71b30f48781500d996901bef7732b5d6ed6a54ce561d057edce19
4
- data.tar.gz: 8dd64ea610eaf53314e918d91846d42839b522550e2b29a2a626228f7c459fbd
3
+ metadata.gz: 67b64fd17d7735528e37b9098db56f71a5332e01e58e9dfd958052da1b68e220
4
+ data.tar.gz: 7d5ab4b8b54fd439be53eba3f40df928ea35852d321945f972b09bfad5a4de24
5
5
  SHA512:
6
- metadata.gz: d85651252a56a48c32c3285205cea32d0aa99e8098b396168cb1bf3357b61a9deff6ef90ceecee3b891761205bcb1d420235d4859b780629555080c040d9b25e
7
- data.tar.gz: e69fa976556441508e1f46f7bb2636989a3cb810922636b1e460e3fa68130c7dc60538e55609349365506bf3fe21807a379c4afd5f54f4272582f02071164eef
6
+ metadata.gz: 462b167b1073fcdbe74dc9d1731e07b068a5b49c9f13e1204f7a8c58925a972dfef541f28388ed163efa59b8057df2f535f46ced8fdeb0e76308a505e58231fb
7
+ data.tar.gz: '035070842f07d55edc194ec60b8210858e01a68b8261f444c4e1c6af5d8418e5436ebc8a8aca071dbf608ba0baabdce3ae6b7d08adbf12e05317eeb0bb4387d7'
data/README.md CHANGED
@@ -18,6 +18,10 @@ gem 'hind'
18
18
 
19
19
  ## Usage
20
20
 
21
+ ### GitLab CI Integration
22
+
23
+ See [GitLab CI Integration Guide](docs/gitlab_ci.md) for detailed instructions.
24
+
21
25
  ### Generating LSIF Data
22
26
 
23
27
  To generate LSIF data for your Ruby project:
@@ -39,6 +43,16 @@ hind lsif -e "test/**/*" -e "spec/**/*"
39
43
  hind lsif -v
40
44
  ```
41
45
 
46
+ ### Generating SCIP Data
47
+
48
+ To generate SCIP data for your Ruby project:
49
+
50
+ ```bash
51
+ hind scip
52
+ ```
53
+
54
+ This will produce an `index.scip` file which is a JSON serialization of the SCIP index.
55
+
42
56
  Options:
43
57
  - `-d, --directory DIR` - Root directory to process (default: current directory)
44
58
  - `-o, --output FILE` - Output file path (default: dump.lsif)
@@ -100,6 +114,25 @@ bundle exec rspec
100
114
  bundle exec bin/hind
101
115
  ```
102
116
 
117
+ ## Roadmap / TODO
118
+
119
+ The following features are planned for future releases to improve indexing fidelity and performance:
120
+
121
+ - **Ruby Semantic Fidelity**
122
+ - [ ] Ancestor Chain Resolution (`include`, `extend`, `prepend`)
123
+ - [ ] Local Variables & Method Parameters
124
+ - [ ] Dynamic Method Support (`attr_accessor`, `delegate`, etc.)
125
+ - [ ] Block & Lambda Scopes
126
+ - **External Dependencies**
127
+ - [ ] External Gem Resolution (linking to symbols in dependencies)
128
+ - [ ] Core & Standard Library Linking
129
+ - **Advanced SCIP/LSIF Features**
130
+ - [ ] SCIP Relationships (implementation/inheritance edges)
131
+ - [ ] LSIF Monikers for cross-repo resolution
132
+ - **Performance**
133
+ - [ ] Parallel Processing for large codebases
134
+ - [ ] Incremental Indexing
135
+
103
136
  ## Contributing
104
137
 
105
138
  Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/hind.
data/lib/hind/cli.rb CHANGED
@@ -5,33 +5,98 @@ require 'json'
5
5
  require 'pathname'
6
6
  require 'fileutils'
7
7
 
8
+ require_relative 'lsif/global_state'
9
+ require_relative 'lsif/generator'
10
+ require_relative 'scip/generator'
11
+
8
12
  module Hind
9
13
  class CLI < Thor
10
14
  class_option :verbose, type: :boolean, aliases: '-v', desc: 'Enable verbose output'
11
15
 
12
- desc 'lsif', 'Generate LSIF index for Ruby classes, modules, and constants'
13
- method_option :directory, type: :string, aliases: '-d', default: '.', desc: 'Root directory to process'
16
+ desc 'lsif [DIRECTORY]', 'Generate LSIF index for Ruby classes, modules, and constants'
17
+ method_option :directory, type: :string, aliases: '-d', desc: 'Root directory to process (deprecated, use positional argument)'
14
18
  method_option :output, type: :string, aliases: '-o', default: 'dump.lsif', desc: 'Output file path'
15
19
  method_option :glob, type: :string, aliases: '-g', default: '**/*.rb', desc: 'File pattern to match'
16
20
  method_option :force, type: :boolean, aliases: '-f', desc: 'Overwrite output file if it exists'
17
21
  method_option :exclude, type: :array, aliases: '-e', desc: 'Patterns to exclude'
18
- def lsif
19
- validate_directory(options[:directory])
22
+ def lsif(dir = options[:directory] || '.')
23
+ validate_directory(dir)
20
24
  validate_output_file(options[:output], options[:force])
21
25
 
22
- files = find_files(options[:directory], options[:glob], options[:exclude])
23
- abort "No files found matching pattern '#{options[:glob]}'" if files.empty?
26
+ files = find_files(dir, options[:glob], options[:exclude])
27
+ abort "No files found matching pattern '#{options[:glob]}' in #{dir}" if files.empty?
24
28
 
25
- say "Found #{files.length} files to process", :green if options[:verbose]
29
+ say "Found #{files.length} files to process in #{dir}", :green if options[:verbose]
26
30
 
27
31
  begin
28
- generate_lsif(files, options)
32
+ generate_lsif(files, dir, options)
33
+
34
+ # Add debug info from the global state
35
+ if options[:verbose]
36
+ debug_info = LSIF::GlobalState.instance.debug_info
37
+ say "\nGlobal State Summary:", :cyan
38
+ say " Classes: #{debug_info[:classes_count]} (#{debug_info[:open_classes_count]} open classes)", :cyan
39
+ say " Modules: #{debug_info[:modules_count]} (#{debug_info[:open_modules_count]} open modules)", :cyan
40
+ say " Constants: #{debug_info[:constants_count]}", :cyan
41
+ say " References: #{debug_info[:references_count]}", :cyan
42
+ say " Result Sets: #{debug_info[:result_sets_count]}", :cyan
43
+
44
+ # Report on the most frequently reopened classes/modules
45
+ if debug_info[:open_classes_count] > 0
46
+ most_opened_classes = LSIF::GlobalState.instance.classes
47
+ .map { |name, data| [name, data[:definitions].size] }
48
+ .select { |_, count| count > 1 }
49
+ .sort_by { |_, count| -count }
50
+ .take(5)
51
+
52
+ say "\nMost frequently reopened classes:", :cyan
53
+ most_opened_classes.each do |name, count|
54
+ say " #{name}: #{count} definitions", :cyan
55
+ end
56
+ end
57
+
58
+ if debug_info[:open_modules_count] > 0
59
+ most_opened_modules = LSIF::GlobalState.instance.modules
60
+ .map { |name, data| [name, data[:definitions].size] }
61
+ .select { |_, count| count > 1 }
62
+ .sort_by { |_, count| -count }
63
+ .take(5)
64
+
65
+ say "\nMost frequently reopened modules:", :cyan
66
+ most_opened_modules.each do |name, count|
67
+ say " #{name}: #{count} definitions", :cyan
68
+ end
69
+ end
70
+ end
71
+
29
72
  say "\nLSIF data has been written to: #{options[:output]}", :green if options[:verbose]
30
73
  rescue => e
31
74
  handle_error(e, options[:verbose])
32
75
  end
33
76
  end
34
77
 
78
+ desc 'scip [DIRECTORY]', 'Generate SCIP index'
79
+ method_option :directory, type: :string, aliases: '-d', desc: 'Root directory to process (deprecated, use positional argument)'
80
+ method_option :output, type: :string, aliases: '-o', default: 'index.scip', desc: 'Output file path'
81
+ method_option :glob, type: :string, aliases: '-g', default: '**/*.rb', desc: 'File pattern to match'
82
+ method_option :force, type: :boolean, aliases: '-f', desc: 'Overwrite output file if it exists'
83
+ method_option :exclude, type: :array, aliases: '-e', desc: 'Patterns to exclude'
84
+ def scip(dir = options[:directory] || '.')
85
+ validate_directory(dir)
86
+ validate_output_file(options[:output], options[:force])
87
+
88
+ files = find_files(dir, options[:glob], options[:exclude])
89
+ abort "No files found matching pattern '#{options[:glob]}' in #{dir}" if files.empty?
90
+
91
+ say "Found #{files.length} files to process in #{dir}", :green if options[:verbose]
92
+
93
+ generator = Hind::SCIP::Generator.new(File.expand_path(dir))
94
+ index = generator.execute(files)
95
+
96
+ File.write(options[:output], index.to_proto, mode: 'wb')
97
+ say "SCIP index written to #{options[:output]}", :green
98
+ end
99
+
35
100
  desc 'version', 'Show version'
36
101
  def version
37
102
  say "Hind version #{Hind::VERSION}"
@@ -39,76 +104,26 @@ module Hind
39
104
 
40
105
  private
41
106
 
42
- def generate_lsif(files, options)
107
+ def generate_lsif(files, directory, options)
43
108
  # Initialize generator with absolute project root
44
109
  generator = Hind::LSIF::Generator.new(
45
110
  {
46
111
  vertex_id: 1,
47
- initial: true,
48
- projectRoot: File.expand_path(options[:directory])
112
+ projectRoot: File.expand_path(directory)
49
113
  }
50
114
  )
51
115
 
52
116
  File.open(options[:output], 'w') do |output_file|
53
- say 'First pass: Collecting declarations...', :cyan if options[:verbose]
117
+ say 'Processing files...', :cyan if options[:verbose]
54
118
 
55
- # Write initial LSIF data (metadata and project vertices)
56
- initial_data = generator.get_initial_data
57
- if initial_data&.any?
58
- say 'Writing initial LSIF data...', :cyan if options[:verbose]
59
- output_file.puts(initial_data.map(&:to_json).join("\n"))
60
- end
119
+ lsif_data = generator.execute(files, options)
61
120
 
62
- # First pass: Process all files to collect declarations
63
- declaration_data = {}
64
- files.each do |file|
65
- absolute_path = File.expand_path(file)
66
- relative_path = Pathname.new(absolute_path)
67
- .relative_path_from(Pathname.new(generator.metadata[:projectRoot]))
68
- .to_s
69
-
70
- begin
71
- content = File.read(absolute_path)
72
- file_declaration_data = generator.collect_file_declarations(content, relative_path)
73
- declaration_data.merge!(file_declaration_data)
74
- rescue => e
75
- warn "Warning: Failed to read file '#{file}': #{e.message}"
76
- next
77
- end
121
+ # Get counts from global state
122
+ if options[:verbose]
123
+ say "Found #{LSIF::GlobalState.instance.classes.size} classes, #{LSIF::GlobalState.instance.modules.size} modules, and #{LSIF::GlobalState.instance.constants.size} constants", :cyan
78
124
  end
79
125
 
80
- say "Found #{declaration_data[:declarations]&.size} declarations (classes, modules, constants)", :cyan if options[:verbose]
81
-
82
- # Write declaration LSIF data next
83
- if declaration_data[:lsif_data].any?
84
- output_file.puts(declaration_data[:lsif_data].map(&:to_json).join("\n"))
85
- end
86
-
87
- say 'Processing files for references...', :cyan if options[:verbose]
88
-
89
- # Second pass: Process each file for references
90
- files.each do |file|
91
- absolute_path = File.expand_path(file)
92
- relative_path = Pathname.new(absolute_path)
93
- .relative_path_from(Pathname.new(generator.metadata[:projectRoot]))
94
- .to_s
95
-
96
- if options[:verbose]
97
- say "Processing file: #{relative_path}", :cyan
98
- end
99
-
100
- begin
101
- content = File.read(absolute_path)
102
- reference_lsif_data = generator.process_file(
103
- content: content,
104
- uri: relative_path
105
- )
106
- output_file.puts(reference_lsif_data.map(&:to_json).join("\n"))
107
- rescue => e
108
- warn "Warning: Failed to read file '#{file}': #{e.message}"
109
- next
110
- end
111
- end
126
+ output_file.puts(lsif_data.map(&:to_json).join("\n"))
112
127
  end
113
128
  end
114
129
 
@@ -127,7 +142,12 @@ module Hind
127
142
 
128
143
  def find_files(directory, glob, exclude_patterns)
129
144
  pattern = File.join(directory, glob)
130
- files = Dir.glob(pattern)
145
+ absolute_directory = File.expand_path(directory)
146
+
147
+ files = Dir.glob(pattern).map do |file|
148
+ # Return relative path to the directory for indexing
149
+ Pathname.new(File.expand_path(file)).relative_path_from(Pathname.new(absolute_directory)).to_s
150
+ end
131
151
 
132
152
  exclude_patterns&.each do |exclude|
133
153
  files.reject! { |f| File.fnmatch?(exclude, f) }
@@ -7,13 +7,14 @@ require 'pathname'
7
7
 
8
8
  require_relative 'visitors/declaration_visitor'
9
9
  require_relative 'visitors/reference_visitor'
10
+ require_relative 'global_state'
10
11
 
11
12
  module Hind
12
13
  module LSIF
13
14
  class Generator
14
15
  LSIF_VERSION = '0.4.3'
15
16
 
16
- attr_reader :metadata, :global_state, :document_id, :current_uri
17
+ attr_reader :metadata, :document_id, :current_uri
17
18
 
18
19
  def initialize(metadata = {})
19
20
  @vertex_id = metadata[:vertex_id] || 1
@@ -22,73 +23,166 @@ module Hind
22
23
  projectRoot: File.expand_path(metadata[:projectRoot] || Dir.pwd)
23
24
  }.merge(metadata)
24
25
 
25
- @global_state = GlobalState.new
26
+ # Reset the global state when initializing a new generator
27
+ GlobalState.instance.reset
26
28
  @document_ids = {}
27
29
  @current_document_id = nil
28
30
  @lsif_data = []
29
31
  @current_uri = nil
30
32
  @last_vertex_id = @vertex_id
31
-
32
- initialize_project if metadata[:initial]
33
+ @last_reference_index = 0
34
+ @initial_data = []
35
+ @initial_data_emitted = false
33
36
  end
34
37
 
35
- def collect_file_declarations(content, path)
36
- @current_uri = path
37
- @document_id = nil
38
- @current_document_id = nil
38
+ def execute(files, options)
39
+ # We need to ensure metadata/project vertices are the very first ones.
40
+ # If we collect declarations first, we might emit vertices.
41
+ # So we should initialize project first.
42
+
43
+ unless @initial_data_emitted
44
+ initialize_project
45
+ @initial_data_emitted = true
46
+ # Ensure these are at the start of @lsif_data
47
+ # specific logic: if we just added them, they are at the end (idx 0 if empty)
48
+ # but if we called collect_declarations separately?
49
+ # Actually, execute is the main entry point. @lsif_data is empty approx.
50
+ end
39
51
 
40
- begin
41
- ast = Parser.new(content).parse
42
- setup_document
43
- visitor = DeclarationVisitor.new(self, path)
44
- visitor.visit(ast)
45
- finalize_document_state
46
- rescue => e
47
- warn "Warning: Failed to collect declarations from '#{path}': #{e.message}"
52
+ # First pass: Declarations
53
+ files.each do |file|
54
+ @document_id = nil # Reset for each file
55
+ absolute_path = File.join(@metadata[:projectRoot], file)
56
+ next unless File.exist?(absolute_path)
57
+
58
+ source = File.read(absolute_path)
59
+ collect_file_declarations(source, file)
48
60
  end
49
61
 
50
- # Store the last used vertex ID and reference index
51
- @last_vertex_id = @vertex_id
52
- @last_reference_index = @lsif_data.length
62
+ # Second pass: References
63
+ files.each do |file|
64
+ @document_id = nil # Reset for each file
65
+ absolute_path = File.join(@metadata[:projectRoot], file)
66
+ next unless File.exist?(absolute_path)
67
+
68
+ source = File.read(absolute_path)
69
+ process_file(content: source, uri: file)
70
+ end
71
+
72
+ finalize_document_state
73
+
74
+ @lsif_data
75
+ end
53
76
 
54
- {
55
- declarations: @global_state.declarations,
56
- lsif_data: @lsif_data
57
- }
77
+ def collect_file_declarations(content, uri)
78
+ @current_uri = uri
79
+ result = Prism.parse(content)
80
+
81
+ declaration_visitor = DeclarationVisitor.new(self, uri)
82
+ result.value.accept(declaration_visitor)
83
+
84
+ { lsif_data: @lsif_data - @initial_data }
85
+ ensure
86
+ @current_uri = nil
58
87
  end
59
88
 
60
- def process_file(params)
61
- @current_uri = params[:uri]
62
- content = params[:content]
89
+ def process_file(content:, uri:)
90
+ @current_uri = uri
91
+ setup_document if @document_id.nil? || @document_ids[uri].nil?
92
+ @document_id = @document_ids[uri]
93
+ @current_document_id = @document_id
94
+
95
+ result = Prism.parse(content)
96
+
97
+ reference_visitor = ReferenceVisitor.new(self, uri)
98
+ result.value.accept(reference_visitor)
99
+
100
+ finalize_document_state
101
+ @lsif_data
102
+ ensure
103
+ @current_uri = nil
104
+ end
105
+ def get_initial_data
106
+ @initial_data
107
+ end
63
108
 
64
- # Restore vertex ID from last declaration pass
65
- @vertex_id = @last_vertex_id
109
+ def register_class_declaration(declaration)
110
+ return unless @current_uri && declaration[:node]
66
111
 
67
- @document_id = nil
68
- @current_document_id = nil
112
+ qualified_name = declaration[:name]
69
113
 
70
- setup_document
71
- ast = Parser.new(content).parse
114
+ setup_document if @document_id.nil?
115
+ current_doc_id = @document_id
72
116
 
73
- visitor = ReferenceVisitor.new(self, @current_uri)
74
- visitor.visit(ast)
117
+ range_id = create_range(declaration[:node].constant_path.location)
118
+ return unless range_id
75
119
 
76
- finalize_document_state
120
+ result_set_id = emit_vertex('resultSet')
121
+ emit_edge('next', range_id, result_set_id)
77
122
 
78
- # Update last vertex ID
79
- @last_vertex_id = @vertex_id
123
+ def_result_id = emit_vertex('definitionResult')
124
+ emit_edge('textDocument/definition', result_set_id, def_result_id)
125
+
126
+ emit_edge('item', def_result_id, [range_id], 'definitions', current_doc_id)
127
+
128
+ hover_content = generate_class_hover_content(declaration)
129
+ hover_id = emit_vertex('hoverResult', {
130
+ contents: [{
131
+ language: 'ruby',
132
+ value: hover_content
133
+ }]
134
+ })
135
+ emit_edge('textDocument/hover', result_set_id, hover_id)
136
+
137
+ declaration[:range_id] = range_id
138
+ declaration[:result_set_id] = result_set_id
139
+ declaration[:document_id] = current_doc_id
140
+ declaration[:file] = @current_uri
80
141
 
81
- # Return only the new LSIF data since last call
82
- result = @lsif_data[@last_reference_index..]
83
- @last_reference_index = @lsif_data.length
84
- result
142
+ GlobalState.instance.add_class(qualified_name, declaration)
143
+
144
+ result_set_id
85
145
  end
86
146
 
87
- def get_initial_data
88
- @initial_data
147
+ def register_module_declaration(declaration)
148
+ return unless @current_uri && declaration[:node]
149
+
150
+ qualified_name = declaration[:name]
151
+
152
+ setup_document if @document_id.nil?
153
+ current_doc_id = @document_id
154
+
155
+ range_id = create_range(declaration[:node].constant_path.location)
156
+ return unless range_id
157
+
158
+ result_set_id = emit_vertex('resultSet')
159
+ emit_edge('next', range_id, result_set_id)
160
+
161
+ def_result_id = emit_vertex('definitionResult')
162
+ emit_edge('textDocument/definition', result_set_id, def_result_id)
163
+
164
+ emit_edge('item', def_result_id, [range_id], 'definitions', current_doc_id)
165
+
166
+ hover_content = generate_module_hover_content(declaration)
167
+ hover_id = emit_vertex('hoverResult', {
168
+ contents: [{
169
+ language: 'ruby',
170
+ value: hover_content
171
+ }]
172
+ })
173
+ emit_edge('textDocument/hover', result_set_id, hover_id)
174
+
175
+ declaration[:range_id] = range_id
176
+ declaration[:result_set_id] = result_set_id
177
+ declaration[:document_id] = current_doc_id
178
+ declaration[:file] = @current_uri
179
+
180
+ GlobalState.instance.add_module(qualified_name, declaration)
181
+
182
+ result_set_id
89
183
  end
90
184
 
91
- def register_declaration(declaration)
185
+ def register_constant_declaration(declaration)
92
186
  return unless @current_uri && declaration[:node]
93
187
 
94
188
  qualified_name = declaration[:name]
@@ -96,15 +190,7 @@ module Hind
96
190
  setup_document if @document_id.nil?
97
191
  current_doc_id = @document_id
98
192
 
99
- range_id = if declaration[:type] == :constant
100
- create_range(declaration[:node].name_loc)
101
- elsif declaration[:type] == :module
102
- create_range(declaration[:node].constant_path.location)
103
- elsif declaration[:type] == :class
104
- create_range(declaration[:node].constant_path.location)
105
- else
106
- create_range(declaration[:node].location)
107
- end
193
+ range_id = create_range(declaration[:node].name_loc)
108
194
  return unless range_id
109
195
 
110
196
  result_set_id = emit_vertex('resultSet')
@@ -115,7 +201,7 @@ module Hind
115
201
 
116
202
  emit_edge('item', def_result_id, [range_id], 'definitions', current_doc_id)
117
203
 
118
- hover_content = generate_hover_content(declaration)
204
+ hover_content = generate_constant_hover_content(declaration)
119
205
  hover_id = emit_vertex('hoverResult', {
120
206
  contents: [{
121
207
  language: 'ruby',
@@ -124,21 +210,19 @@ module Hind
124
210
  })
125
211
  emit_edge('textDocument/hover', result_set_id, hover_id)
126
212
 
127
- @global_state.add_declaration(qualified_name, {
128
- type: declaration[:type],
129
- scope: declaration[:scope],
130
- file: @current_uri,
131
- range_id: range_id,
132
- result_set_id: result_set_id,
133
- document_id: current_doc_id
134
- }.merge(declaration))
213
+ declaration[:range_id] = range_id
214
+ declaration[:result_set_id] = result_set_id
215
+ declaration[:document_id] = current_doc_id
216
+ declaration[:file] = @current_uri
217
+
218
+ GlobalState.instance.add_constant(qualified_name, declaration)
135
219
 
136
220
  result_set_id
137
221
  end
138
222
 
139
223
  def register_reference(reference)
140
224
  return unless @current_uri && reference[:node]
141
- return unless @global_state.has_declaration?(reference[:name])
225
+ return unless GlobalState.instance.has_declaration?(reference[:name])
142
226
 
143
227
  setup_document if @document_id.nil?
144
228
  current_doc_id = @document_id
@@ -146,10 +230,11 @@ module Hind
146
230
  range_id = create_range(reference[:node].location)
147
231
  return unless range_id
148
232
 
149
- declaration = @global_state.declarations[reference[:name]]
150
- return unless declaration[:result_set_id]
233
+ # Get the primary declaration for this reference
234
+ declaration = GlobalState.instance.get_declaration(reference[:name])
235
+ return unless declaration && declaration[:result_set_id]
151
236
 
152
- @global_state.add_reference(reference[:name], @current_uri, range_id, current_doc_id)
237
+ GlobalState.instance.add_reference(reference[:name], @current_uri, range_id, current_doc_id)
153
238
  emit_edge('next', range_id, declaration[:result_set_id])
154
239
 
155
240
  reference_result = emit_vertex('referenceResult')
@@ -160,7 +245,7 @@ module Hind
160
245
  private
161
246
 
162
247
  def initialize_project
163
- emit_vertex('metaData', {
248
+ metadata_vertex = emit_vertex('metaData', {
164
249
  version: LSIF_VERSION,
165
250
  projectRoot: path_to_uri(@metadata[:projectRoot]),
166
251
  positionEncoding: 'utf-16',
@@ -170,11 +255,18 @@ module Hind
170
255
  }
171
256
  })
172
257
 
173
- @global_state.project_id = emit_vertex('project', {kind: 'ruby'})
258
+ project_id = emit_vertex('project', {kind: 'ruby'})
259
+ GlobalState.instance.project_id = project_id
260
+
261
+ # Store initial data separately
262
+ @initial_data = @lsif_data.dup
174
263
  end
175
264
 
176
265
  def setup_document
177
- return if @document_id
266
+ if @document_ids[@current_uri]
267
+ @document_id = @document_ids[@current_uri]
268
+ return
269
+ end
178
270
  return unless @current_uri
179
271
 
180
272
  file_path = File.join(@metadata[:projectRoot], @current_uri)
@@ -187,13 +279,13 @@ module Hind
187
279
  @document_ids[@current_uri] = @document_id
188
280
  @current_document_id = @document_id
189
281
 
190
- emit_edge('contains', @global_state.project_id, [@document_id]) if @global_state.project_id
282
+ emit_edge('contains', GlobalState.instance.project_id, [@document_id]) if GlobalState.instance.project_id
191
283
  end
192
284
 
193
285
  def finalize_document_state
194
286
  return unless @current_uri && @document_id
195
287
 
196
- ranges = @global_state.get_ranges_for_file(@current_uri)
288
+ ranges = GlobalState.instance.get_ranges_for_file(@current_uri)
197
289
  if ranges&.any?
198
290
  emit_edge('contains', @document_id, ranges, nil, @document_id)
199
291
  end
@@ -213,7 +305,7 @@ module Hind
213
305
  }
214
306
  })
215
307
 
216
- @global_state.add_range(@current_uri, range_id)
308
+ GlobalState.instance.add_range(@current_uri, range_id)
217
309
  range_id
218
310
  end
219
311
 
@@ -263,20 +355,19 @@ module Hind
263
355
  @vertex_id - 1
264
356
  end
265
357
 
266
- def generate_hover_content(declaration)
267
- case declaration[:type]
268
- when :class
269
- hover = ["class #{declaration[:name]}"]
270
- hover << " < #{declaration[:superclass]}" if declaration[:superclass]
271
- hover.join
272
- when :module
273
- "module #{declaration[:name]}"
274
- when :constant
275
- value_info = declaration[:node].value.respond_to?(:content) ? " = #{declaration[:node].value.content}" : ''
276
- "#{declaration[:name]}#{value_info}"
277
- else
278
- declaration[:name].to_s
279
- end
358
+ def generate_class_hover_content(declaration)
359
+ hover = ["class #{declaration[:name]}"]
360
+ hover << " < #{declaration[:superclass]}" if declaration[:superclass]
361
+ hover.join
362
+ end
363
+
364
+ def generate_module_hover_content(declaration)
365
+ "module #{declaration[:name]}"
366
+ end
367
+
368
+ def generate_constant_hover_content(declaration)
369
+ value_info = declaration[:node].value.respond_to?(:content) ? " = #{declaration[:node].value.content}" : ''
370
+ "#{declaration[:name]}#{value_info}"
280
371
  end
281
372
 
282
373
  def format_hover_data(data)