#18560 · @superherointj · opened Mar 21, 2026 at 10:02 PM UTC · last updated Mar 21, 2026 at 10:02 PM UTC

fix(lsp): resolve extension matching for Dockerfile files

appfix
65
+7542 files

Score breakdown

Impact

7.0

Clarity

9.0

Urgency

6.0

Ease Of Review

9.0

Guidelines

9.0

Readiness

8.0

Size

7.0

Trust

5.0

Traction

2.0

Summary

This PR fixes LSP extension matching for Dockerfiles, specifically addressing case sensitivity and files without extensions. It introduces a new matchesExtension function with comprehensive unit tests and manual verification.

Open in GitHub

Description

Issue for this PR

Closes #18558

Type of change

  • [x] Bug fix

What does this PR do?

Fixes LSP extension matching for files named exactly Dockerfile (without a file extension) and adds case-insensitive extension matching.

The old code used case-sensitive Array.includes() which failed for:

  • Dockerfile (no extension, basename fallback didn't match due to case)
  • file.Dockerfile (case mismatch with .dockerfile extension)

The new matchesExtension function handles:

  1. Case-insensitive extension matching (.Dockerfile matches .dockerfile)
  2. Files without extensions matching basenames (Dockerfile matches Dockerfile extension)

How did you verify your code works?

  • Added 8 unit tests covering various edge cases
  • All tests pass
  • Manually tested with actual Dockerfile files:
    • Dockerfile - LSP spawns and initializes correctly
    • file.dockerfile - continues to work
    • something.Dockerfile - case-insensitive matching works

Checklist

  • [x] I have tested my changes locally
  • [x] I have not included unrelated changes in this PR

Linked Issues

#18558 fix(lsp): resolve extension matching for Dockerfile files

View issue

Comments

No comments.

Changed Files

packages/opencode/src/lsp/index.ts

+224
@@ -175,9 +175,28 @@ export namespace LSP {
})
}
export const matchesExtension = (file: string, extensions: string[]): boolean => {
if (extensions.length === 0) return true
const parsed = path.parse(file)
const ext = parsed.ext
const basename = parsed.base
// Check if extension matches (case-insensitive)
if (ext && extensions.some((e) => e.toLowerCase() === ext.toLowerCase())) {
return true
}
// For files with no extension, check if basename matches any extension
// This handles cases like "Dockerfile" matching an extension of "Dockerfile"
if (!ext && extensions.some((e) => e.toLowerCase() === basename.toLowerCase())) {
return true
}
return false
}
async function getClients(file: string) {
const s = await state()
const extension = path.parse(file).ext || file
const result: LSPClient.Info[] = []
async function schedule(server: LSPServer.Info, root: string, key: string) {
@@ -222,7 +241,7 @@ export namespace LSP {
}
for (const server of Object.values(s.servers)) {
if (server.extensions.length && !server.extensions.includes(extension)) continue

packages/opencode/test/lsp/matches-extension.test.ts

+530
@@ -0,0 +1,53 @@
import { describe, expect, test } from "bun:test"
import { LSP } from "../../src/lsp/index"
describe("LSP.matchesExtension", () => {
describe("files with extensions", () => {
test("matches exact extension", () => {
expect(LSP.matchesExtension("file.dockerfile", [".dockerfile", "Dockerfile"])).toBe(true)
expect(LSP.matchesExtension("file.sh", [".sh", ".bash"])).toBe(true)
})
test("matches case-insensitive extension", () => {
expect(LSP.matchesExtension("file.Dockerfile", [".dockerfile", "Dockerfile"])).toBe(true)
expect(LSP.matchesExtension("file.DOCKERFILE", [".dockerfile"])).toBe(true)
expect(LSP.matchesExtension("file.dockerfile", [".Dockerfile"])).toBe(true)
})
test("does not match different extension", () => {
expect(LSP.matchesExtension("file.txt", [".dockerfile"])).toBe(false)
expect(LSP.matchesExtension("file.sh", [".dockerfile"])).toBe(false)
})
})
describe("files without extensions (like Dockerfile)", () => {
test("matches basename exactly", () => {
expect(LSP.matchesExtension("Dockerfile", [".dockerfile", "Dockerfile"])).toBe(true)
expect(