opencode icon indicating copy to clipboard operation
opencode copied to clipboard

feat: Allowing for wildcards in formatter exts

Open OpeOginni opened this issue 2 months ago • 9 comments

feat: Allowing for wildcards in formatter extenstions, utilizing already existing Wildcards utils

Solved issue #3431

Tested with this config and it works

{
    "$schema": "https://opencode.ai/config.json",
    "formatter": {
    "prettier": {
        "extensions": [".ts"],
        "command": ["npx", "prettier", "--write", "$FILE"],
        "enabled": true
      },
      "biome": {
        "extensions": ["*.test.ts"],
        "command": ["npx", "@biomejs/biome", "format", "--write", "$FILE"],
        "enabled": true
      }
    }
}

@rekram1-node I want to ask,

  • If a user mistakely enables two formatters for a single file type what should be the system's response?

OpeOginni avatar Nov 02 '25 14:11 OpeOginni

@OpeOginni I think it should just run both of them in that case.

I was using a python codebase a while ago and we wanted to run pylint and black on any file edit + save, and then both would run and adjust the code

rekram1-node avatar Nov 04 '25 15:11 rekram1-node

Okay cool, the this fix should work. Ill do some more testing and maybe make some docs update as well?

OpeOginni avatar Nov 04 '25 16:11 OpeOginni

@OpeOginni I tested your changes but now formatters don't run in certain cases, ex:

.test.ts won't be formatted.

I don't think this should be fixed by just adding ".test.ts" to the array either.

rekram1-node avatar Nov 04 '25 19:11 rekram1-node

@rekram1-node Hmm thats weird, in this case is it that you didnt explicitly add *.test.ts to the extensions, only adding .ts without a wildcard?

OpeOginni avatar Nov 04 '25 19:11 OpeOginni

@OpeOginni im talking about our defaults in formatter.ts

But this would also apply to any existing users we dont wanna push breaking changes if we can help it.

What do u think makes sense given that constraint?

rekram1-node avatar Nov 04 '25 19:11 rekram1-node

@rekram1-node Maybe, having a system that checks for any explicit extension combination such as *.test.ts if none exists we treat the whole system as previous where a normal .ts would be like using *.ts and would work for all files.

Ig this way only users who want to make use of these specific formatter feature, will be given it, but if a normal user doesnt see the need (not in their config), formatting works as it did before?

OpeOginni avatar Nov 04 '25 19:11 OpeOginni

I guess we could also have some exclusion...

Can we derive the behavior from how editors do this? I'm sure there is an existing UX for best practice if you can point to some examples of how other people do it we should follow one of those "standards"

rekram1-node avatar Nov 04 '25 21:11 rekram1-node

I tried to get a working logic

  async function getFormatter(ext: string, fullExt: string) {
    const formatters = await state().then((x) => x.formatters)
    const possibleFormatters = []

    // Step 1: Collect all formatters that could apply to this file
    // Include if: direct ext match OR wildcard pattern matches fullExt
    for (const item of Object.values(formatters)) {
      log.info("checking", { name: item.name, ext, fullExt })
      
      const matches = item.extensions.some(pattern => {
        if (pattern.includes("*") || pattern.includes("?")) {
          return Wildcard.match(fullExt, pattern)
        }
        return pattern === ext
      })
      
      if (!matches) continue
      if (!(await isEnabled(item))) continue
      possibleFormatters.push(item)
    }

    // Step 2: Apply precedence - prefer formatters with explicit fullExt wildcard matches
    // Strong: formatters with wildcard patterns that match fullExt (e.g., "*.test.ts")
    // Weak: formatters that only match base ext (e.g., ".ts")
    // If any strong matches exist, use only those; otherwise use all possible formatters
    const strongFormatters = possibleFormatters.filter(formatter =>
      formatter.extensions.some(pattern => 
        (pattern.includes("*") || pattern.includes("?")) && 
        Wildcard.match(fullExt, pattern)
      )
    )
    if (strongFormatters.length) return strongFormatters
    return possibleFormatters
  }

Let me know what you think?

Ill try to check if any editor "standards" exists now.

OpeOginni avatar Nov 04 '25 21:11 OpeOginni

@rekram1-node Was looking at how it is handled by Biome and some prettier plugins, the new logic works, only thing we can add is some sort of "longest matching pattern wins" that Biome implements.

In this case if two formatters have extensions as *.test.ts and *.ts, it will only take the formatter with *.test.ts for a file like example.test.ts.

@OpeOginni I think it should just run both of them in that case.

But based on your answer here, I don't know if thats what we want.

In the case of a user not having any wildcards at all, normal .ts would work for all files that ends with .ts as long as there was no other strong matches.

OpeOginni avatar Nov 05 '25 09:11 OpeOginni