Dealing with LSP Encoding

Microsoft's Language Server Protocol requires UTF-8 encoding for your text, but by default it wants you to measure offsets and positions using UTF-16 "code units", which is a different encoding system. This mismatch makes it difficult to comply with the specification. (Unless you happen to be writing in Java or JS or C#, which all match the strange UTF-16 "code unit" behavior by default)

How to opt out of the Code Units

Your first goal should be to try to negotiate for UTF-8 byte offsets instead of UTF-16 code units.

The LSP capability negotiation works like this:

  1. Client sends `initialize` request: The client lists which position encodings it supports, in order of preference. To negotiate UTF-8 as a client, put utf-8 first:
  1. Server responds: The server should pick utf-8 from the client's list if it supports it:
  1. Both sides use the agreed encoding: Once negotiated, all position and offset measurements in the protocol use this encoding for the rest of the session.

When the server chooses utf-8, both sides can measure positions in plain byte offsets.

How to deal with the Code Units

If the above negotiation fails, then you have to deal with UTF-16 code units.

A code point is Unicode's term for a single character in its character set. UTF-8 uses between 1 and 4 bytes per code point:

Code pointaα𝕒
# of bytes1234
# of codepoints1111
# of UTF-16 code units1112

You can see that UTF-16 code units are different from counting codepoints. Your Unicode library probably supports codepoints, but for LSP compliance, you need code units.

Instead of incorrectly using codepoints from a library, I recommend computing the code unit offsets yourself. This involves iterating over your string and counting a running total.

One way to count the number of UTF-16 code units is to iterate through each code point in your string and refer to the table above, counting each code point as +1, except 4-byte code points, which are +2.

Byte-level counting trick

Another approach is to iterate over raw bytes instead of code points. In some languages, iterating over bytes is simpler or faster than decoding UTF-8 code points, especially if your standard library doesn't provide good UTF-8 iteration.

The key insight is that UTF-8's byte structure tells us how many bytes are in each code point:

Since UTF-16 code units count 1-3 byte code points as 1 unit and 4-byte code points as 2 units, we can count as we iterate over bytes:

If you're doing this byte trick, the continuation byte being +1 is important: this ensures that when you reach a target offset, you're positioned at the end of a code point, not in the middle of one.

See also