Skip to content

Commit 1aedffb

Browse files
ahogappaclaude
authored andcommitted
Negotiate position encoding in LSP Initialize (LSP 3.17)
Implements `general.positionEncodings` negotiation per LSP 3.17 spec. The server picks the first encoding from the client's preference list that it supports (`utf-8` / `utf-16` / `utf-32`) and reports it back via `capabilities.positionEncoding`. Falls back to UTF-16 (mandatory per spec) if the client doesn't propose any supported encoding. The negotiated value flows into per-workspace Services through `core_options.merge(position_encoding: ...)`, so each Service computes column positions in the agreed encoding. See: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocuments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7cbe399 commit 1aedffb

3 files changed

Lines changed: 72 additions & 4 deletions

File tree

lib/typeprof/lsp/messages.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ def run
5656
class Message::Initialize < Message
5757
METHOD = "initialize" # request (required)
5858
def run
59+
# Must negotiate encoding before add_workspaces so newly created Services honor it.
60+
client_encodings = @params.dig(:capabilities, :general, :positionEncodings)
61+
@server.negotiate_position_encoding(client_encodings)
62+
5963
folders = @params[:workspaceFolders].map do |folder|
6064
folder => { uri:, }
6165
@server.uri_to_path(uri)
@@ -65,6 +69,7 @@ def run
6569

6670
respond(
6771
capabilities: {
72+
positionEncoding: @server.lsp_position_encoding,
6873
textDocumentSync: {
6974
openClose: true,
7075
change: 2, # Incremental

lib/typeprof/lsp/server.rb

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ def self.start_socket(core_options, port = 0)
4747
end
4848
end
4949

50+
# see: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocuments
51+
LSP_POSITION_ENCODINGS = {
52+
Encoding::UTF_8 => "utf-8",
53+
Encoding::UTF_16LE => "utf-16",
54+
Encoding::UTF_32LE => "utf-32",
55+
}.freeze
56+
57+
private_constant :LSP_POSITION_ENCODINGS
58+
5059
def initialize(core_options, reader, writer, url_schema: nil)
5160
@core_options = core_options
5261
@cores = {}
@@ -62,9 +71,29 @@ def initialize(core_options, reader, writer, url_schema: nil)
6271
@diagnostic_severity = :error
6372
end
6473

65-
attr_reader :open_texts
74+
attr_reader :open_texts, :position_encoding
6675
attr_accessor :signature_enabled
6776

77+
# Pick the first mutually-supported encoding from the client's preference-ordered list
78+
# and store it. Falls back to UTF-16LE (mandatory per LSP 3.17 spec).
79+
def negotiate_position_encoding(client_encodings)
80+
@position_encoding = pick_position_encoding(client_encodings)
81+
end
82+
83+
def lsp_position_encoding
84+
LSP_POSITION_ENCODINGS.fetch(@position_encoding)
85+
end
86+
87+
def pick_position_encoding(client_encodings)
88+
return Encoding::UTF_16LE unless client_encodings.is_a?(Array)
89+
client_encodings.each do |enc|
90+
encoding = LSP_POSITION_ENCODINGS.key(enc)
91+
return encoding if encoding
92+
end
93+
Encoding::UTF_16LE
94+
end
95+
private :pick_position_encoding
96+
6897
#: (String) -> String
6998
def path_to_uri(path)
7099
@url_schema + File.expand_path(path).split("/").map {|s| CGI.escapeURIComponent(s) }.join("/")
@@ -105,9 +134,10 @@ def add_workspaces(folders)
105134
end
106135
end
107136
@core_options[:exclude_patterns] = conf[:exclude] if conf[:exclude]
137+
service_options = @core_options.merge(position_encoding: @position_encoding)
108138
conf[:analysis_unit_dirs].each do |dir|
109139
dir = File.expand_path(dir, path)
110-
core = @cores[dir] = TypeProf::Core::Service.new(@core_options)
140+
core = @cores[dir] = TypeProf::Core::Service.new(service_options)
111141
core.add_workspace(dir, @rbs_dir)
112142
end
113143
else

test/lsp/lsp_test.rb

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@ def setup
2828
@id = 0
2929
end
3030

31-
def init(fixture)
31+
def init(fixture, position_encodings: nil, expected_position_encoding: "utf-16")
3232
@folder = @lsp.path_to_uri(File.expand_path(File.join(__dir__, "..", "fixtures", fixture))) + "/"
33-
id = request("initialize", workspaceFolders: [{ uri: @folder }])
33+
params = { workspaceFolders: [{ uri: @folder }] }
34+
params[:capabilities] = { general: { positionEncodings: position_encodings } } if position_encodings
35+
id = request("initialize", **params)
3436
expect_response(id) do |recv|
3537
assert_equal({ name: "typeprof", version: TypeProf::VERSION }, recv[:serverInfo])
38+
assert_equal(expected_position_encoding, recv[:capabilities][:positionEncoding])
3639
end
3740
notify("initialized")
3841
end
@@ -405,6 +408,36 @@ class Foo
405408
end
406409
end
407410

411+
def test_position_encoding_default
412+
init("basic")
413+
assert_equal(Encoding::UTF_16LE, @lsp.position_encoding)
414+
end
415+
416+
def test_position_encoding_utf8_preferred
417+
init("basic", position_encodings: ["utf-8", "utf-16"], expected_position_encoding: "utf-8")
418+
assert_equal(Encoding::UTF_8, @lsp.position_encoding)
419+
end
420+
421+
def test_position_encoding_empty_array
422+
init("basic", position_encodings: [], expected_position_encoding: "utf-16")
423+
assert_equal(Encoding::UTF_16LE, @lsp.position_encoding)
424+
end
425+
426+
def test_position_encoding_unsupported_only
427+
init("basic", position_encodings: ["ascii"], expected_position_encoding: "utf-16")
428+
assert_equal(Encoding::UTF_16LE, @lsp.position_encoding)
429+
end
430+
431+
def test_position_encoding_prefers_first_supported
432+
init("basic", position_encodings: ["ascii", "utf-16", "utf-8"], expected_position_encoding: "utf-16")
433+
assert_equal(Encoding::UTF_16LE, @lsp.position_encoding)
434+
end
435+
436+
def test_position_encoding_utf32_preferred
437+
init("basic", position_encodings: ["utf-32", "utf-16"], expected_position_encoding: "utf-32")
438+
assert_equal(Encoding::UTF_32LE, @lsp.position_encoding)
439+
end
440+
408441
def test_type_definition_for_local_variable
409442
init("type_definition")
410443

0 commit comments

Comments
 (0)