klenod-lsp 0.0.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 +7 -0
- data/README.md +114 -0
- data/lib/klenod/lsp/analysis.rb +29 -0
- data/lib/klenod/lsp/cli.rb +33 -0
- data/lib/klenod/lsp/diagnostics.rb +110 -0
- data/lib/klenod/lsp/documents.rb +72 -0
- data/lib/klenod/lsp/graph_index.rb +193 -0
- data/lib/klenod/lsp/languages/css.rb +45 -0
- data/lib/klenod/lsp/languages/haml/classes.rb +202 -0
- data/lib/klenod/lsp/languages/haml/completion.rb +60 -0
- data/lib/klenod/lsp/languages/haml/props.rb +111 -0
- data/lib/klenod/lsp/languages/haml/rename.rb +42 -0
- data/lib/klenod/lsp/languages/haml/ruby_regions.rb +85 -0
- data/lib/klenod/lsp/languages/haml.rb +87 -0
- data/lib/klenod/lsp/languages/imports.rb +428 -0
- data/lib/klenod/lsp/languages/ruby.rb +41 -0
- data/lib/klenod/lsp/languages/syntax.rb +64 -0
- data/lib/klenod/lsp/languages.rb +38 -0
- data/lib/klenod/lsp/renames.rb +141 -0
- data/lib/klenod/lsp/routes.rb +105 -0
- data/lib/klenod/lsp/server.rb +568 -0
- data/lib/klenod/lsp/symbols.rb +177 -0
- data/lib/klenod/lsp/text.rb +78 -0
- data/lib/klenod/lsp/version.rb +7 -0
- data/lib/klenod/lsp/workspace.rb +155 -0
- data/lib/klenod/lsp.rb +9 -0
- metadata +96 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "language_server-protocol"
|
|
4
|
+
|
|
5
|
+
require "klenod/build/module_id"
|
|
6
|
+
|
|
7
|
+
require_relative "diagnostics"
|
|
8
|
+
require_relative "languages"
|
|
9
|
+
|
|
10
|
+
module Klenod
|
|
11
|
+
module LSP
|
|
12
|
+
# Workspace edits that keep imports valid when the editor renames or
|
|
13
|
+
# moves files or folders under the source directory.
|
|
14
|
+
#
|
|
15
|
+
# Importers get their literals rewritten to the new location, and a
|
|
16
|
+
# moved module gets its own relative imports rewritten from its new
|
|
17
|
+
# directory. Each literal keeps its style: root-relative stays
|
|
18
|
+
# root-relative, `./` stays `./`, bare stays bare, and an extension is
|
|
19
|
+
# kept only when the literal had one. Edits are keyed by the files' old
|
|
20
|
+
# URIs because the client applies them before renaming.
|
|
21
|
+
module Renames
|
|
22
|
+
Interface = LanguageServer::Protocol::Interface
|
|
23
|
+
|
|
24
|
+
Move = Data.define(:old_path, :new_path) do
|
|
25
|
+
def apply(path)
|
|
26
|
+
return new_path if path == old_path
|
|
27
|
+
return path unless path.start_with?("#{old_path}/")
|
|
28
|
+
|
|
29
|
+
new_path + path.delete_prefix(old_path)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
def call(files, index, workspace)
|
|
36
|
+
moves = files.filter_map do |old_uri, new_uri|
|
|
37
|
+
old_path = workspace.path_for_uri(old_uri)
|
|
38
|
+
new_path = workspace.path_for_uri(new_uri)
|
|
39
|
+
Move.new(old_path, new_path) if old_path && new_path
|
|
40
|
+
end
|
|
41
|
+
return nil if moves.empty?
|
|
42
|
+
|
|
43
|
+
changes = Hash.new { |hash, uri| hash[uri] = [] }
|
|
44
|
+
moves.each do |move|
|
|
45
|
+
moved_module_ids(move, index, workspace).each do |module_id|
|
|
46
|
+
index.ensure_collected(module_id)
|
|
47
|
+
importer_edits(module_id, moves, index, workspace, changes)
|
|
48
|
+
own_edits(module_id, moves, index, workspace, changes)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
return nil if changes.empty?
|
|
52
|
+
|
|
53
|
+
sorted = changes.transform_values { |edits| edits.sort_by { |edit| [edit.range.start.line, edit.range.start.character] } }
|
|
54
|
+
Interface::WorkspaceEdit.new(changes: sorted)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def moved_module_ids(move, index, workspace)
|
|
58
|
+
if File.directory?(move.old_path)
|
|
59
|
+
index.records.keys.select do |module_id|
|
|
60
|
+
path = workspace.path_for_module_id(module_id)
|
|
61
|
+
path&.start_with?("#{move.old_path}/")
|
|
62
|
+
end
|
|
63
|
+
else
|
|
64
|
+
module_id = workspace.module_id_for_path(move.old_path)
|
|
65
|
+
module_id ? [module_id] : []
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def importer_edits(module_id, moves, index, workspace, changes)
|
|
70
|
+
new_target_path = apply_moves(workspace.path_for_module_id(module_id), moves)
|
|
71
|
+
|
|
72
|
+
index.dependents(module_id).each do |importer_id|
|
|
73
|
+
record = index.record(importer_id)
|
|
74
|
+
importer_path = record && workspace.path_for_module_id(importer_id)
|
|
75
|
+
next unless importer_path
|
|
76
|
+
|
|
77
|
+
lines = record.source.lines(chomp: true)
|
|
78
|
+
importer_new_path = apply_moves(importer_path, moves)
|
|
79
|
+
record.resolved_dependencies.each do |resolved|
|
|
80
|
+
next unless resolved.module_id == module_id
|
|
81
|
+
|
|
82
|
+
add_edit(changes, workspace.uri_for_path(importer_path), lines, resolved.dependency.specifier.to_s, importer_new_path, new_target_path, workspace)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def own_edits(module_id, moves, index, workspace, changes)
|
|
88
|
+
record = index.record(module_id)
|
|
89
|
+
path = record && workspace.path_for_module_id(module_id)
|
|
90
|
+
return unless path
|
|
91
|
+
|
|
92
|
+
lines = record.source.lines(chomp: true)
|
|
93
|
+
new_path = apply_moves(path, moves)
|
|
94
|
+
record.resolved_dependencies.each do |resolved|
|
|
95
|
+
specifier = resolved.dependency.specifier.to_s
|
|
96
|
+
next if specifier.start_with?("/") || specifier.match?(Klenod::Build::ModuleId::SCHEME_PATTERN)
|
|
97
|
+
|
|
98
|
+
target_path = workspace.path_for_module_id(resolved.module_id)
|
|
99
|
+
next unless target_path
|
|
100
|
+
|
|
101
|
+
add_edit(changes, workspace.uri_for_path(path), lines, specifier, new_path, apply_moves(target_path, moves), workspace)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def add_edit(changes, uri, lines, specifier, importer_path, target_path, workspace)
|
|
106
|
+
replacement = rewrite_specifier(specifier, importer_path, target_path, workspace.source_dir)
|
|
107
|
+
return if replacement == specifier
|
|
108
|
+
|
|
109
|
+
syntax = Languages.syntax_for(File.extname(workspace.path_for_uri(uri).to_s))
|
|
110
|
+
Diagnostics.literal_spans(specifier, lines, syntax: syntax).each do |span|
|
|
111
|
+
next if changes[uri].any? { |edit| edit.range.start.line == span.line && edit.range.start.character == span.start_character }
|
|
112
|
+
|
|
113
|
+
changes[uri] << Interface::TextEdit.new(range: span.to_range, new_text: replacement)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def rewrite_specifier(specifier, importer_path, target_path, source_dir)
|
|
118
|
+
path_part, query = specifier.split("?", 2)
|
|
119
|
+
target = File.extname(path_part).empty? ? target_path.delete_suffix(File.extname(target_path)) : target_path
|
|
120
|
+
|
|
121
|
+
rewritten =
|
|
122
|
+
if path_part.start_with?("/")
|
|
123
|
+
"/#{Pathname.new(target).relative_path_from(Pathname.new(source_dir))}"
|
|
124
|
+
else
|
|
125
|
+
relative = Pathname.new(target).relative_path_from(Pathname.new(File.dirname(importer_path))).to_s
|
|
126
|
+
if path_part.start_with?("./")
|
|
127
|
+
relative.start_with?("../") ? relative : "./#{relative}"
|
|
128
|
+
else
|
|
129
|
+
relative
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
query ? "#{rewritten}?#{query}" : rewritten
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def apply_moves(path, moves)
|
|
137
|
+
moves.reduce(path) { |current, move| move.apply(current) }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "klenod/build/plugins/router_plugin"
|
|
4
|
+
|
|
5
|
+
module Klenod
|
|
6
|
+
module LSP
|
|
7
|
+
# What the router plugin knows about a module: the route a page or
|
|
8
|
+
# handler serves, the routes a layout wraps, or the special view it is.
|
|
9
|
+
# The manifest is discovered from the pages directory and kept until
|
|
10
|
+
# files change.
|
|
11
|
+
class Routes
|
|
12
|
+
def initialize(workspace)
|
|
13
|
+
@workspace = workspace
|
|
14
|
+
@manifest = nil
|
|
15
|
+
@discovered = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def invalidate
|
|
19
|
+
@manifest = nil
|
|
20
|
+
@discovered = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def descriptions(module_id)
|
|
24
|
+
manifest = self.manifest
|
|
25
|
+
return [] unless manifest
|
|
26
|
+
|
|
27
|
+
id = module_id.to_s
|
|
28
|
+
descriptions = []
|
|
29
|
+
manifest.routes.each do |route|
|
|
30
|
+
descriptions << "Route #{route.path}" if [route.page_module_id, route.handler_module_id].compact.any? { |candidate| candidate.to_s == id }
|
|
31
|
+
end
|
|
32
|
+
wrapped = manifest.routes.count { |route| [*route.layout_module_ids, route.slot_layout_module_id].compact.any? { |candidate| candidate.to_s == id } }
|
|
33
|
+
descriptions << "Layout for #{wrapped} #{(wrapped == 1) ? "route" : "routes"}" if wrapped.positive?
|
|
34
|
+
manifest.special_views.each do |view|
|
|
35
|
+
descriptions << "#{view.kind.to_s.tr("_", " ").capitalize} view for #{view.path}" if view.view_module_id.to_s == id
|
|
36
|
+
end
|
|
37
|
+
descriptions
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Markdown for a hover on a route module: the route and its params,
|
|
41
|
+
# the page and handler files, and the layouts wrapping it; for a layout,
|
|
42
|
+
# the routes it wraps; for a special view, its kind and scope.
|
|
43
|
+
def hover_markdown(module_id)
|
|
44
|
+
manifest = self.manifest
|
|
45
|
+
return nil unless manifest
|
|
46
|
+
|
|
47
|
+
id = module_id.to_s
|
|
48
|
+
sections = []
|
|
49
|
+
manifest.routes.each do |route|
|
|
50
|
+
next unless [route.page_module_id, route.handler_module_id].compact.any? { |candidate| candidate.to_s == id }
|
|
51
|
+
|
|
52
|
+
lines = ["**Route** `#{route.path}` · `#{directory_form(route)}`"]
|
|
53
|
+
params = route.params
|
|
54
|
+
lines << "Params: #{params.map { |param| "`#{param.name}` (#{param.kind})" }.join(", ")}" unless params.empty?
|
|
55
|
+
lines << "Page: `#{route.page_module_id.path}`" if route.page_module_id
|
|
56
|
+
lines << "Handler: `#{route.handler_module_id.path}`" if route.handler_module_id
|
|
57
|
+
lines << layouts_line(route.layout_module_ids)
|
|
58
|
+
sections << lines.compact.join("\n\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
wrapped = manifest.routes.select { |route| [*route.layout_module_ids, route.slot_layout_module_id].compact.any? { |candidate| candidate.to_s == id } }
|
|
62
|
+
unless wrapped.empty?
|
|
63
|
+
listed = wrapped.first(10).map { |route| "`#{route.path}`" }
|
|
64
|
+
listed << "…" if wrapped.length > 10
|
|
65
|
+
sections << "**Layout** for #{wrapped.length} #{(wrapped.length == 1) ? "route" : "routes"}: #{listed.join(", ")}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
manifest.special_views.each do |view|
|
|
69
|
+
next unless view.view_module_id.to_s == id
|
|
70
|
+
|
|
71
|
+
lines = ["**#{view.kind.to_s.tr("_", " ").capitalize} view** for `#{view.path}`"]
|
|
72
|
+
lines << "Status: #{view.status}" if view.status
|
|
73
|
+
lines << layouts_line(view.layout_module_ids)
|
|
74
|
+
sections << lines.compact.join("\n\n")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sections.empty? ? nil : sections.join("\n\n---\n\n")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# The directory spelling of a route, e.g. `pages/docs/[slug]`, next to
|
|
83
|
+
# the URL pattern the router matches, e.g. `/docs/:slug`.
|
|
84
|
+
def directory_form(route)
|
|
85
|
+
File.dirname(route.module_id.path)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def layouts_line(layout_module_ids)
|
|
89
|
+
return nil if layout_module_ids.nil? || layout_module_ids.empty?
|
|
90
|
+
|
|
91
|
+
"Layouts: #{layout_module_ids.map { |layout_id| "`#{layout_id.path}`" }.join(" → ")}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def manifest
|
|
95
|
+
return @manifest if @discovered
|
|
96
|
+
|
|
97
|
+
@discovered = true
|
|
98
|
+
plugin = @workspace.context.graph.plugins.find { |candidate| candidate.is_a?(Klenod::Build::Plugins::RouterPlugin::Plugin) }
|
|
99
|
+
@manifest = plugin&.discover(source_dir: @workspace.source_dir)
|
|
100
|
+
rescue Klenod::Build::Error
|
|
101
|
+
@manifest = nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|