Skip to content

[FEATURE]: typescript-go LSP first-party support #12522

Closed as not planned
Closed as not planned

Description

@rubnogueira

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Since #67 was closed, I'm opening a new issue.

The feature request is for Opencode support for tsgo as a first-party LSP.

PR: #12492

Activity

  1. added
    discussionUsed for feature requests, proposals, ideas, etc. Open discussion
    on Feb 6, 2026
  2. github-actions commented on Feb 6, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    Feel free to ignore if this addresses your specific case.

  3. LeonMueller-OneAndOnly commented on Mar 10, 2026

    @LeonMueller-OneAndOnly
    Contributor

    Edit after publish:

    The proposed proxy here works, its the right idea, but the implementation is quite wasteful cpu + ram wise. I created a new version that solves this here:
    https://github.com/LeonMueller-OneAndOnly/opencode-tsgo-lsp-proxy


    Original comment:

    One workaround I am currently using is following:

    opencode.json

    {
      "$schema": "https://opencode.ai/config.json",
    
      "lsp": {
        "typescript": {
          "command": ["node", "scripts/opencode-tsgo-lsp-proxy.cjs"],
          "extensions": [".cjs", ".mjs", ".js", ".jsx", ".ts", ".tsx", ".mts", ".cts"]
        }
      }
    }

    scripts/opencode-tsgo-lsp-proxy.cjs

    #!/usr/bin/env node
    
    const { spawn } = require("child_process")
    const fs = require("fs")
    const path = require("path")
    
    const tsgoBin = path.resolve(process.cwd(), "node_modules/.bin/tsgo")
    const tsgoCommand = fs.existsSync(tsgoBin) ? tsgoBin : "tsgo"
    
    const server = spawn(tsgoCommand, ["--lsp", "--stdio"], {
      stdio: ["pipe", "pipe", "pipe"],
      env: process.env,
    })
    
    let nextRequestId = 900000000
    const proxyDiagnosticRequestById = new Map()
    const latestResultIdByUri = new Map()
    const inFlightByUri = new Map()
    const rerunByUri = new Set()
    const debounceTimerByUri = new Map()
    let supportsTextDocumentDiagnostic = true
    
    const writeFrame = (stream, message) => {
      const body = JSON.stringify(message)
      stream.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`)
    }
    
    const forwardToClient = (message) => writeFrame(process.stdout, message)
    const forwardToServer = (message) => writeFrame(server.stdin, message)
    
    const scheduleDiagnostic = (uri) => {
      if (!uri || !supportsTextDocumentDiagnostic) return
      const existingTimer = debounceTimerByUri.get(uri)
      if (existingTimer) clearTimeout(existingTimer)
      const timer = setTimeout(() => {
        debounceTimerByUri.delete(uri)
        requestDiagnostic(uri)
      }, 120)
      debounceTimerByUri.set(uri, timer)
    }
    
    const requestDiagnostic = (uri) => {
      if (!supportsTextDocumentDiagnostic) return
      if (inFlightByUri.has(uri)) {
        rerunByUri.add(uri)
        return
      }
    
      const id = nextRequestId++
      inFlightByUri.set(uri, id)
      proxyDiagnosticRequestById.set(id, uri)
    
      const previousResultId = latestResultIdByUri.get(uri)
      const params = {
        textDocument: { uri },
      }
    
      if (typeof previousResultId === "string" && previousResultId.length > 0) {
        params.previousResultId = previousResultId
      }
    
      forwardToServer({
        jsonrpc: "2.0",
        id,
        method: "textDocument/diagnostic",
        params,
      })
    }
    
    const handleProxyDiagnosticResponse = (responseId, result, error) => {
      const uri = proxyDiagnosticRequestById.get(responseId)
      if (!uri) return false
    
      proxyDiagnosticRequestById.delete(responseId)
      inFlightByUri.delete(uri)
    
      if (error) {
        supportsTextDocumentDiagnostic = false
      } else if (result && typeof result === "object") {
        if (typeof result.resultId === "string") {
          latestResultIdByUri.set(uri, result.resultId)
        }
        if (result.kind === "full" && Array.isArray(result.items)) {
          forwardToClient({
            jsonrpc: "2.0",
            method: "textDocument/publishDiagnostics",
            params: {
              uri,
              diagnostics: result.items,
            },
          })
        }
      }
    
      if (rerunByUri.has(uri)) {
        rerunByUri.delete(uri)
        requestDiagnostic(uri)
      }
    
      return true
    }
    
    const onClientMessage = (message) => {
      forwardToServer(message)
    
      if (!message || typeof message !== "object" || typeof message.method !== "string") return
    
      const method = message.method
    
      if (method === "textDocument/didClose") {
        const uri = message.params?.textDocument?.uri
        if (uri) {
          const timer = debounceTimerByUri.get(uri)
          if (timer) {
            clearTimeout(timer)
            debounceTimerByUri.delete(uri)
          }
          latestResultIdByUri.delete(uri)
          inFlightByUri.delete(uri)
          rerunByUri.delete(uri)
          forwardToClient({
            jsonrpc: "2.0",
            method: "textDocument/publishDiagnostics",
            params: {
              uri,
              diagnostics: [],
            },
          })
        }
        return
      }
    
      if (
        method === "textDocument/didOpen" ||
        method === "textDocument/didChange" ||
        method === "textDocument/didSave"
      ) {
        const uri = message.params?.textDocument?.uri
        requestDiagnostic(uri)
        scheduleDiagnostic(uri)
      }
    }
    
    const onServerMessage = (message) => {
      if (message && typeof message === "object" && typeof message.id === "number") {
        if (handleProxyDiagnosticResponse(message.id, message.result, message.error)) return
      }
      forwardToClient(message)
    }
    
    const createParser = (onMessage) => {
      let buffer = Buffer.alloc(0)
    
      return (chunk) => {
        buffer = Buffer.concat([buffer, chunk])
    
        for (;;) {
          const headerEnd = buffer.indexOf("\r\n\r\n")
          if (headerEnd === -1) return
    
          const headerText = buffer.slice(0, headerEnd).toString("utf8")
          const lengthMatch = /Content-Length:\s*(\d+)/i.exec(headerText)
          if (!lengthMatch) {
            buffer = buffer.slice(headerEnd + 4)
            continue
          }
    
          const bodyLength = Number(lengthMatch[1])
          const bodyStart = headerEnd + 4
          if (buffer.length < bodyStart + bodyLength) return
    
          const body = buffer.slice(bodyStart, bodyStart + bodyLength).toString("utf8")
          buffer = buffer.slice(bodyStart + bodyLength)
    
          let parsed
          try {
            parsed = JSON.parse(body)
          } catch {
            continue
          }
    
          onMessage(parsed)
        }
      }
    }
    
    const parseClient = createParser(onClientMessage)
    const parseServer = createParser(onServerMessage)
    
    process.stdin.on("data", parseClient)
    server.stdout.on("data", parseServer)
    
    server.stderr.on("data", (chunk) => {
      process.stderr.write(chunk)
    })
    
    const teardown = (code) => {
      for (const timer of debounceTimerByUri.values()) clearTimeout(timer)
      debounceTimerByUri.clear()
      if (!server.killed) server.kill("SIGTERM")
      process.exit(code)
    }
    
    server.on("exit", (code, signal) => {
      if (signal) {
        process.exit(1)
        return
      }
      process.exit(code ?? 0)
    })
    
    process.on("SIGINT", () => teardown(130))
    process.on("SIGTERM", () => teardown(143))
  4. assigned and unassigned on Apr 26, 2026
  5. removed
    discussionUsed for feature requests, proposals, ideas, etc. Open discussion
    on May 3, 2026
  6. github-actions commented on Aug 2, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions