refactor(CodeEditor): support file extensions explicitly (#9707)

This commit is contained in:
one 2025-08-31 18:31:28 +08:00 committed by GitHub
parent 1ee57f1385
commit 4dbe5c8055
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 63 additions and 4 deletions

View File

@ -0,0 +1,43 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getNormalizedExtension } from '../utils'
const mocks = vi.hoisted(() => ({
getExtensionByLanguage: vi.fn()
}))
vi.mock('@renderer/utils/code-language', () => ({
getExtensionByLanguage: mocks.getExtensionByLanguage
}))
describe('getNormalizedExtension', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should return custom mapping for custom language', async () => {
mocks.getExtensionByLanguage.mockReturnValue(undefined)
await expect(getNormalizedExtension('svg')).resolves.toBe('xml')
await expect(getNormalizedExtension('SVG')).resolves.toBe('xml')
})
it('should prefer custom mapping when both custom and linguist exist', async () => {
mocks.getExtensionByLanguage.mockReturnValue('.svg')
await expect(getNormalizedExtension('svg')).resolves.toBe('xml')
})
it('should return linguist mapping when available (strip leading dot)', async () => {
mocks.getExtensionByLanguage.mockReturnValue('.ts')
await expect(getNormalizedExtension('TypeScript')).resolves.toBe('ts')
})
it('should return extension when input already looks like extension (leading dot)', async () => {
mocks.getExtensionByLanguage.mockReturnValue(undefined)
await expect(getNormalizedExtension('.json')).resolves.toBe('json')
})
it('should return language as-is when no rules matched', async () => {
mocks.getExtensionByLanguage.mockReturnValue(undefined)
await expect(getNormalizedExtension('unknownLanguage')).resolves.toBe('unknownLanguage')
})
})

View File

@ -8,7 +8,9 @@ import { getNormalizedExtension } from './utils'
const logger = loggerService.withContext('CodeEditorHooks')
// 语言对应的 linter 加载器
/** linter
* key: 语言文件扩展名 `.`
*/
const linterLoaders: Record<string, () => Promise<any>> = {
json: async () => {
const jsonParseLinter = await import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter)
@ -64,13 +66,15 @@ async function loadLanguageExtension(language: string): Promise<Extension | null
* linter
*/
async function loadLinterExtension(language: string): Promise<Extension | null> {
const loader = linterLoaders[language]
const fileExt = await getNormalizedExtension(language)
const loader = linterLoaders[fileExt]
if (!loader) return null
try {
return await loader()
} catch (error) {
logger.debug(`Failed to load linter for ${language}`, error as Error)
logger.debug(`Failed to load linter for ${language} (${fileExt})`, error as Error)
return null
}
}

View File

@ -20,7 +20,13 @@ export interface CodeEditorProps {
value: string
/** Placeholder when the editor content is empty. */
placeholder?: string | HTMLElement
/** Code language, supports aliases. */
/**
* Code language string.
* - Case-insensitive.
* - Supports common names: javascript, json, python, etc.
* - Supports aliases: c#/csharp, objective-c++/obj-c++/objc++, etc.
* - Supports file extensions: .cpp/cpp, .js/js, .py/py, etc.
*/
language: string
/** Fired when ref.save() is called or the save shortcut is triggered. */
onSave?: (newContent: string) => void

View File

@ -13,6 +13,7 @@ const _customLanguageExtensions: Record<string, string> = {
* @uiw/codemirror-extensions-langs
* -
* - github linguist
* -
* @param language
* @returns `.`
*/
@ -29,6 +30,11 @@ export async function getNormalizedExtension(language: string) {
return linguistExt.slice(1)
}
// 如果语言名称像扩展名
if (language.startsWith('.') && language.length > 1) {
return language.slice(1)
}
// 回退到语言名称
return language
}