diff --git a/src/renderer/src/components/CodeEditor/__tests__/utils.test.ts b/src/renderer/src/components/CodeEditor/__tests__/utils.test.ts new file mode 100644 index 0000000000..b02d10e152 --- /dev/null +++ b/src/renderer/src/components/CodeEditor/__tests__/utils.test.ts @@ -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') + }) +}) diff --git a/src/renderer/src/components/CodeEditor/hooks.ts b/src/renderer/src/components/CodeEditor/hooks.ts index 7917cebd80..b6689644e9 100644 --- a/src/renderer/src/components/CodeEditor/hooks.ts +++ b/src/renderer/src/components/CodeEditor/hooks.ts @@ -8,7 +8,9 @@ import { getNormalizedExtension } from './utils' const logger = loggerService.withContext('CodeEditorHooks') -// 语言对应的 linter 加载器 +/** 语言对应的 linter 加载器 + * key: 语言文件扩展名(不包含 `.`) + */ const linterLoaders: Record Promise> = { json: async () => { const jsonParseLinter = await import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter) @@ -64,13 +66,15 @@ async function loadLanguageExtension(language: string): Promise { - 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 } } diff --git a/src/renderer/src/components/CodeEditor/index.tsx b/src/renderer/src/components/CodeEditor/index.tsx index 749ec57b01..7c541cbdb8 100644 --- a/src/renderer/src/components/CodeEditor/index.tsx +++ b/src/renderer/src/components/CodeEditor/index.tsx @@ -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 diff --git a/src/renderer/src/components/CodeEditor/utils.ts b/src/renderer/src/components/CodeEditor/utils.ts index 251778b9d1..ef5941720e 100644 --- a/src/renderer/src/components/CodeEditor/utils.ts +++ b/src/renderer/src/components/CodeEditor/utils.ts @@ -13,6 +13,7 @@ const _customLanguageExtensions: Record = { * 获取语言的扩展名,用于 @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 }