neoclide/coc.nvim · error

`range` cannot span multiple lines

Error message

`range` cannot span multiple lines

What it means

The semantic tokens protocol encodes tokens as single-line deltas, so every token range must start and end on the same line. _push validates range.start.line === range.end.line and throws when a range spans multiple lines.

Source

Thrown at src/model/semanticTokensBuilder.ts:82

      if (typeof arg4 === 'undefined') {
        arg4 = 0
      }
      // 1st overload
      return this._pushEncoded(arg0, arg1, arg2, arg3, arg4)
    }
    if (Range.is(arg0) && typeof arg1 === 'string' && isStrArrayOrUndefined(arg2)) {
      // 2nd overload
      return this._push(arg0, arg1, arg2)
    }
    throw new Error('Illegal argument')
  }

  private _push(range: Range, tokenType: string, tokenModifiers?: string[]): void {
    if (!this._hasLegend) {
      throw new Error('Legend must be provided in constructor')
    }
    if (range.start.line !== range.end.line) {
      throw new Error('`range` cannot span multiple lines')
    }
    if (!this._tokenTypeStrToInt.has(tokenType)) {
      throw new Error('`tokenType` is not in the provided legend')
    }
    const line = range.start.line
    const char = range.start.character
    const length = range.end.character - range.start.character
    const nTokenType = this._tokenTypeStrToInt.get(tokenType)!
    let nTokenModifiers = 0
    if (tokenModifiers) {
      for (const tokenModifier of tokenModifiers) {
        if (!this._tokenModifierStrToInt.has(tokenModifier)) {
          throw new Error('`tokenModifier` is not in the provided legend')
        }
        const nTokenModifier = this._tokenModifierStrToInt.get(tokenModifier)!
        nTokenModifiers |= (1 << nTokenModifier) >>> 0
      }
    }

View on GitHub (pinned to 50e974d969)

Solutions

  1. Split multi-line tokens into one token per line before pushing
  2. Clamp the range to a single line: use range.start.line for both endpoints with the length limited to that line's content
  3. Fix the token producer (grammar/matcher) so matches never cross lines

Example fix

// before
builder.push(new Range(start, endMultiLine), 'comment')
// after
for (let line = start.line; line <= end.line; line++) {
  const lineEnd = line === end.line ? end : doc.lineAt(line).range.end
  const lineStart = line === start.line ? start : new Position(line, 0)
  if (!lineStart.isEqual(lineEnd)) builder.push(new Range(lineStart, lineEnd), 'comment')
}
Defensive patterns

Strategy: validation

Validate before calling

if (range.start.line !== range.end.line) {
  // split into per-line ranges before pushing
}

Type guard

function isSingleLine(r: Range): boolean {
  return r.start.line === r.end.line
}

Try / catch

try {
  builder.push(range, tokenType)
} catch (e) {
  if (e.message.includes('cannot span multiple lines')) {
    splitAndPushPerLine(range, tokenType, builder)
  }
}

Prevention

When it happens

Trigger: Calling push() with a Range whose start.line differs from end.line — e.g. tokenizing a multiline comment/string/region as one token instead of splitting it per line.

Common situations: Textmate/Tree-sitter grammars that yield multiline captures; highlighting block comments or heredocs; incorrectly computing a token range from a multi-line match.

Related errors


AI-assisted analysis of neoclide/coc.nvim@50e974d969 (2026-08-31). Data as JSON: /api/errors/22b8e56fadc74ae1. Report an issue: GitHub.