diff --git a/packages/ui/package.json b/packages/ui/package.json index 631167e1bf..00dce3b034 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -15,7 +15,8 @@ "lint": "eslint src --ext .ts,.tsx --fix", "type-check": "tsc --noEmit", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "update:languages": "tsx scripts/update-languages.ts" }, "keywords": [ "ui", @@ -53,17 +54,27 @@ "@types/react": "^19.0.12", "@types/react-dom": "^19.0.4", "@types/styled-components": "^5.1.34", + "@uiw/codemirror-extensions-langs": "^4.25.1", + "@uiw/codemirror-themes-all": "^4.25.1", + "@uiw/react-codemirror": "^4.25.1", "antd": "^5.22.5", "eslint-plugin-storybook": "9.1.6", "framer-motion": "^12.23.12", + "linguist-languages": "^9.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", "storybook": "^9.1.6", "styled-components": "^6.1.15", "tsdown": "^0.12.9", + "tsx": "^4.20.5", "typescript": "^5.6.2", "vitest": "^3.2.4" }, + "resolutions": { + "@codemirror/language": "6.11.3", + "@codemirror/lint": "6.8.5", + "@codemirror/view": "6.38.1" + }, "sideEffects": false, "engines": { "node": ">=18.0.0" diff --git a/packages/ui/scripts/update-languages.ts b/packages/ui/scripts/update-languages.ts new file mode 100644 index 0000000000..9b5c98d620 --- /dev/null +++ b/packages/ui/scripts/update-languages.ts @@ -0,0 +1,135 @@ +import { exec } from 'child_process' +import * as fs from 'fs/promises' +import * as linguistLanguages from 'linguist-languages' +import * as path from 'path' +import { promisify } from 'util' + +const execAsync = promisify(exec) + +type LanguageData = { + type: string + aliases?: string[] + extensions?: string[] +} + +const LANGUAGES_FILE_PATH = path.join(__dirname, '../src/config/languages.ts') + +/** + * Extracts and filters necessary language data from the linguist-languages package. + * @returns A record of language data. + */ +function extractAllLanguageData(): Record { + console.log('🔍 Extracting language data from linguist-languages...') + const languages = Object.entries(linguistLanguages).reduce( + (acc, [name, langData]) => { + const { type, extensions, aliases } = langData as any + + // Only include languages with extensions or aliases + if ((extensions && extensions.length > 0) || (aliases && aliases.length > 0)) { + acc[name] = { + type: type || 'programming', + ...(extensions && { extensions }), + ...(aliases && { aliases }) + } + } + return acc + }, + {} as Record + ) + console.log(`✅ Extracted ${Object.keys(languages).length} languages.`) + return languages +} + +/** + * Generates the content for the languages.ts file. + * @param languages The language data to include in the file. + * @returns The generated file content as a string. + */ +function generateLanguagesFileContent(languages: Record): string { + console.log('📝 Generating languages.ts file content...') + const sortedLanguages = Object.fromEntries(Object.entries(languages).sort(([a], [b]) => a.localeCompare(b))) + + const languagesObjectString = JSON.stringify(sortedLanguages, null, 2) + + const content = `/** + * Code language list. + * Data source: linguist-languages + * + * ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ + * THIS FILE IS AUTOMATICALLY GENERATED BY A SCRIPT. DO NOT EDIT IT MANUALLY! + * Run \`yarn update:languages\` to update this file. + * ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ + * + */ + +type LanguageData = { + type: string; + aliases?: string[]; + extensions?: string[]; +}; + +export const languages: Record = ${languagesObjectString}; +` + console.log('✅ File content generated.') + return content +} + +/** + * Formats a file using Prettier. + * @param filePath The path to the file to format. + */ +async function formatWithPrettier(filePath: string): Promise { + console.log('🎨 Formatting file with Prettier...') + try { + await execAsync(`yarn prettier --write ${filePath}`) + console.log('✅ Prettier formatting complete.') + } catch (e: any) { + console.error('❌ Prettier formatting failed:', e.stdout || e.stderr) + throw new Error('Prettier formatting failed.') + } +} + +/** + * Checks a file with TypeScript compiler. + * @param filePath The path to the file to check. + */ +async function checkTypeScript(filePath: string): Promise { + console.log('🧐 Checking file with TypeScript compiler...') + try { + await execAsync(`yarn tsc --noEmit --skipLibCheck ${filePath}`) + console.log('✅ TypeScript check passed.') + } catch (e: any) { + console.error('❌ TypeScript check failed:', e.stdout || e.stderr) + throw new Error('TypeScript check failed.') + } +} + +/** + * Main function to update the languages.ts file. + */ +async function updateLanguagesFile(): Promise { + console.log('🚀 Starting to update languages.ts...') + try { + const extractedLanguages = extractAllLanguageData() + const fileContent = generateLanguagesFileContent(extractedLanguages) + + await fs.writeFile(LANGUAGES_FILE_PATH, fileContent, 'utf-8') + console.log(`✅ Successfully wrote to ${LANGUAGES_FILE_PATH}`) + + await formatWithPrettier(LANGUAGES_FILE_PATH) + await checkTypeScript(LANGUAGES_FILE_PATH) + + console.log('🎉 Successfully updated languages.ts file!') + console.log(`📊 Contains ${Object.keys(extractedLanguages).length} languages.`) + } catch (error) { + console.error('❌ An error occurred during the update process:', (error as Error).message) + // No need to restore backup as we write only at the end of successful generation. + process.exit(1) + } +} + +if (require.main === module) { + updateLanguagesFile() +} + +export { updateLanguagesFile } diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 889b1e328b..ebfb955c1d 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -41,6 +41,14 @@ export { default as WebSearchIcon } from './icons/WebSearchIcon' export { default as WrapIcon } from './icons/WrapIcon' // Interactive Components +export { + default as CodeEditor, + type CodeEditorHandles, + type CodeEditorProps, + type CodeMirrorTheme, + getCmThemeByName, + getCmThemeNames +} from './interactive/CodeEditor' export { default as CollapsibleSearchBar } from './interactive/CollapsibleSearchBar' export { DraggableList, useDraggableReorder } from './interactive/DraggableList' export type { EditableNumberProps } from './interactive/EditableNumber' diff --git a/packages/ui/src/components/interactive/CodeEditor/CodeEditor.tsx b/packages/ui/src/components/interactive/CodeEditor/CodeEditor.tsx new file mode 100644 index 0000000000..b202205d5c --- /dev/null +++ b/packages/ui/src/components/interactive/CodeEditor/CodeEditor.tsx @@ -0,0 +1,135 @@ +import CodeMirror, { Annotation, BasicSetupOptions, EditorView } from '@uiw/react-codemirror' +import { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react' +import { memo } from 'react' + +import { useBlurHandler, useHeightListener, useLanguageExtensions, useSaveKeymap } from './hooks' +import { CodeEditorProps } from './types' +import { prepareCodeChanges } from './utils' + +/** + * A code editor component based on CodeMirror. + * This is a wrapper of ReactCodeMirror. + */ +const CodeEditor = ({ + ref, + value, + placeholder, + language, + onSave, + onChange, + onBlur, + onHeightChange, + height, + maxHeight, + minHeight, + options, + extensions, + theme = 'light', + fontSize = 16, + style, + className, + editable = true, + expanded = true, + wrapped = true +}: CodeEditorProps) => { + const basicSetup = useMemo(() => { + return { + dropCursor: true, + allowMultipleSelections: true, + indentOnInput: true, + bracketMatching: true, + closeBrackets: true, + rectangularSelection: true, + crosshairCursor: true, + highlightActiveLineGutter: false, + highlightSelectionMatches: true, + closeBracketsKeymap: options?.keymap, + searchKeymap: options?.keymap, + foldKeymap: options?.keymap, + completionKeymap: options?.keymap, + lintKeymap: options?.keymap, + ...(options as BasicSetupOptions) + } + }, [options]) + + const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? '')) + const editorViewRef = useRef(null) + + const langExtensions = useLanguageExtensions(language, options?.lint) + + const handleSave = useCallback(() => { + const currentDoc = editorViewRef.current?.state.doc.toString() ?? '' + onSave?.(currentDoc) + }, [onSave]) + + // Calculate changes during streaming response to update EditorView + // Cannot handle user editing code during streaming response (and probably doesn't need to) + useEffect(() => { + if (!editorViewRef.current) return + + const newContent = options?.stream ? (value ?? '').trimEnd() : (value ?? '') + const currentDoc = editorViewRef.current.state.doc.toString() + + const changes = prepareCodeChanges(currentDoc, newContent) + + if (changes && changes.length > 0) { + editorViewRef.current.dispatch({ + changes, + annotations: [Annotation.define().of(true)] + }) + } + }, [options?.stream, value]) + + const saveKeymapExtension = useSaveKeymap({ onSave, enabled: options?.keymap }) + const blurExtension = useBlurHandler({ onBlur }) + const heightListenerExtension = useHeightListener({ onHeightChange }) + + const customExtensions = useMemo(() => { + return [ + ...(extensions ?? []), + ...langExtensions, + ...(wrapped ? [EditorView.lineWrapping] : []), + saveKeymapExtension, + blurExtension, + heightListenerExtension + ].flat() + }, [extensions, langExtensions, wrapped, saveKeymapExtension, blurExtension, heightListenerExtension]) + + useImperativeHandle(ref, () => ({ + save: handleSave + })) + + return ( + { + editorViewRef.current = view + onHeightChange?.(view.scrollDOM?.scrollHeight ?? 0) + }} + onChange={(value, viewUpdate) => { + if (onChange && viewUpdate.docChanged) onChange(value) + }} + basicSetup={basicSetup} + style={{ + fontSize, + marginTop: 0, + borderRadius: 'inherit', + ...style + }} + className={`code-editor ${className ?? ''}`} + /> + ) +} + +CodeEditor.displayName = 'CodeEditor' + +export default memo(CodeEditor) diff --git a/packages/ui/src/components/interactive/CodeEditor/__tests__/utils.test.ts b/packages/ui/src/components/interactive/CodeEditor/__tests__/utils.test.ts new file mode 100644 index 0000000000..ebc9a3d7d3 --- /dev/null +++ b/packages/ui/src/components/interactive/CodeEditor/__tests__/utils.test.ts @@ -0,0 +1,41 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { getNormalizedExtension } from '../utils' + +const hoisted = vi.hoisted(() => ({ + languages: { + svg: { extensions: ['.svg'] }, + TypeScript: { extensions: ['.ts'] } + } +})) + +vi.mock('@shared/config/languages', () => ({ + languages: hoisted.languages +})) + +describe('getNormalizedExtension', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should return custom mapping for custom language', async () => { + 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 () => { + await expect(getNormalizedExtension('svg')).resolves.toBe('xml') + }) + + it('should return linguist mapping when available (strip leading dot)', async () => { + await expect(getNormalizedExtension('TypeScript')).resolves.toBe('ts') + }) + + it('should return extension when input already looks like extension (leading dot)', async () => { + await expect(getNormalizedExtension('.json')).resolves.toBe('json') + }) + + it('should return language as-is when no rules matched', async () => { + await expect(getNormalizedExtension('unknownLanguage')).resolves.toBe('unknownLanguage') + }) +}) diff --git a/packages/ui/src/components/interactive/CodeEditor/hooks.ts b/packages/ui/src/components/interactive/CodeEditor/hooks.ts new file mode 100644 index 0000000000..7c4f564763 --- /dev/null +++ b/packages/ui/src/components/interactive/CodeEditor/hooks.ts @@ -0,0 +1,202 @@ +import { linter } from '@codemirror/lint' // statically imported by @uiw/codemirror-extensions-basic-setup +import { EditorView } from '@codemirror/view' +import { Extension, keymap } from '@uiw/react-codemirror' +import { useEffect, useMemo, useState } from 'react' + +import { getNormalizedExtension } from './utils' + +/** 语言对应的 linter 加载器 + * key: 语言文件扩展名(不包含 `.`) + */ +const linterLoaders: Record Promise> = { + json: async () => { + const jsonParseLinter = await import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter) + return linter(jsonParseLinter()) + } +} + +/** + * 特殊语言加载器 + * key: 语言文件扩展名(不包含 `.`) + */ +const specialLanguageLoaders: Record Promise> = { + dot: async () => { + const mod = await import('@viz-js/lang-dot') + return mod.dot() + }, + // @uiw/codemirror-extensions-langs 4.25.1 移除了 mermaid 支持,这里加回来 + mmd: async () => { + const mod = await import('codemirror-lang-mermaid') + return mod.mermaid() + } +} + +/** + * 加载语言扩展 + */ +async function loadLanguageExtension(language: string): Promise { + const fileExt = await getNormalizedExtension(language) + + // 尝试加载特殊语言 + const specialLoader = specialLanguageLoaders[fileExt] + if (specialLoader) { + try { + return await specialLoader() + } catch (error) { + console.debug(`Failed to load language ${language} (${fileExt})`, error as Error) + return null + } + } + + // 回退到 uiw/codemirror 包含的语言 + try { + const { loadLanguage } = await import('@uiw/codemirror-extensions-langs') + const extension = loadLanguage(fileExt as any) + return extension || null + } catch (error) { + console.debug(`Failed to load language ${language} (${fileExt})`, error as Error) + return null + } +} + +/** + * 加载 linter 扩展 + */ +async function loadLinterExtension(language: string): Promise { + const fileExt = await getNormalizedExtension(language) + + const loader = linterLoaders[fileExt] + if (!loader) return null + + try { + return await loader() + } catch (error) { + console.debug(`Failed to load linter for ${language} (${fileExt})`, error as Error) + return null + } +} + +/** + * 加载语言相关扩展 + */ +export const useLanguageExtensions = (language: string, lint?: boolean) => { + const [extensions, setExtensions] = useState([]) + + useEffect(() => { + let cancelled = false + + const loadAllExtensions = async () => { + try { + // 加载所有扩展 + const [languageResult, linterResult] = await Promise.allSettled([ + loadLanguageExtension(language), + lint ? loadLinterExtension(language) : Promise.resolve(null) + ]) + + if (cancelled) return + + const results: Extension[] = [] + + // 语言扩展 + if (languageResult.status === 'fulfilled' && languageResult.value) { + results.push(languageResult.value) + } + + // linter 扩展 + if (linterResult.status === 'fulfilled' && linterResult.value) { + results.push(linterResult.value) + } + + setExtensions(results) + } catch (error) { + if (!cancelled) { + console.debug('Failed to load language extensions:', error as Error) + setExtensions([]) + } + } + } + + loadAllExtensions() + + return () => { + cancelled = true + } + }, [language, lint]) + + return extensions +} + +interface UseSaveKeymapProps { + onSave?: (content: string) => void + enabled?: boolean +} + +/** + * CodeMirror 扩展,用于处理保存快捷键 (Cmd/Ctrl + S) + * @param onSave 保存时触发的回调函数 + * @param enabled 是否启用此快捷键 + * @returns 扩展或空数组 + */ +export function useSaveKeymap({ onSave, enabled = true }: UseSaveKeymapProps) { + return useMemo(() => { + if (!enabled || !onSave) { + return [] + } + + return keymap.of([ + { + key: 'Mod-s', + run: (view: EditorView) => { + onSave(view.state.doc.toString()) + return true + }, + preventDefault: true + } + ]) + }, [onSave, enabled]) +} + +interface UseBlurHandlerProps { + onBlur?: (content: string) => void +} + +/** + * CodeMirror 扩展,用于处理编辑器的 blur 事件 + * @param onBlur blur 事件触发时的回调函数 + * @returns 扩展或空数组 + */ +export function useBlurHandler({ onBlur }: UseBlurHandlerProps) { + return useMemo(() => { + if (!onBlur) { + return [] + } + return EditorView.domEventHandlers({ + blur: (_event, view) => { + onBlur(view.state.doc.toString()) + } + }) + }, [onBlur]) +} + +interface UseHeightListenerProps { + onHeightChange?: (scrollHeight: number) => void +} + +/** + * CodeMirror 扩展,用于监听编辑器高度变化 + * @param onHeightChange 高度变化时触发的回调函数 + * @returns 扩展或空数组 + */ +export function useHeightListener({ onHeightChange }: UseHeightListenerProps) { + return useMemo(() => { + if (!onHeightChange) { + return [] + } + + return EditorView.updateListener.of((update) => { + if (update.docChanged || update.heightChanged) { + onHeightChange(update.view.scrollDOM?.scrollHeight ?? 0) + } + }) + }, [onHeightChange]) +} diff --git a/packages/ui/src/components/interactive/CodeEditor/index.ts b/packages/ui/src/components/interactive/CodeEditor/index.ts new file mode 100644 index 0000000000..4a2e55f9fb --- /dev/null +++ b/packages/ui/src/components/interactive/CodeEditor/index.ts @@ -0,0 +1,3 @@ +export { default } from './CodeEditor' +export * from './types' +export { getCmThemeByName, getCmThemeNames } from './utils' diff --git a/packages/ui/src/components/interactive/CodeEditor/types.ts b/packages/ui/src/components/interactive/CodeEditor/types.ts new file mode 100644 index 0000000000..bc91377f70 --- /dev/null +++ b/packages/ui/src/components/interactive/CodeEditor/types.ts @@ -0,0 +1,93 @@ +import { BasicSetupOptions, Extension } from '@uiw/react-codemirror' + +export type CodeMirrorTheme = 'light' | 'dark' | 'none' | Extension + +export interface CodeEditorHandles { + save?: () => void +} + +export interface CodeEditorProps { + ref?: React.RefObject + /** Value used in controlled mode, e.g., code blocks. */ + value: string + /** Placeholder when the editor content is empty. */ + placeholder?: string | HTMLElement + /** + * 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 + /** Fired when the editor content changes. */ + onChange?: (newContent: string) => void + /** Fired when the editor loses focus. */ + onBlur?: (newContent: string) => void + /** Fired when the editor height changes. */ + onHeightChange?: (scrollHeight: number) => void + /** + * Fixed editor height, not exceeding maxHeight. + * Only works when expanded is false. + */ + height?: string + /** + * Maximum editor height. + * Only works when expanded is false. + */ + maxHeight?: string + /** Minimum editor height. */ + minHeight?: string + /** Editor options that extend BasicSetupOptions. */ + options?: { + /** + * Whether to enable special treatment for stream response. + * @default false + */ + stream?: boolean + /** + * Whether to enable linting. + * @default false + */ + lint?: boolean + /** + * Whether to enable keymap. + * @default false + */ + keymap?: boolean + } & BasicSetupOptions + /** Additional extensions for CodeMirror. */ + extensions?: Extension[] + /** + * CodeMirror theme name: 'light', 'dark', 'none', Extension. + * @default 'light' + */ + theme?: CodeMirrorTheme + /** + * Font size that overrides the app setting. + * @default 16 + */ + fontSize?: number + /** Style overrides for the editor, passed directly to CodeMirror's style property. */ + style?: React.CSSProperties + /** CSS class name appended to the default `code-editor` class. */ + className?: string + /** + * Whether the editor is editable. + * @default true + */ + editable?: boolean + /** + * Whether the editor is expanded. + * If true, the height and maxHeight props are ignored. + * @default true + */ + expanded?: boolean + /** + * Whether the code lines are wrapped. + * @default true + */ + wrapped?: boolean +} diff --git a/packages/ui/src/components/interactive/CodeEditor/utils.ts b/packages/ui/src/components/interactive/CodeEditor/utils.ts new file mode 100644 index 0000000000..a89f3e12e5 --- /dev/null +++ b/packages/ui/src/components/interactive/CodeEditor/utils.ts @@ -0,0 +1,126 @@ +import * as cmThemes from '@uiw/codemirror-themes-all' +import { Extension } from '@uiw/react-codemirror' +import diff from 'fast-diff' + +import { getExtensionByLanguage } from '../../../utils/codeLanguage' +import { CodeMirrorTheme } from './types' + +/** + * Computes code changes using fast-diff and converts them to CodeMirror changes. + * Could handle all types of changes, though insertions are most common during streaming responses. + * @param oldCode The old code content + * @param newCode The new code content + * @returns An array of changes for EditorView.dispatch + */ +export function prepareCodeChanges(oldCode: string, newCode: string) { + const diffResult = diff(oldCode, newCode) + + const changes: { from: number; to: number; insert: string }[] = [] + let offset = 0 + + // operation: 1=insert, -1=delete, 0=equal + for (const [operation, text] of diffResult) { + if (operation === 1) { + changes.push({ + from: offset, + to: offset, + insert: text + }) + } else if (operation === -1) { + changes.push({ + from: offset, + to: offset + text.length, + insert: '' + }) + offset += text.length + } else { + offset += text.length + } + } + + return changes +} + +// Custom language file extension mapping +// key: language name in lowercase +// value: file extension +const _customLanguageExtensions: Record = { + svg: 'xml', + vab: 'vb', + graphviz: 'dot' +} + +/** + * Get the file extension of the language, for @uiw/codemirror-extensions-langs + * - First, search for custom extensions + * - Then, search for github linguist extensions + * - Finally, assume the name is already an extension + * @param language language name + * @returns file extension (without `.` prefix) + */ +export async function getNormalizedExtension(language: string) { + let lang = language + + // If the language name looks like an extension, remove the dot + if (language.startsWith('.') && language.length > 1) { + lang = language.slice(1) + } + + const lowerLanguage = lang.toLowerCase() + + // 1. Search for custom extensions + const customExt = _customLanguageExtensions[lowerLanguage] + if (customExt) { + return customExt + } + + // 2. Search for github linguist extensions + const linguistExt = getExtensionByLanguage(lang) + if (linguistExt) { + return linguistExt.slice(1) + } + + // Fallback to language name + return lang +} + +/** + * Get the list of CodeMirror theme names + * - Include auto, light, dark + * - Include all themes in @uiw/codemirror-themes-all + * + * A more robust approach might be to hardcode the theme list + * @returns theme name list + */ +export function getCmThemeNames(): string[] { + return ['auto', 'light', 'dark'] + .concat(Object.keys(cmThemes)) + .filter((item) => typeof cmThemes[item as keyof typeof cmThemes] !== 'function') + .filter((item) => !/^(defaultSettings)/.test(item as string) && !/(Style)$/.test(item as string)) +} + +/** + * Get the CodeMirror theme object by theme name + * @param name theme name + * @returns theme object + */ +export function getCmThemeByName(name: string): CodeMirrorTheme { + // 1. Search for the extension of the corresponding theme in @uiw/codemirror-themes-all + const candidate = (cmThemes as Record)[name] + if ( + Object.prototype.hasOwnProperty.call(cmThemes, name) && + typeof candidate !== 'function' && + !/^defaultSettings/i.test(name) && + !/(Style)$/.test(name) + ) { + return candidate as Extension + } + + // 2. Basic string theme + if (name === 'light' || name === 'dark' || name === 'none') { + return name + } + + // 3. If not found, fallback to light + return 'light' +} diff --git a/packages/ui/src/config/languages.ts b/packages/ui/src/config/languages.ts new file mode 100644 index 0000000000..7b1a432a33 --- /dev/null +++ b/packages/ui/src/config/languages.ts @@ -0,0 +1,3695 @@ +/** + * Code language list. + * Data source: linguist-languages + * + * ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ + * THIS FILE IS AUTOMATICALLY GENERATED BY A SCRIPT. DO NOT EDIT IT MANUALLY! + * Run `yarn update:languages` to update this file. + * ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ + * + */ + +type LanguageData = { + type: string + aliases?: string[] + extensions?: string[] +} + +export const languages: Record = { + '1C Enterprise': { + type: 'programming', + extensions: ['.bsl', '.os'] + }, + '2-Dimensional Array': { + type: 'data', + extensions: ['.2da'] + }, + '4D': { + type: 'programming', + extensions: ['.4dm'] + }, + ABAP: { + type: 'programming', + extensions: ['.abap'] + }, + 'ABAP CDS': { + type: 'programming', + extensions: ['.asddls'] + }, + ABNF: { + type: 'data', + extensions: ['.abnf'] + }, + ActionScript: { + type: 'programming', + extensions: ['.as'], + aliases: ['actionscript 3', 'actionscript3', 'as3'] + }, + Ada: { + type: 'programming', + extensions: ['.adb', '.ada', '.ads'], + aliases: ['ada95', 'ada2005'] + }, + 'Adblock Filter List': { + type: 'data', + extensions: ['.txt'], + aliases: ['ad block filters', 'ad block', 'adb', 'adblock'] + }, + 'Adobe Font Metrics': { + type: 'data', + extensions: ['.afm'], + aliases: ['acfm', 'adobe composite font metrics', 'adobe multiple font metrics', 'amfm'] + }, + Agda: { + type: 'programming', + extensions: ['.agda'] + }, + 'AGS Script': { + type: 'programming', + extensions: ['.asc', '.ash'], + aliases: ['ags'] + }, + AIDL: { + type: 'programming', + extensions: ['.aidl'] + }, + Aiken: { + type: 'programming', + extensions: ['.ak'] + }, + AL: { + type: 'programming', + extensions: ['.al'] + }, + Alloy: { + type: 'programming', + extensions: ['.als'] + }, + 'Alpine Abuild': { + type: 'programming', + aliases: ['abuild', 'apkbuild'] + }, + 'Altium Designer': { + type: 'data', + extensions: ['.OutJob', '.PcbDoc', '.PrjPCB', '.SchDoc'], + aliases: ['altium'] + }, + AMPL: { + type: 'programming', + extensions: ['.ampl', '.mod'] + }, + AngelScript: { + type: 'programming', + extensions: ['.as', '.angelscript'] + }, + 'Answer Set Programming': { + type: 'programming', + extensions: ['.lp'] + }, + Antlers: { + type: 'markup', + extensions: ['.antlers.html', '.antlers.php', '.antlers.xml'] + }, + ANTLR: { + type: 'programming', + extensions: ['.g4'] + }, + ApacheConf: { + type: 'data', + extensions: ['.apacheconf', '.vhost'], + aliases: ['aconf', 'apache'] + }, + Apex: { + type: 'programming', + extensions: ['.cls', '.apex', '.trigger'] + }, + 'API Blueprint': { + type: 'markup', + extensions: ['.apib'] + }, + APL: { + type: 'programming', + extensions: ['.apl', '.dyalog'] + }, + 'Apollo Guidance Computer': { + type: 'programming', + extensions: ['.agc'] + }, + AppleScript: { + type: 'programming', + extensions: ['.applescript', '.scpt'], + aliases: ['osascript'] + }, + Arc: { + type: 'programming', + extensions: ['.arc'] + }, + AsciiDoc: { + type: 'prose', + extensions: ['.asciidoc', '.adoc', '.asc'] + }, + ASL: { + type: 'programming', + extensions: ['.asl', '.dsl'] + }, + 'ASN.1': { + type: 'data', + extensions: ['.asn', '.asn1'] + }, + 'ASP.NET': { + type: 'programming', + extensions: ['.asax', '.ascx', '.ashx', '.asmx', '.aspx', '.axd'], + aliases: ['aspx', 'aspx-vb'] + }, + AspectJ: { + type: 'programming', + extensions: ['.aj'] + }, + Assembly: { + type: 'programming', + extensions: ['.asm', '.a51', '.i', '.inc', '.nas', '.nasm', '.s'], + aliases: ['asm', 'nasm'] + }, + Astro: { + type: 'markup', + extensions: ['.astro'] + }, + Asymptote: { + type: 'programming', + extensions: ['.asy'] + }, + ATS: { + type: 'programming', + extensions: ['.dats', '.hats', '.sats'], + aliases: ['ats2'] + }, + Augeas: { + type: 'programming', + extensions: ['.aug'] + }, + AutoHotkey: { + type: 'programming', + extensions: ['.ahk', '.ahkl'], + aliases: ['ahk'] + }, + AutoIt: { + type: 'programming', + extensions: ['.au3'], + aliases: ['au3', 'AutoIt3', 'AutoItScript'] + }, + 'Avro IDL': { + type: 'data', + extensions: ['.avdl'] + }, + Awk: { + type: 'programming', + extensions: ['.awk', '.auk', '.gawk', '.mawk', '.nawk'] + }, + B4X: { + type: 'programming', + extensions: ['.bas'], + aliases: ['basic for android'] + }, + Ballerina: { + type: 'programming', + extensions: ['.bal'] + }, + BASIC: { + type: 'programming', + extensions: ['.bas'] + }, + Batchfile: { + type: 'programming', + extensions: ['.bat', '.cmd'], + aliases: ['bat', 'batch', 'dosbatch', 'winbatch'] + }, + Beef: { + type: 'programming', + extensions: ['.bf'] + }, + Befunge: { + type: 'programming', + extensions: ['.befunge', '.bf'] + }, + Berry: { + type: 'programming', + extensions: ['.be'], + aliases: ['be'] + }, + BibTeX: { + type: 'markup', + extensions: ['.bib', '.bibtex'] + }, + 'BibTeX Style': { + type: 'programming', + extensions: ['.bst'] + }, + Bicep: { + type: 'programming', + extensions: ['.bicep', '.bicepparam'] + }, + Bikeshed: { + type: 'markup', + extensions: ['.bs'] + }, + Bison: { + type: 'programming', + extensions: ['.bison'] + }, + BitBake: { + type: 'programming', + extensions: ['.bb', '.bbappend', '.bbclass', '.inc'] + }, + Blade: { + type: 'markup', + extensions: ['.blade', '.blade.php'] + }, + BlitzBasic: { + type: 'programming', + extensions: ['.bb', '.decls'], + aliases: ['b3d', 'blitz3d', 'blitzplus', 'bplus'] + }, + BlitzMax: { + type: 'programming', + extensions: ['.bmx'], + aliases: ['bmax'] + }, + Bluespec: { + type: 'programming', + extensions: ['.bsv'], + aliases: ['bluespec bsv', 'bsv'] + }, + 'Bluespec BH': { + type: 'programming', + extensions: ['.bs'], + aliases: ['bh', 'bluespec classic'] + }, + Boo: { + type: 'programming', + extensions: ['.boo'] + }, + Boogie: { + type: 'programming', + extensions: ['.bpl'] + }, + BQN: { + type: 'programming', + extensions: ['.bqn'] + }, + Brainfuck: { + type: 'programming', + extensions: ['.b', '.bf'] + }, + BrighterScript: { + type: 'programming', + extensions: ['.bs'] + }, + Brightscript: { + type: 'programming', + extensions: ['.brs'] + }, + BuildStream: { + type: 'data', + extensions: ['.bst'] + }, + C: { + type: 'programming', + extensions: ['.c', '.cats', '.h', '.h.in', '.idc'] + }, + 'C-ObjDump': { + type: 'data', + extensions: ['.c-objdump'] + }, + 'C#': { + type: 'programming', + extensions: ['.cs', '.cake', '.cs.pp', '.csx', '.linq'], + aliases: ['csharp', 'cake', 'cakescript'] + }, + 'C++': { + type: 'programming', + extensions: [ + '.cpp', + '.c++', + '.cc', + '.cp', + '.cppm', + '.cxx', + '.h', + '.h++', + '.hh', + '.hpp', + '.hxx', + '.inc', + '.inl', + '.ino', + '.ipp', + '.ixx', + '.re', + '.tcc', + '.tpp', + '.txx' + ], + aliases: ['cpp'] + }, + 'C2hs Haskell': { + type: 'programming', + extensions: ['.chs'], + aliases: ['c2hs'] + }, + C3: { + type: 'programming', + extensions: ['.c3'] + }, + 'Cabal Config': { + type: 'data', + extensions: ['.cabal'], + aliases: ['Cabal'] + }, + Caddyfile: { + type: 'data', + extensions: ['.caddyfile'], + aliases: ['Caddy'] + }, + Cadence: { + type: 'programming', + extensions: ['.cdc'] + }, + Cairo: { + type: 'programming', + extensions: ['.cairo'] + }, + 'Cairo Zero': { + type: 'programming', + extensions: ['.cairo'] + }, + CameLIGO: { + type: 'programming', + extensions: ['.mligo'] + }, + 'CAP CDS': { + type: 'programming', + extensions: ['.cds'], + aliases: ['cds'] + }, + "Cap'n Proto": { + type: 'programming', + extensions: ['.capnp'] + }, + Carbon: { + type: 'programming', + extensions: ['.carbon'] + }, + CartoCSS: { + type: 'programming', + extensions: ['.mss'], + aliases: ['Carto'] + }, + Ceylon: { + type: 'programming', + extensions: ['.ceylon'] + }, + Chapel: { + type: 'programming', + extensions: ['.chpl'], + aliases: ['chpl'] + }, + Charity: { + type: 'programming', + extensions: ['.ch'] + }, + Checksums: { + type: 'data', + extensions: [ + '.crc32', + '.md2', + '.md4', + '.md5', + '.sha1', + '.sha2', + '.sha224', + '.sha256', + '.sha256sum', + '.sha3', + '.sha384', + '.sha512' + ], + aliases: ['checksum', 'hash', 'hashes', 'sum', 'sums'] + }, + ChucK: { + type: 'programming', + extensions: ['.ck'] + }, + CIL: { + type: 'data', + extensions: ['.cil'] + }, + Circom: { + type: 'programming', + extensions: ['.circom'] + }, + Cirru: { + type: 'programming', + extensions: ['.cirru'] + }, + Clarion: { + type: 'programming', + extensions: ['.clw'] + }, + Clarity: { + type: 'programming', + extensions: ['.clar'] + }, + 'Classic ASP': { + type: 'programming', + extensions: ['.asp'], + aliases: ['asp'] + }, + Clean: { + type: 'programming', + extensions: ['.icl', '.dcl'] + }, + Click: { + type: 'programming', + extensions: ['.click'] + }, + CLIPS: { + type: 'programming', + extensions: ['.clp'] + }, + Clojure: { + type: 'programming', + extensions: ['.clj', '.bb', '.boot', '.cl2', '.cljc', '.cljs', '.cljs.hl', '.cljscm', '.cljx', '.hic'] + }, + 'Closure Templates': { + type: 'markup', + extensions: ['.soy'], + aliases: ['soy'] + }, + Clue: { + type: 'programming', + extensions: ['.clue'] + }, + CMake: { + type: 'programming', + extensions: ['.cmake', '.cmake.in'] + }, + COBOL: { + type: 'programming', + extensions: ['.cob', '.cbl', '.ccp', '.cobol', '.cpy'] + }, + CodeQL: { + type: 'programming', + extensions: ['.ql', '.qll'], + aliases: ['ql'] + }, + CoffeeScript: { + type: 'programming', + extensions: ['.coffee', '._coffee', '.cake', '.cjsx', '.iced'], + aliases: ['coffee', 'coffee-script'] + }, + ColdFusion: { + type: 'programming', + extensions: ['.cfm', '.cfml'], + aliases: ['cfm', 'cfml', 'coldfusion html'] + }, + 'ColdFusion CFC': { + type: 'programming', + extensions: ['.cfc'], + aliases: ['cfc'] + }, + COLLADA: { + type: 'data', + extensions: ['.dae'] + }, + 'Common Lisp': { + type: 'programming', + extensions: ['.lisp', '.asd', '.cl', '.l', '.lsp', '.ny', '.podsl', '.sexp'], + aliases: ['lisp'] + }, + 'Common Workflow Language': { + type: 'programming', + extensions: ['.cwl'], + aliases: ['cwl'] + }, + 'Component Pascal': { + type: 'programming', + extensions: ['.cp', '.cps'] + }, + 'CoNLL-U': { + type: 'data', + extensions: ['.conllu', '.conll'], + aliases: ['CoNLL', 'CoNLL-X'] + }, + Cooklang: { + type: 'markup', + extensions: ['.cook'] + }, + Cool: { + type: 'programming', + extensions: ['.cl'] + }, + 'Cpp-ObjDump': { + type: 'data', + extensions: ['.cppobjdump', '.c++-objdump', '.c++objdump', '.cpp-objdump', '.cxx-objdump'], + aliases: ['c++-objdump'] + }, + Creole: { + type: 'prose', + extensions: ['.creole'] + }, + crontab: { + type: 'data', + aliases: ['cron', 'cron table'] + }, + Crystal: { + type: 'programming', + extensions: ['.cr'] + }, + CSON: { + type: 'data', + extensions: ['.cson'] + }, + Csound: { + type: 'programming', + extensions: ['.orc', '.udo'], + aliases: ['csound-orc'] + }, + 'Csound Document': { + type: 'programming', + extensions: ['.csd'], + aliases: ['csound-csd'] + }, + 'Csound Score': { + type: 'programming', + extensions: ['.sco'], + aliases: ['csound-sco'] + }, + CSS: { + type: 'markup', + extensions: ['.css'] + }, + CSV: { + type: 'data', + extensions: ['.csv'] + }, + Cuda: { + type: 'programming', + extensions: ['.cu', '.cuh'] + }, + CUE: { + type: 'programming', + extensions: ['.cue'] + }, + 'Cue Sheet': { + type: 'data', + extensions: ['.cue'] + }, + 'cURL Config': { + type: 'data', + aliases: ['curlrc'] + }, + Curry: { + type: 'programming', + extensions: ['.curry'] + }, + CWeb: { + type: 'programming', + extensions: ['.w'] + }, + Cycript: { + type: 'programming', + extensions: ['.cy'] + }, + Cylc: { + type: 'data', + extensions: ['.cylc'] + }, + Cypher: { + type: 'programming', + extensions: ['.cyp', '.cypher'] + }, + Cython: { + type: 'programming', + extensions: ['.pyx', '.pxd', '.pxi'], + aliases: ['pyrex'] + }, + D: { + type: 'programming', + extensions: ['.d', '.di'], + aliases: ['Dlang'] + }, + 'D-ObjDump': { + type: 'data', + extensions: ['.d-objdump'] + }, + D2: { + type: 'markup', + extensions: ['.d2'], + aliases: ['d2lang'] + }, + Dafny: { + type: 'programming', + extensions: ['.dfy'] + }, + 'Darcs Patch': { + type: 'data', + extensions: ['.darcspatch', '.dpatch'], + aliases: ['dpatch'] + }, + Dart: { + type: 'programming', + extensions: ['.dart'] + }, + Daslang: { + type: 'programming', + extensions: ['.das'] + }, + DataWeave: { + type: 'programming', + extensions: ['.dwl'] + }, + 'Debian Package Control File': { + type: 'data', + extensions: ['.dsc'] + }, + DenizenScript: { + type: 'programming', + extensions: ['.dsc'] + }, + desktop: { + type: 'data', + extensions: ['.desktop', '.desktop.in', '.service'] + }, + Dhall: { + type: 'programming', + extensions: ['.dhall'] + }, + Diff: { + type: 'data', + extensions: ['.diff', '.patch'], + aliases: ['udiff'] + }, + 'DIGITAL Command Language': { + type: 'programming', + extensions: ['.com'], + aliases: ['dcl'] + }, + dircolors: { + type: 'data', + extensions: ['.dircolors'] + }, + 'DirectX 3D File': { + type: 'data', + extensions: ['.x'] + }, + DM: { + type: 'programming', + extensions: ['.dm'], + aliases: ['byond'] + }, + 'DNS Zone': { + type: 'data', + extensions: ['.zone', '.arpa'] + }, + Dockerfile: { + type: 'programming', + extensions: ['.dockerfile', '.containerfile'], + aliases: ['Containerfile'] + }, + Dogescript: { + type: 'programming', + extensions: ['.djs'] + }, + Dotenv: { + type: 'data', + extensions: ['.env'] + }, + DTrace: { + type: 'programming', + extensions: ['.d'], + aliases: ['dtrace-script'] + }, + Dylan: { + type: 'programming', + extensions: ['.dylan', '.dyl', '.intr', '.lid'] + }, + E: { + type: 'programming', + extensions: ['.e'] + }, + 'E-mail': { + type: 'data', + extensions: ['.eml', '.mbox'], + aliases: ['email', 'eml', 'mail', 'mbox'] + }, + Eagle: { + type: 'data', + extensions: ['.sch', '.brd'] + }, + Earthly: { + type: 'programming', + aliases: ['Earthfile'] + }, + Easybuild: { + type: 'data', + extensions: ['.eb'] + }, + EBNF: { + type: 'data', + extensions: ['.ebnf'] + }, + eC: { + type: 'programming', + extensions: ['.ec', '.eh'] + }, + 'Ecere Projects': { + type: 'data', + extensions: ['.epj'] + }, + ECL: { + type: 'programming', + extensions: ['.ecl', '.eclxml'] + }, + ECLiPSe: { + type: 'programming', + extensions: ['.ecl'] + }, + Ecmarkup: { + type: 'markup', + extensions: ['.html'], + aliases: ['ecmarkdown'] + }, + Edge: { + type: 'markup', + extensions: ['.edge'] + }, + EdgeQL: { + type: 'programming', + extensions: ['.edgeql', '.esdl'], + aliases: ['esdl'] + }, + EditorConfig: { + type: 'data', + extensions: ['.editorconfig'], + aliases: ['editor-config'] + }, + 'Edje Data Collection': { + type: 'data', + extensions: ['.edc'] + }, + edn: { + type: 'data', + extensions: ['.edn'] + }, + Eiffel: { + type: 'programming', + extensions: ['.e'] + }, + EJS: { + type: 'markup', + extensions: ['.ejs', '.ect', '.ejs.t', '.jst'] + }, + Elixir: { + type: 'programming', + extensions: ['.ex', '.exs'] + }, + Elm: { + type: 'programming', + extensions: ['.elm'] + }, + Elvish: { + type: 'programming', + extensions: ['.elv'] + }, + 'Emacs Lisp': { + type: 'programming', + extensions: ['.el', '.emacs', '.emacs.desktop'], + aliases: ['elisp', 'emacs'] + }, + EmberScript: { + type: 'programming', + extensions: ['.em', '.emberscript'] + }, + EQ: { + type: 'programming', + extensions: ['.eq'] + }, + Erlang: { + type: 'programming', + extensions: ['.erl', '.app', '.app.src', '.es', '.escript', '.hrl', '.xrl', '.yrl'] + }, + Euphoria: { + type: 'programming', + extensions: ['.e', '.ex'] + }, + 'F*': { + type: 'programming', + extensions: ['.fst', '.fsti'], + aliases: ['fstar'] + }, + 'F#': { + type: 'programming', + extensions: ['.fs', '.fsi', '.fsx'], + aliases: ['fsharp'] + }, + Factor: { + type: 'programming', + extensions: ['.factor'] + }, + Fancy: { + type: 'programming', + extensions: ['.fy', '.fancypack'] + }, + Fantom: { + type: 'programming', + extensions: ['.fan'] + }, + Faust: { + type: 'programming', + extensions: ['.dsp'] + }, + Fennel: { + type: 'programming', + extensions: ['.fnl'] + }, + 'FIGlet Font': { + type: 'data', + extensions: ['.flf'], + aliases: ['FIGfont'] + }, + 'Filebench WML': { + type: 'programming', + extensions: ['.f'] + }, + Filterscript: { + type: 'programming', + extensions: ['.fs'] + }, + FIRRTL: { + type: 'programming', + extensions: ['.fir'] + }, + fish: { + type: 'programming', + extensions: ['.fish'] + }, + Fluent: { + type: 'programming', + extensions: ['.ftl'] + }, + FLUX: { + type: 'programming', + extensions: ['.fx', '.flux'] + }, + Formatted: { + type: 'data', + extensions: ['.for', '.eam.fs'] + }, + Forth: { + type: 'programming', + extensions: ['.fth', '.4th', '.f', '.for', '.forth', '.fr', '.frt', '.fs'] + }, + Fortran: { + type: 'programming', + extensions: ['.f', '.f77', '.for', '.fpp'] + }, + 'Fortran Free Form': { + type: 'programming', + extensions: ['.f90', '.f03', '.f08', '.f95'] + }, + FreeBASIC: { + type: 'programming', + extensions: ['.bi', '.bas'], + aliases: ['fb'] + }, + FreeMarker: { + type: 'programming', + extensions: ['.ftl', '.ftlh'], + aliases: ['ftl'] + }, + Frege: { + type: 'programming', + extensions: ['.fr'] + }, + Futhark: { + type: 'programming', + extensions: ['.fut'] + }, + 'G-code': { + type: 'programming', + extensions: ['.g', '.cnc', '.gco', '.gcode'] + }, + 'Game Maker Language': { + type: 'programming', + extensions: ['.gml'] + }, + GAML: { + type: 'programming', + extensions: ['.gaml'] + }, + GAMS: { + type: 'programming', + extensions: ['.gms'] + }, + GAP: { + type: 'programming', + extensions: ['.g', '.gap', '.gd', '.gi', '.tst'] + }, + 'GCC Machine Description': { + type: 'programming', + extensions: ['.md'] + }, + GDB: { + type: 'programming', + extensions: ['.gdb', '.gdbinit'] + }, + GDScript: { + type: 'programming', + extensions: ['.gd'] + }, + GDShader: { + type: 'programming', + extensions: ['.gdshader', '.gdshaderinc'] + }, + GEDCOM: { + type: 'data', + extensions: ['.ged'] + }, + Gemini: { + type: 'prose', + extensions: ['.gmi'], + aliases: ['gemtext'] + }, + 'Genero 4gl': { + type: 'programming', + extensions: ['.4gl'] + }, + 'Genero per': { + type: 'markup', + extensions: ['.per'] + }, + Genie: { + type: 'programming', + extensions: ['.gs'] + }, + Genshi: { + type: 'programming', + extensions: ['.kid'], + aliases: ['xml+genshi', 'xml+kid'] + }, + 'Gentoo Ebuild': { + type: 'programming', + extensions: ['.ebuild'] + }, + 'Gentoo Eclass': { + type: 'programming', + extensions: ['.eclass'] + }, + 'Gerber Image': { + type: 'data', + extensions: [ + '.gbr', + '.cmp', + '.gbl', + '.gbo', + '.gbp', + '.gbs', + '.gko', + '.gml', + '.gpb', + '.gpt', + '.gtl', + '.gto', + '.gtp', + '.gts', + '.ncl', + '.sol' + ], + aliases: ['rs-274x'] + }, + 'Gettext Catalog': { + type: 'prose', + extensions: ['.po', '.pot'], + aliases: ['pot'] + }, + Gherkin: { + type: 'programming', + extensions: ['.feature', '.story'], + aliases: ['cucumber'] + }, + 'Git Attributes': { + type: 'data', + aliases: ['gitattributes'] + }, + 'Git Config': { + type: 'data', + extensions: ['.gitconfig'], + aliases: ['gitconfig', 'gitmodules'] + }, + 'Git Revision List': { + type: 'data', + aliases: ['Git Blame Ignore Revs'] + }, + Gleam: { + type: 'programming', + extensions: ['.gleam'] + }, + 'Glimmer JS': { + type: 'programming', + extensions: ['.gjs'] + }, + 'Glimmer TS': { + type: 'programming', + extensions: ['.gts'] + }, + GLSL: { + type: 'programming', + extensions: [ + '.glsl', + '.fp', + '.frag', + '.frg', + '.fs', + '.fsh', + '.fshader', + '.geo', + '.geom', + '.glslf', + '.glslv', + '.gs', + '.gshader', + '.rchit', + '.rmiss', + '.shader', + '.tesc', + '.tese', + '.vert', + '.vrx', + '.vs', + '.vsh', + '.vshader' + ] + }, + Glyph: { + type: 'programming', + extensions: ['.glf'] + }, + 'Glyph Bitmap Distribution Format': { + type: 'data', + extensions: ['.bdf'] + }, + GN: { + type: 'data', + extensions: ['.gn', '.gni'] + }, + Gnuplot: { + type: 'programming', + extensions: ['.gp', '.gnu', '.gnuplot', '.p', '.plot', '.plt'] + }, + Go: { + type: 'programming', + extensions: ['.go'], + aliases: ['golang'] + }, + 'Go Checksums': { + type: 'data', + aliases: ['go.sum', 'go sum', 'go.work.sum', 'go work sum'] + }, + 'Go Module': { + type: 'data', + aliases: ['go.mod', 'go mod'] + }, + 'Go Workspace': { + type: 'data', + aliases: ['go.work', 'go work'] + }, + 'Godot Resource': { + type: 'data', + extensions: ['.gdnlib', '.gdns', '.tres', '.tscn'] + }, + Golo: { + type: 'programming', + extensions: ['.golo'] + }, + Gosu: { + type: 'programming', + extensions: ['.gs', '.gst', '.gsx', '.vark'] + }, + Grace: { + type: 'programming', + extensions: ['.grace'] + }, + Gradle: { + type: 'data', + extensions: ['.gradle'] + }, + 'Gradle Kotlin DSL': { + type: 'data', + extensions: ['.gradle.kts'] + }, + 'Grammatical Framework': { + type: 'programming', + extensions: ['.gf'], + aliases: ['gf'] + }, + 'Graph Modeling Language': { + type: 'data', + extensions: ['.gml'] + }, + GraphQL: { + type: 'data', + extensions: ['.graphql', '.gql', '.graphqls'] + }, + 'Graphviz (DOT)': { + type: 'data', + extensions: ['.dot', '.gv'] + }, + Groovy: { + type: 'programming', + extensions: ['.groovy', '.grt', '.gtpl', '.gvy'] + }, + 'Groovy Server Pages': { + type: 'programming', + extensions: ['.gsp'], + aliases: ['gsp', 'java server page'] + }, + GSC: { + type: 'programming', + extensions: ['.gsc', '.csc', '.gsh'] + }, + Hack: { + type: 'programming', + extensions: ['.hack', '.hh', '.hhi', '.php'] + }, + Haml: { + type: 'markup', + extensions: ['.haml', '.haml.deface'] + }, + Handlebars: { + type: 'markup', + extensions: ['.handlebars', '.hbs'], + aliases: ['hbs', 'htmlbars'] + }, + HAProxy: { + type: 'data', + extensions: ['.cfg'] + }, + Harbour: { + type: 'programming', + extensions: ['.hb'] + }, + Hare: { + type: 'programming', + extensions: ['.ha'] + }, + Haskell: { + type: 'programming', + extensions: ['.hs', '.hs-boot', '.hsc'] + }, + Haxe: { + type: 'programming', + extensions: ['.hx', '.hxsl'] + }, + HCL: { + type: 'programming', + extensions: ['.hcl', '.nomad', '.tf', '.tfvars', '.workflow'], + aliases: ['HashiCorp Configuration Language', 'terraform'] + }, + HIP: { + type: 'programming', + extensions: ['.hip'] + }, + HiveQL: { + type: 'programming', + extensions: ['.q', '.hql'] + }, + HLSL: { + type: 'programming', + extensions: ['.hlsl', '.cginc', '.fx', '.fxh', '.hlsli'] + }, + HOCON: { + type: 'data', + extensions: ['.hocon'] + }, + HolyC: { + type: 'programming', + extensions: ['.hc'] + }, + hoon: { + type: 'programming', + extensions: ['.hoon'] + }, + 'Hosts File': { + type: 'data', + aliases: ['hosts'] + }, + HTML: { + type: 'markup', + extensions: ['.html', '.hta', '.htm', '.html.hl', '.inc', '.xht', '.xhtml'], + aliases: ['xhtml'] + }, + 'HTML+ECR': { + type: 'markup', + extensions: ['.ecr'], + aliases: ['ecr'] + }, + 'HTML+EEX': { + type: 'markup', + extensions: ['.html.eex', '.heex', '.leex'], + aliases: ['eex', 'heex', 'leex'] + }, + 'HTML+ERB': { + type: 'markup', + extensions: ['.erb', '.erb.deface', '.rhtml'], + aliases: ['erb', 'rhtml', 'html+ruby'] + }, + 'HTML+PHP': { + type: 'markup', + extensions: ['.phtml'] + }, + 'HTML+Razor': { + type: 'markup', + extensions: ['.cshtml', '.razor'], + aliases: ['razor'] + }, + HTTP: { + type: 'data', + extensions: ['.http'] + }, + HXML: { + type: 'data', + extensions: ['.hxml'] + }, + Hy: { + type: 'programming', + extensions: ['.hy'], + aliases: ['hylang'] + }, + HyPhy: { + type: 'programming', + extensions: ['.bf'] + }, + iCalendar: { + type: 'data', + extensions: ['.ics', '.ical'], + aliases: ['iCal'] + }, + IDL: { + type: 'programming', + extensions: ['.pro', '.dlm'] + }, + Idris: { + type: 'programming', + extensions: ['.idr', '.lidr'] + }, + 'Ignore List': { + type: 'data', + extensions: ['.gitignore'], + aliases: ['ignore', 'gitignore', 'git-ignore'] + }, + 'IGOR Pro': { + type: 'programming', + extensions: ['.ipf'], + aliases: ['igor', 'igorpro'] + }, + 'ImageJ Macro': { + type: 'programming', + extensions: ['.ijm'], + aliases: ['ijm'] + }, + Imba: { + type: 'programming', + extensions: ['.imba'] + }, + 'Inform 7': { + type: 'programming', + extensions: ['.ni', '.i7x'], + aliases: ['i7', 'inform7'] + }, + INI: { + type: 'data', + extensions: ['.ini', '.cfg', '.cnf', '.dof', '.frm', '.lektorproject', '.prefs', '.pro', '.properties', '.url'], + aliases: ['dosini'] + }, + Ink: { + type: 'programming', + extensions: ['.ink'] + }, + 'Inno Setup': { + type: 'programming', + extensions: ['.iss', '.isl'] + }, + Io: { + type: 'programming', + extensions: ['.io'] + }, + Ioke: { + type: 'programming', + extensions: ['.ik'] + }, + 'IRC log': { + type: 'data', + extensions: ['.irclog', '.weechatlog'], + aliases: ['irc', 'irc logs'] + }, + Isabelle: { + type: 'programming', + extensions: ['.thy'] + }, + ISPC: { + type: 'programming', + extensions: ['.ispc'] + }, + J: { + type: 'programming', + extensions: ['.ijs'] + }, + Jai: { + type: 'programming', + extensions: ['.jai'] + }, + Janet: { + type: 'programming', + extensions: ['.janet'] + }, + Jasmin: { + type: 'programming', + extensions: ['.j'] + }, + Java: { + type: 'programming', + extensions: ['.java', '.jav', '.jsh'] + }, + 'Java Properties': { + type: 'data', + extensions: ['.properties'] + }, + 'Java Server Pages': { + type: 'programming', + extensions: ['.jsp', '.tag'], + aliases: ['jsp'] + }, + 'Java Template Engine': { + type: 'programming', + extensions: ['.jte'], + aliases: ['jte'] + }, + JavaScript: { + type: 'programming', + extensions: [ + '.js', + '._js', + '.bones', + '.cjs', + '.es', + '.es6', + '.frag', + '.gs', + '.jake', + '.javascript', + '.jsb', + '.jscad', + '.jsfl', + '.jslib', + '.jsm', + '.jspre', + '.jss', + '.jsx', + '.mjs', + '.njs', + '.pac', + '.sjs', + '.ssjs', + '.xsjs', + '.xsjslib' + ], + aliases: ['js', 'node'] + }, + 'JavaScript+ERB': { + type: 'programming', + extensions: ['.js.erb'] + }, + JCL: { + type: 'programming', + extensions: ['.jcl'] + }, + 'Jest Snapshot': { + type: 'data', + extensions: ['.snap'] + }, + 'JetBrains MPS': { + type: 'programming', + extensions: ['.mps', '.mpl', '.msd'], + aliases: ['mps'] + }, + JFlex: { + type: 'programming', + extensions: ['.flex', '.jflex'] + }, + Jinja: { + type: 'markup', + extensions: ['.jinja', '.j2', '.jinja2'], + aliases: ['django', 'html+django', 'html+jinja', 'htmldjango'] + }, + Jison: { + type: 'programming', + extensions: ['.jison'] + }, + 'Jison Lex': { + type: 'programming', + extensions: ['.jisonlex'] + }, + Jolie: { + type: 'programming', + extensions: ['.ol', '.iol'] + }, + jq: { + type: 'programming', + extensions: ['.jq'] + }, + JSON: { + type: 'data', + extensions: [ + '.json', + '.4DForm', + '.4DProject', + '.avsc', + '.geojson', + '.gltf', + '.har', + '.ice', + '.JSON-tmLanguage', + '.json.example', + '.jsonl', + '.mcmeta', + '.sarif', + '.tact', + '.tfstate', + '.tfstate.backup', + '.topojson', + '.webapp', + '.webmanifest', + '.yy', + '.yyp' + ], + aliases: ['geojson', 'jsonl', 'sarif', 'topojson'] + }, + 'JSON with Comments': { + type: 'data', + extensions: [ + '.jsonc', + '.code-snippets', + '.code-workspace', + '.sublime-build', + '.sublime-color-scheme', + '.sublime-commands', + '.sublime-completions', + '.sublime-keymap', + '.sublime-macro', + '.sublime-menu', + '.sublime-mousemap', + '.sublime-project', + '.sublime-settings', + '.sublime-theme', + '.sublime-workspace', + '.sublime_metrics', + '.sublime_session' + ], + aliases: ['jsonc'] + }, + JSON5: { + type: 'data', + extensions: ['.json5'] + }, + JSONiq: { + type: 'programming', + extensions: ['.jq'] + }, + JSONLD: { + type: 'data', + extensions: ['.jsonld'] + }, + Jsonnet: { + type: 'programming', + extensions: ['.jsonnet', '.libsonnet'] + }, + Julia: { + type: 'programming', + extensions: ['.jl'] + }, + 'Jupyter Notebook': { + type: 'markup', + extensions: ['.ipynb'], + aliases: ['IPython Notebook'] + }, + Just: { + type: 'programming', + extensions: ['.just'], + aliases: ['Justfile'] + }, + 'Kaitai Struct': { + type: 'programming', + extensions: ['.ksy'], + aliases: ['ksy'] + }, + KakouneScript: { + type: 'programming', + extensions: ['.kak'], + aliases: ['kak', 'kakscript'] + }, + KDL: { + type: 'data', + extensions: ['.kdl'] + }, + KerboScript: { + type: 'programming', + extensions: ['.ks'] + }, + 'KiCad Layout': { + type: 'data', + extensions: ['.kicad_pcb', '.kicad_mod', '.kicad_wks'], + aliases: ['pcbnew'] + }, + 'KiCad Legacy Layout': { + type: 'data', + extensions: ['.brd'] + }, + 'KiCad Schematic': { + type: 'data', + extensions: ['.kicad_sch', '.kicad_sym', '.sch'], + aliases: ['eeschema schematic'] + }, + Kickstart: { + type: 'data', + extensions: ['.ks'] + }, + Kit: { + type: 'markup', + extensions: ['.kit'] + }, + Koka: { + type: 'programming', + extensions: ['.kk'] + }, + Kotlin: { + type: 'programming', + extensions: ['.kt', '.ktm', '.kts'] + }, + KRL: { + type: 'programming', + extensions: ['.krl'] + }, + Kusto: { + type: 'data', + extensions: ['.csl', '.kql'] + }, + kvlang: { + type: 'markup', + extensions: ['.kv'] + }, + LabVIEW: { + type: 'programming', + extensions: ['.lvproj', '.lvclass', '.lvlib'] + }, + Lark: { + type: 'data', + extensions: ['.lark'] + }, + Lasso: { + type: 'programming', + extensions: ['.lasso', '.las', '.lasso8', '.lasso9'], + aliases: ['lassoscript'] + }, + Latte: { + type: 'markup', + extensions: ['.latte'] + }, + Lean: { + type: 'programming', + extensions: ['.lean', '.hlean'] + }, + 'Lean 4': { + type: 'programming', + extensions: ['.lean'] + }, + Leo: { + type: 'programming', + extensions: ['.leo'] + }, + Less: { + type: 'markup', + extensions: ['.less'], + aliases: ['less-css'] + }, + Lex: { + type: 'programming', + extensions: ['.l', '.lex'], + aliases: ['flex'] + }, + LFE: { + type: 'programming', + extensions: ['.lfe'] + }, + LigoLANG: { + type: 'programming', + extensions: ['.ligo'] + }, + LilyPond: { + type: 'programming', + extensions: ['.ly', '.ily'] + }, + Limbo: { + type: 'programming', + extensions: ['.b', '.m'] + }, + 'Linear Programming': { + type: 'programming', + extensions: ['.lp'] + }, + 'Linker Script': { + type: 'programming', + extensions: ['.ld', '.lds', '.x'] + }, + 'Linux Kernel Module': { + type: 'data', + extensions: ['.mod'] + }, + Liquid: { + type: 'markup', + extensions: ['.liquid'] + }, + 'Literate Agda': { + type: 'programming', + extensions: ['.lagda'] + }, + 'Literate CoffeeScript': { + type: 'programming', + extensions: ['.litcoffee', '.coffee.md'], + aliases: ['litcoffee'] + }, + 'Literate Haskell': { + type: 'programming', + extensions: ['.lhs'], + aliases: ['lhaskell', 'lhs'] + }, + 'LiveCode Script': { + type: 'programming', + extensions: ['.livecodescript'] + }, + LiveScript: { + type: 'programming', + extensions: ['.ls', '._ls'], + aliases: ['live-script', 'ls'] + }, + LLVM: { + type: 'programming', + extensions: ['.ll'] + }, + Logos: { + type: 'programming', + extensions: ['.xm', '.x', '.xi'] + }, + Logtalk: { + type: 'programming', + extensions: ['.lgt', '.logtalk'] + }, + LOLCODE: { + type: 'programming', + extensions: ['.lol'] + }, + LookML: { + type: 'programming', + extensions: ['.lkml', '.lookml'] + }, + LoomScript: { + type: 'programming', + extensions: ['.ls'] + }, + LSL: { + type: 'programming', + extensions: ['.lsl', '.lslp'] + }, + 'LTspice Symbol': { + type: 'data', + extensions: ['.asy'] + }, + Lua: { + type: 'programming', + extensions: ['.lua', '.fcgi', '.nse', '.p8', '.pd_lua', '.rbxs', '.rockspec', '.wlua'] + }, + Luau: { + type: 'programming', + extensions: ['.luau'] + }, + M: { + type: 'programming', + extensions: ['.mumps', '.m'], + aliases: ['mumps'] + }, + M3U: { + type: 'data', + extensions: ['.m3u', '.m3u8'], + aliases: ['hls playlist', 'm3u playlist'] + }, + M4: { + type: 'programming', + extensions: ['.m4', '.mc'] + }, + M4Sugar: { + type: 'programming', + extensions: ['.m4'], + aliases: ['autoconf'] + }, + Macaulay2: { + type: 'programming', + extensions: ['.m2'], + aliases: ['m2'] + }, + Makefile: { + type: 'programming', + extensions: ['.mak', '.d', '.make', '.makefile', '.mk', '.mkfile'], + aliases: ['bsdmake', 'make', 'mf'] + }, + Mako: { + type: 'programming', + extensions: ['.mako', '.mao'] + }, + Markdown: { + type: 'prose', + extensions: [ + '.md', + '.livemd', + '.markdown', + '.mdown', + '.mdwn', + '.mkd', + '.mkdn', + '.mkdown', + '.ronn', + '.scd', + '.workbook' + ], + aliases: ['md', 'pandoc'] + }, + Marko: { + type: 'markup', + extensions: ['.marko'], + aliases: ['markojs'] + }, + Mask: { + type: 'markup', + extensions: ['.mask'] + }, + Mathematica: { + type: 'programming', + extensions: ['.mathematica', '.cdf', '.m', '.ma', '.mt', '.nb', '.nbp', '.wl', '.wlt'], + aliases: ['mma', 'wolfram', 'wolfram language', 'wolfram lang', 'wl'] + }, + MATLAB: { + type: 'programming', + extensions: ['.matlab', '.m'], + aliases: ['octave'] + }, + Max: { + type: 'programming', + extensions: ['.maxpat', '.maxhelp', '.maxproj', '.mxt', '.pat'], + aliases: ['max/msp', 'maxmsp'] + }, + MAXScript: { + type: 'programming', + extensions: ['.ms', '.mcr'] + }, + mcfunction: { + type: 'programming', + extensions: ['.mcfunction'] + }, + mdsvex: { + type: 'markup', + extensions: ['.svx'] + }, + MDX: { + type: 'markup', + extensions: ['.mdx'] + }, + Mercury: { + type: 'programming', + extensions: ['.m', '.moo'] + }, + Mermaid: { + type: 'markup', + extensions: ['.mmd', '.mermaid'], + aliases: ['mermaid example'] + }, + Metal: { + type: 'programming', + extensions: ['.metal'] + }, + 'Microsoft Developer Studio Project': { + type: 'data', + extensions: ['.dsp'] + }, + 'Microsoft Visual Studio Solution': { + type: 'data', + extensions: ['.sln'] + }, + MiniD: { + type: 'programming', + extensions: ['.minid'] + }, + MiniYAML: { + type: 'data', + extensions: ['.yaml', '.yml'] + }, + MiniZinc: { + type: 'programming', + extensions: ['.mzn'] + }, + 'MiniZinc Data': { + type: 'data', + extensions: ['.dzn'] + }, + Mint: { + type: 'programming', + extensions: ['.mint'] + }, + Mirah: { + type: 'programming', + extensions: ['.druby', '.duby', '.mirah'] + }, + 'mIRC Script': { + type: 'programming', + extensions: ['.mrc'] + }, + MLIR: { + type: 'programming', + extensions: ['.mlir'] + }, + Modelica: { + type: 'programming', + extensions: ['.mo'] + }, + 'Modula-2': { + type: 'programming', + extensions: ['.mod'] + }, + 'Modula-3': { + type: 'programming', + extensions: ['.i3', '.ig', '.m3', '.mg'] + }, + 'Module Management System': { + type: 'programming', + extensions: ['.mms', '.mmk'] + }, + Mojo: { + type: 'programming', + extensions: ['.mojo'] + }, + Monkey: { + type: 'programming', + extensions: ['.monkey', '.monkey2'] + }, + 'Monkey C': { + type: 'programming', + extensions: ['.mc'] + }, + Moocode: { + type: 'programming', + extensions: ['.moo'] + }, + MoonBit: { + type: 'programming', + extensions: ['.mbt'] + }, + MoonScript: { + type: 'programming', + extensions: ['.moon'] + }, + Motoko: { + type: 'programming', + extensions: ['.mo'] + }, + 'Motorola 68K Assembly': { + type: 'programming', + extensions: ['.asm', '.i', '.inc', '.s', '.x68'], + aliases: ['m68k'] + }, + Move: { + type: 'programming', + extensions: ['.move'] + }, + MQL4: { + type: 'programming', + extensions: ['.mq4', '.mqh'] + }, + MQL5: { + type: 'programming', + extensions: ['.mq5', '.mqh'] + }, + MTML: { + type: 'markup', + extensions: ['.mtml'] + }, + MUF: { + type: 'programming', + extensions: ['.muf', '.m'] + }, + mupad: { + type: 'programming', + extensions: ['.mu'] + }, + Muse: { + type: 'prose', + extensions: ['.muse'], + aliases: ['amusewiki', 'emacs muse'] + }, + Mustache: { + type: 'markup', + extensions: ['.mustache'] + }, + Myghty: { + type: 'programming', + extensions: ['.myt'] + }, + nanorc: { + type: 'data', + extensions: ['.nanorc'] + }, + Nasal: { + type: 'programming', + extensions: ['.nas'] + }, + NASL: { + type: 'programming', + extensions: ['.nasl', '.inc'] + }, + NCL: { + type: 'programming', + extensions: ['.ncl'] + }, + Nearley: { + type: 'programming', + extensions: ['.ne', '.nearley'] + }, + Nemerle: { + type: 'programming', + extensions: ['.n'] + }, + NEON: { + type: 'data', + extensions: ['.neon'], + aliases: ['nette object notation', 'ne-on'] + }, + nesC: { + type: 'programming', + extensions: ['.nc'] + }, + NetLinx: { + type: 'programming', + extensions: ['.axs', '.axi'] + }, + 'NetLinx+ERB': { + type: 'programming', + extensions: ['.axs.erb', '.axi.erb'] + }, + NetLogo: { + type: 'programming', + extensions: ['.nlogo'] + }, + NewLisp: { + type: 'programming', + extensions: ['.nl', '.lisp', '.lsp'] + }, + Nextflow: { + type: 'programming', + extensions: ['.nf'] + }, + Nginx: { + type: 'data', + extensions: ['.nginx', '.nginxconf', '.vhost'], + aliases: ['nginx configuration file'] + }, + Nickel: { + type: 'programming', + extensions: ['.ncl'] + }, + Nim: { + type: 'programming', + extensions: ['.nim', '.nim.cfg', '.nimble', '.nimrod', '.nims'] + }, + Ninja: { + type: 'data', + extensions: ['.ninja'] + }, + Nit: { + type: 'programming', + extensions: ['.nit'] + }, + Nix: { + type: 'programming', + extensions: ['.nix'], + aliases: ['nixos'] + }, + NL: { + type: 'data', + extensions: ['.nl'] + }, + NMODL: { + type: 'programming', + extensions: ['.mod'] + }, + Noir: { + type: 'programming', + extensions: ['.nr'], + aliases: ['nargo'] + }, + 'NPM Config': { + type: 'data', + aliases: ['npmrc'] + }, + NSIS: { + type: 'programming', + extensions: ['.nsi', '.nsh'] + }, + Nu: { + type: 'programming', + extensions: ['.nu'], + aliases: ['nush'] + }, + NumPy: { + type: 'programming', + extensions: ['.numpy', '.numpyw', '.numsc'] + }, + Nunjucks: { + type: 'markup', + extensions: ['.njk'], + aliases: ['njk'] + }, + Nushell: { + type: 'programming', + extensions: ['.nu'], + aliases: ['nu-script', 'nushell-script'] + }, + NWScript: { + type: 'programming', + extensions: ['.nss'] + }, + 'OASv2-json': { + type: 'data', + extensions: ['.json'] + }, + 'OASv2-yaml': { + type: 'data', + extensions: ['.yaml', '.yml'] + }, + 'OASv3-json': { + type: 'data', + extensions: ['.json'] + }, + 'OASv3-yaml': { + type: 'data', + extensions: ['.yaml', '.yml'] + }, + Oberon: { + type: 'programming', + extensions: ['.ob2'] + }, + ObjDump: { + type: 'data', + extensions: ['.objdump'] + }, + 'Object Data Instance Notation': { + type: 'data', + extensions: ['.odin'] + }, + 'Objective-C': { + type: 'programming', + extensions: ['.m', '.h'], + aliases: ['obj-c', 'objc', 'objectivec'] + }, + 'Objective-C++': { + type: 'programming', + extensions: ['.mm'], + aliases: ['obj-c++', 'objc++', 'objectivec++'] + }, + 'Objective-J': { + type: 'programming', + extensions: ['.j', '.sj'], + aliases: ['obj-j', 'objectivej', 'objj'] + }, + ObjectScript: { + type: 'programming', + extensions: ['.cls'] + }, + OCaml: { + type: 'programming', + extensions: ['.ml', '.eliom', '.eliomi', '.ml4', '.mli', '.mll', '.mly'] + }, + Odin: { + type: 'programming', + extensions: ['.odin'], + aliases: ['odinlang', 'odin-lang'] + }, + Omgrofl: { + type: 'programming', + extensions: ['.omgrofl'] + }, + 'OMNeT++ MSG': { + type: 'programming', + extensions: ['.msg'], + aliases: ['omnetpp-msg'] + }, + 'OMNeT++ NED': { + type: 'programming', + extensions: ['.ned'], + aliases: ['omnetpp-ned'] + }, + ooc: { + type: 'programming', + extensions: ['.ooc'] + }, + Opa: { + type: 'programming', + extensions: ['.opa'] + }, + Opal: { + type: 'programming', + extensions: ['.opal'] + }, + 'Open Policy Agent': { + type: 'programming', + extensions: ['.rego'] + }, + 'OpenAPI Specification v2': { + type: 'data', + aliases: ['oasv2'] + }, + 'OpenAPI Specification v3': { + type: 'data', + aliases: ['oasv3'] + }, + OpenCL: { + type: 'programming', + extensions: ['.cl', '.opencl'] + }, + 'OpenEdge ABL': { + type: 'programming', + extensions: ['.p', '.cls', '.w'], + aliases: ['progress', 'openedge', 'abl'] + }, + OpenQASM: { + type: 'programming', + extensions: ['.qasm'] + }, + 'OpenRC runscript': { + type: 'programming', + aliases: ['openrc'] + }, + OpenSCAD: { + type: 'programming', + extensions: ['.scad'] + }, + 'OpenStep Property List': { + type: 'data', + extensions: ['.plist', '.glyphs'] + }, + 'OpenType Feature File': { + type: 'data', + extensions: ['.fea'], + aliases: ['AFDKO'] + }, + 'Option List': { + type: 'data', + aliases: ['opts', 'ackrc'] + }, + Org: { + type: 'prose', + extensions: ['.org'] + }, + OverpassQL: { + type: 'programming', + extensions: ['.overpassql'] + }, + Ox: { + type: 'programming', + extensions: ['.ox', '.oxh', '.oxo'] + }, + Oxygene: { + type: 'programming', + extensions: ['.oxygene'] + }, + Oz: { + type: 'programming', + extensions: ['.oz'] + }, + P4: { + type: 'programming', + extensions: ['.p4'] + }, + Pact: { + type: 'programming', + extensions: ['.pact'] + }, + Pan: { + type: 'programming', + extensions: ['.pan'] + }, + Papyrus: { + type: 'programming', + extensions: ['.psc'] + }, + Parrot: { + type: 'programming', + extensions: ['.parrot'] + }, + 'Parrot Assembly': { + type: 'programming', + extensions: ['.pasm'], + aliases: ['pasm'] + }, + 'Parrot Internal Representation': { + type: 'programming', + extensions: ['.pir'], + aliases: ['pir'] + }, + Pascal: { + type: 'programming', + extensions: ['.pas', '.dfm', '.dpr', '.inc', '.lpr', '.pascal', '.pp'], + aliases: ['delphi', 'objectpascal'] + }, + Pawn: { + type: 'programming', + extensions: ['.pwn', '.inc', '.sma'] + }, + PDDL: { + type: 'programming', + extensions: ['.pddl'] + }, + 'PEG.js': { + type: 'programming', + extensions: ['.pegjs', '.peggy'] + }, + Pep8: { + type: 'programming', + extensions: ['.pep'] + }, + Perl: { + type: 'programming', + extensions: ['.pl', '.al', '.cgi', '.fcgi', '.perl', '.ph', '.plx', '.pm', '.psgi', '.t'], + aliases: ['cperl'] + }, + PHP: { + type: 'programming', + extensions: ['.php', '.aw', '.ctp', '.fcgi', '.inc', '.php3', '.php4', '.php5', '.phps', '.phpt'], + aliases: ['inc'] + }, + Pic: { + type: 'markup', + extensions: ['.pic', '.chem'], + aliases: ['pikchr'] + }, + Pickle: { + type: 'data', + extensions: ['.pkl'] + }, + PicoLisp: { + type: 'programming', + extensions: ['.l'] + }, + PigLatin: { + type: 'programming', + extensions: ['.pig'] + }, + Pike: { + type: 'programming', + extensions: ['.pike', '.pmod'] + }, + Pkl: { + type: 'programming', + extensions: ['.pkl'] + }, + PlantUML: { + type: 'data', + extensions: ['.puml', '.iuml', '.plantuml'] + }, + PLpgSQL: { + type: 'programming', + extensions: ['.pgsql', '.sql'] + }, + PLSQL: { + type: 'programming', + extensions: [ + '.pls', + '.bdy', + '.ddl', + '.fnc', + '.pck', + '.pkb', + '.pks', + '.plb', + '.plsql', + '.prc', + '.spc', + '.sql', + '.tpb', + '.tps', + '.trg', + '.vw' + ] + }, + Pod: { + type: 'prose', + extensions: ['.pod'] + }, + 'Pod 6': { + type: 'prose', + extensions: ['.pod', '.pod6'] + }, + PogoScript: { + type: 'programming', + extensions: ['.pogo'] + }, + Polar: { + type: 'programming', + extensions: ['.polar'] + }, + Pony: { + type: 'programming', + extensions: ['.pony'] + }, + Portugol: { + type: 'programming', + extensions: ['.por'] + }, + PostCSS: { + type: 'markup', + extensions: ['.pcss', '.postcss'] + }, + PostScript: { + type: 'markup', + extensions: ['.ps', '.eps', '.epsi', '.pfa'], + aliases: ['postscr'] + }, + 'POV-Ray SDL': { + type: 'programming', + extensions: ['.pov', '.inc'], + aliases: ['pov-ray', 'povray'] + }, + PowerBuilder: { + type: 'programming', + extensions: ['.pbt', '.sra', '.sru', '.srw'] + }, + PowerShell: { + type: 'programming', + extensions: ['.ps1', '.psd1', '.psm1'], + aliases: ['posh', 'pwsh'] + }, + Praat: { + type: 'programming', + extensions: ['.praat'] + }, + Prisma: { + type: 'data', + extensions: ['.prisma'] + }, + Processing: { + type: 'programming', + extensions: ['.pde'] + }, + Proguard: { + type: 'data', + extensions: ['.pro'] + }, + Prolog: { + type: 'programming', + extensions: ['.pl', '.plt', '.pro', '.prolog', '.yap'] + }, + Promela: { + type: 'programming', + extensions: ['.pml'] + }, + 'Propeller Spin': { + type: 'programming', + extensions: ['.spin'] + }, + 'Protocol Buffer': { + type: 'data', + extensions: ['.proto'], + aliases: ['proto', 'protobuf', 'Protocol Buffers'] + }, + 'Protocol Buffer Text Format': { + type: 'data', + extensions: ['.textproto', '.pbt', '.pbtxt'], + aliases: ['text proto', 'protobuf text format'] + }, + 'Public Key': { + type: 'data', + extensions: ['.asc', '.pub'] + }, + Pug: { + type: 'markup', + extensions: ['.jade', '.pug'] + }, + Puppet: { + type: 'programming', + extensions: ['.pp'] + }, + 'Pure Data': { + type: 'data', + extensions: ['.pd'] + }, + PureBasic: { + type: 'programming', + extensions: ['.pb', '.pbi'] + }, + PureScript: { + type: 'programming', + extensions: ['.purs'] + }, + Pyret: { + type: 'programming', + extensions: ['.arr'] + }, + Python: { + type: 'programming', + extensions: [ + '.py', + '.cgi', + '.fcgi', + '.gyp', + '.gypi', + '.lmi', + '.py3', + '.pyde', + '.pyi', + '.pyp', + '.pyt', + '.pyw', + '.rpy', + '.spec', + '.tac', + '.wsgi', + '.xpy' + ], + aliases: ['python3', 'rusthon'] + }, + 'Python console': { + type: 'programming', + aliases: ['pycon'] + }, + 'Python traceback': { + type: 'data', + extensions: ['.pytb'] + }, + q: { + type: 'programming', + extensions: ['.q'] + }, + 'Q#': { + type: 'programming', + extensions: ['.qs'], + aliases: ['qsharp'] + }, + QMake: { + type: 'programming', + extensions: ['.pro', '.pri'] + }, + QML: { + type: 'programming', + extensions: ['.qml', '.qbs'] + }, + 'Qt Script': { + type: 'programming', + extensions: ['.qs'] + }, + QuakeC: { + type: 'programming', + extensions: ['.qc'] + }, + QuickBASIC: { + type: 'programming', + extensions: ['.bas'], + aliases: ['qb', 'qbasic', 'qb64', 'classic qbasic', 'classic quickbasic'] + }, + R: { + type: 'programming', + extensions: ['.r', '.rd', '.rsx'], + aliases: ['Rscript', 'splus'] + }, + Racket: { + type: 'programming', + extensions: ['.rkt', '.rktd', '.rktl', '.scrbl'] + }, + Ragel: { + type: 'programming', + extensions: ['.rl'], + aliases: ['ragel-rb', 'ragel-ruby'] + }, + Raku: { + type: 'programming', + extensions: [ + '.6pl', + '.6pm', + '.nqp', + '.p6', + '.p6l', + '.p6m', + '.pl', + '.pl6', + '.pm', + '.pm6', + '.raku', + '.rakumod', + '.t' + ], + aliases: ['perl6', 'perl-6'] + }, + RAML: { + type: 'markup', + extensions: ['.raml'] + }, + Rascal: { + type: 'programming', + extensions: ['.rsc'] + }, + 'Raw token data': { + type: 'data', + extensions: ['.raw'], + aliases: ['raw'] + }, + RBS: { + type: 'data', + extensions: ['.rbs'] + }, + RDoc: { + type: 'prose', + extensions: ['.rdoc'] + }, + 'Readline Config': { + type: 'data', + aliases: ['inputrc', 'readline'] + }, + REALbasic: { + type: 'programming', + extensions: ['.rbbas', '.rbfrm', '.rbmnu', '.rbres', '.rbtbar', '.rbuistate'] + }, + Reason: { + type: 'programming', + extensions: ['.re', '.rei'] + }, + ReasonLIGO: { + type: 'programming', + extensions: ['.religo'] + }, + Rebol: { + type: 'programming', + extensions: ['.reb', '.r', '.r2', '.r3', '.rebol'] + }, + Red: { + type: 'programming', + extensions: ['.red', '.reds'], + aliases: ['red/system'] + }, + Redcode: { + type: 'programming', + extensions: ['.cw'] + }, + 'Redirect Rules': { + type: 'data', + aliases: ['redirects'] + }, + 'Regular Expression': { + type: 'data', + extensions: ['.regexp', '.regex'], + aliases: ['regexp', 'regex'] + }, + "Ren'Py": { + type: 'programming', + extensions: ['.rpy'], + aliases: ['renpy'] + }, + RenderScript: { + type: 'programming', + extensions: ['.rs', '.rsh'] + }, + ReScript: { + type: 'programming', + extensions: ['.res', '.resi'] + }, + reStructuredText: { + type: 'prose', + extensions: ['.rst', '.rest', '.rest.txt', '.rst.txt'], + aliases: ['rst'] + }, + REXX: { + type: 'programming', + extensions: ['.rexx', '.pprx', '.rex'], + aliases: ['arexx'] + }, + Rez: { + type: 'programming', + extensions: ['.r'] + }, + 'Rich Text Format': { + type: 'markup', + extensions: ['.rtf'] + }, + Ring: { + type: 'programming', + extensions: ['.ring'] + }, + Riot: { + type: 'markup', + extensions: ['.riot'] + }, + RMarkdown: { + type: 'prose', + extensions: ['.qmd', '.rmd'] + }, + RobotFramework: { + type: 'programming', + extensions: ['.robot', '.resource'] + }, + 'robots.txt': { + type: 'data', + aliases: ['robots', 'robots txt'] + }, + Roc: { + type: 'programming', + extensions: ['.roc'] + }, + 'Rocq Prover': { + type: 'programming', + extensions: ['.v', '.coq'], + aliases: ['coq', 'rocq'] + }, + Roff: { + type: 'markup', + extensions: [ + '.roff', + '.1', + '.1in', + '.1m', + '.1x', + '.2', + '.3', + '.3in', + '.3m', + '.3p', + '.3pm', + '.3qt', + '.3x', + '.4', + '.5', + '.6', + '.7', + '.8', + '.9', + '.l', + '.man', + '.mdoc', + '.me', + '.ms', + '.n', + '.nr', + '.rno', + '.tmac' + ], + aliases: ['groff', 'man', 'manpage', 'man page', 'man-page', 'mdoc', 'nroff', 'troff'] + }, + 'Roff Manpage': { + type: 'markup', + extensions: [ + '.1', + '.1in', + '.1m', + '.1x', + '.2', + '.3', + '.3in', + '.3m', + '.3p', + '.3pm', + '.3qt', + '.3x', + '.4', + '.5', + '.6', + '.7', + '.8', + '.9', + '.man', + '.mdoc' + ] + }, + RON: { + type: 'data', + extensions: ['.ron'] + }, + 'ROS Interface': { + type: 'data', + extensions: ['.msg', '.action', '.srv'], + aliases: ['rosmsg'] + }, + Rouge: { + type: 'programming', + extensions: ['.rg'] + }, + 'RouterOS Script': { + type: 'programming', + extensions: ['.rsc'] + }, + RPC: { + type: 'programming', + extensions: ['.x'], + aliases: ['rpcgen', 'oncrpc', 'xdr'] + }, + RPGLE: { + type: 'programming', + extensions: ['.rpgle', '.sqlrpgle'], + aliases: ['ile rpg', 'sqlrpgle'] + }, + 'RPM Spec': { + type: 'data', + extensions: ['.spec'], + aliases: ['specfile'] + }, + Ruby: { + type: 'programming', + extensions: [ + '.rb', + '.builder', + '.eye', + '.fcgi', + '.gemspec', + '.god', + '.jbuilder', + '.mspec', + '.pluginspec', + '.podspec', + '.prawn', + '.rabl', + '.rake', + '.rbi', + '.rbuild', + '.rbw', + '.rbx', + '.ru', + '.ruby', + '.spec', + '.thor', + '.watchr' + ], + aliases: ['jruby', 'macruby', 'rake', 'rb', 'rbx'] + }, + RUNOFF: { + type: 'markup', + extensions: ['.rnh', '.rno'] + }, + Rust: { + type: 'programming', + extensions: ['.rs', '.rs.in'], + aliases: ['rs'] + }, + Sage: { + type: 'programming', + extensions: ['.sage', '.sagews'] + }, + Sail: { + type: 'programming', + extensions: ['.sail'] + }, + SaltStack: { + type: 'programming', + extensions: ['.sls'], + aliases: ['saltstate', 'salt'] + }, + SAS: { + type: 'programming', + extensions: ['.sas'] + }, + Sass: { + type: 'markup', + extensions: ['.sass'] + }, + Scala: { + type: 'programming', + extensions: ['.scala', '.kojo', '.sbt', '.sc'] + }, + Scaml: { + type: 'markup', + extensions: ['.scaml'] + }, + Scenic: { + type: 'programming', + extensions: ['.scenic'] + }, + Scheme: { + type: 'programming', + extensions: ['.scm', '.sch', '.sld', '.sls', '.sps', '.ss'] + }, + Scilab: { + type: 'programming', + extensions: ['.sci', '.sce', '.tst'] + }, + SCSS: { + type: 'markup', + extensions: ['.scss'] + }, + sed: { + type: 'programming', + extensions: ['.sed'] + }, + Self: { + type: 'programming', + extensions: ['.self'] + }, + 'SELinux Policy': { + type: 'data', + extensions: ['.te'], + aliases: ['SELinux Kernel Policy Language', 'sepolicy'] + }, + ShaderLab: { + type: 'programming', + extensions: ['.shader'] + }, + Shell: { + type: 'programming', + extensions: [ + '.sh', + '.bash', + '.bats', + '.cgi', + '.command', + '.fcgi', + '.ksh', + '.sh.in', + '.tmux', + '.tool', + '.trigger', + '.zsh', + '.zsh-theme' + ], + aliases: ['sh', 'shell-script', 'bash', 'zsh', 'envrc'] + }, + 'ShellCheck Config': { + type: 'data', + aliases: ['shellcheckrc'] + }, + ShellSession: { + type: 'programming', + extensions: ['.sh-session'], + aliases: ['bash session', 'console'] + }, + Shen: { + type: 'programming', + extensions: ['.shen'] + }, + Sieve: { + type: 'programming', + extensions: ['.sieve'] + }, + 'Simple File Verification': { + type: 'data', + extensions: ['.sfv'], + aliases: ['sfv'] + }, + Slang: { + type: 'programming', + extensions: ['.slang'] + }, + Slash: { + type: 'programming', + extensions: ['.sl'] + }, + Slice: { + type: 'programming', + extensions: ['.ice'] + }, + Slim: { + type: 'markup', + extensions: ['.slim'] + }, + Slint: { + type: 'markup', + extensions: ['.slint'] + }, + Smali: { + type: 'programming', + extensions: ['.smali'] + }, + Smalltalk: { + type: 'programming', + extensions: ['.st', '.cs'], + aliases: ['squeak'] + }, + Smarty: { + type: 'programming', + extensions: ['.tpl'] + }, + Smithy: { + type: 'programming', + extensions: ['.smithy'] + }, + SmPL: { + type: 'programming', + extensions: ['.cocci'], + aliases: ['coccinelle'] + }, + SMT: { + type: 'programming', + extensions: ['.smt2', '.smt', '.z3'] + }, + Snakemake: { + type: 'programming', + extensions: ['.smk', '.snakefile'], + aliases: ['snakefile'] + }, + Solidity: { + type: 'programming', + extensions: ['.sol'] + }, + SourcePawn: { + type: 'programming', + extensions: ['.sp', '.inc'], + aliases: ['sourcemod'] + }, + SPARQL: { + type: 'data', + extensions: ['.sparql', '.rq'] + }, + 'Spline Font Database': { + type: 'data', + extensions: ['.sfd'] + }, + SQF: { + type: 'programming', + extensions: ['.sqf', '.hqf'] + }, + SQL: { + type: 'data', + extensions: ['.sql', '.cql', '.ddl', '.inc', '.mysql', '.prc', '.tab', '.udf', '.viw'] + }, + SQLPL: { + type: 'programming', + extensions: ['.sql', '.db2'] + }, + Squirrel: { + type: 'programming', + extensions: ['.nut'] + }, + 'SRecode Template': { + type: 'markup', + extensions: ['.srt'] + }, + 'SSH Config': { + type: 'data', + aliases: ['sshconfig', 'sshdconfig', 'ssh_config', 'sshd_config'] + }, + Stan: { + type: 'programming', + extensions: ['.stan'] + }, + 'Standard ML': { + type: 'programming', + extensions: ['.ml', '.fun', '.sig', '.sml'], + aliases: ['sml'] + }, + STAR: { + type: 'data', + extensions: ['.star'] + }, + Starlark: { + type: 'programming', + extensions: ['.bzl', '.star'], + aliases: ['bazel', 'bzl'] + }, + Stata: { + type: 'programming', + extensions: ['.do', '.ado', '.doh', '.ihlp', '.mata', '.matah', '.sthlp'] + }, + STL: { + type: 'data', + extensions: ['.stl'], + aliases: ['ascii stl', 'stla'] + }, + STON: { + type: 'data', + extensions: ['.ston'] + }, + StringTemplate: { + type: 'markup', + extensions: ['.st'] + }, + Stylus: { + type: 'markup', + extensions: ['.styl'] + }, + 'SubRip Text': { + type: 'data', + extensions: ['.srt'] + }, + SugarSS: { + type: 'markup', + extensions: ['.sss'] + }, + SuperCollider: { + type: 'programming', + extensions: ['.sc', '.scd'] + }, + 'Survex data': { + type: 'data', + extensions: ['.svx'] + }, + Svelte: { + type: 'markup', + extensions: ['.svelte'] + }, + SVG: { + type: 'data', + extensions: ['.svg'] + }, + Sway: { + type: 'programming', + extensions: ['.sw'] + }, + Sweave: { + type: 'prose', + extensions: ['.rnw'] + }, + Swift: { + type: 'programming', + extensions: ['.swift'] + }, + SWIG: { + type: 'programming', + extensions: ['.i', '.swg', '.swig'] + }, + SystemVerilog: { + type: 'programming', + extensions: ['.sv', '.svh', '.vh'] + }, + Tact: { + type: 'programming', + extensions: ['.tact'] + }, + Talon: { + type: 'programming', + extensions: ['.talon'] + }, + Tcl: { + type: 'programming', + extensions: ['.tcl', '.adp', '.sdc', '.tcl.in', '.tm', '.xdc'], + aliases: ['sdc', 'xdc'] + }, + Tcsh: { + type: 'programming', + extensions: ['.tcsh', '.csh'] + }, + Tea: { + type: 'markup', + extensions: ['.tea'] + }, + templ: { + type: 'markup', + extensions: ['.templ'] + }, + Terra: { + type: 'programming', + extensions: ['.t'] + }, + 'Terraform Template': { + type: 'markup', + extensions: ['.tftpl'] + }, + TeX: { + type: 'markup', + extensions: [ + '.tex', + '.aux', + '.bbx', + '.cbx', + '.cls', + '.dtx', + '.ins', + '.lbx', + '.ltx', + '.mkii', + '.mkiv', + '.mkvi', + '.sty', + '.toc' + ], + aliases: ['latex'] + }, + Texinfo: { + type: 'prose', + extensions: ['.texinfo', '.texi', '.txi'] + }, + Text: { + type: 'prose', + extensions: ['.txt', '.fr', '.nb', '.ncl', '.no'], + aliases: ['fundamental', 'plain text'] + }, + TextGrid: { + type: 'data', + extensions: ['.TextGrid'] + }, + Textile: { + type: 'prose', + extensions: ['.textile'] + }, + 'TextMate Properties': { + type: 'data', + aliases: ['tm-properties'] + }, + Thrift: { + type: 'programming', + extensions: ['.thrift'] + }, + 'TI Program': { + type: 'programming', + extensions: ['.8xp', '.8xp.txt'] + }, + 'TL-Verilog': { + type: 'programming', + extensions: ['.tlv'] + }, + TLA: { + type: 'programming', + extensions: ['.tla'] + }, + Toit: { + type: 'programming', + extensions: ['.toit'] + }, + TOML: { + type: 'data', + extensions: ['.toml', '.toml.example'] + }, + 'Tor Config': { + type: 'data', + aliases: ['torrc'] + }, + 'Tree-sitter Query': { + type: 'programming', + extensions: ['.scm'], + aliases: ['tsq'] + }, + 'TSPLIB data': { + type: 'data', + extensions: ['.tsp'], + aliases: ['travelling salesman problem', 'traveling salesman problem'] + }, + TSQL: { + type: 'programming', + extensions: ['.sql'] + }, + TSV: { + type: 'data', + extensions: ['.tsv', '.vcf'], + aliases: ['tab-seperated values'] + }, + TSX: { + type: 'programming', + extensions: ['.tsx'] + }, + Turing: { + type: 'programming', + extensions: ['.t', '.tu'] + }, + Turtle: { + type: 'data', + extensions: ['.ttl'] + }, + Twig: { + type: 'markup', + extensions: ['.twig'] + }, + TXL: { + type: 'programming', + extensions: ['.txl'] + }, + 'Type Language': { + type: 'data', + extensions: ['.tl'], + aliases: ['tl'] + }, + TypeScript: { + type: 'programming', + extensions: ['.ts', '.cts', '.mts'], + aliases: ['ts'] + }, + TypeSpec: { + type: 'programming', + extensions: ['.tsp'], + aliases: ['tsp'] + }, + Typst: { + type: 'programming', + extensions: ['.typ'], + aliases: ['typ'] + }, + 'Unified Parallel C': { + type: 'programming', + extensions: ['.upc'] + }, + 'Unity3D Asset': { + type: 'data', + extensions: ['.anim', '.asset', '.mask', '.mat', '.meta', '.prefab', '.unity'] + }, + 'Unix Assembly': { + type: 'programming', + extensions: ['.s', '.ms'], + aliases: ['gas', 'gnu asm', 'unix asm'] + }, + Uno: { + type: 'programming', + extensions: ['.uno'] + }, + UnrealScript: { + type: 'programming', + extensions: ['.uc'] + }, + 'Untyped Plutus Core': { + type: 'programming', + extensions: ['.uplc'] + }, + UrWeb: { + type: 'programming', + extensions: ['.ur', '.urs'], + aliases: ['Ur/Web', 'Ur'] + }, + V: { + type: 'programming', + extensions: ['.v'], + aliases: ['vlang'] + }, + Vala: { + type: 'programming', + extensions: ['.vala', '.vapi'] + }, + 'Valve Data Format': { + type: 'data', + extensions: ['.vdf'], + aliases: ['keyvalues', 'vdf'] + }, + VBA: { + type: 'programming', + extensions: ['.bas', '.cls', '.frm', '.vba'], + aliases: ['visual basic for applications'] + }, + VBScript: { + type: 'programming', + extensions: ['.vbs'] + }, + vCard: { + type: 'data', + extensions: ['.vcf'], + aliases: ['virtual contact file', 'electronic business card'] + }, + VCL: { + type: 'programming', + extensions: ['.vcl'] + }, + 'Velocity Template Language': { + type: 'markup', + extensions: ['.vtl'], + aliases: ['vtl', 'velocity'] + }, + Vento: { + type: 'markup', + extensions: ['.vto'] + }, + Verilog: { + type: 'programming', + extensions: ['.v', '.veo'] + }, + VHDL: { + type: 'programming', + extensions: ['.vhdl', '.vhd', '.vhf', '.vhi', '.vho', '.vhs', '.vht', '.vhw'] + }, + 'Vim Help File': { + type: 'prose', + extensions: ['.txt'], + aliases: ['help', 'vimhelp'] + }, + 'Vim Script': { + type: 'programming', + extensions: ['.vim', '.vba', '.vimrc', '.vmb'], + aliases: ['vim', 'viml', 'nvim', 'vimscript'] + }, + 'Vim Snippet': { + type: 'markup', + extensions: ['.snip', '.snippet', '.snippets'], + aliases: ['SnipMate', 'UltiSnip', 'UltiSnips', 'NeoSnippet'] + }, + 'Visual Basic .NET': { + type: 'programming', + extensions: ['.vb', '.vbhtml'], + aliases: ['visual basic', 'vbnet', 'vb .net', 'vb.net'] + }, + 'Visual Basic 6.0': { + type: 'programming', + extensions: ['.bas', '.cls', '.ctl', '.Dsr', '.frm'], + aliases: ['vb6', 'vb 6', 'visual basic 6', 'visual basic classic', 'classic visual basic'] + }, + Volt: { + type: 'programming', + extensions: ['.volt'] + }, + Vue: { + type: 'markup', + extensions: ['.vue'] + }, + Vyper: { + type: 'programming', + extensions: ['.vy'] + }, + 'Wavefront Material': { + type: 'data', + extensions: ['.mtl'] + }, + 'Wavefront Object': { + type: 'data', + extensions: ['.obj'] + }, + WDL: { + type: 'programming', + extensions: ['.wdl'], + aliases: ['Workflow Description Language'] + }, + 'Web Ontology Language': { + type: 'data', + extensions: ['.owl'] + }, + WebAssembly: { + type: 'programming', + extensions: ['.wast', '.wat'], + aliases: ['wast', 'wasm'] + }, + 'WebAssembly Interface Type': { + type: 'data', + extensions: ['.wit'], + aliases: ['wit'] + }, + WebIDL: { + type: 'programming', + extensions: ['.webidl'] + }, + WebVTT: { + type: 'data', + extensions: ['.vtt'], + aliases: ['vtt'] + }, + 'Wget Config': { + type: 'data', + aliases: ['wgetrc'] + }, + WGSL: { + type: 'programming', + extensions: ['.wgsl'] + }, + Whiley: { + type: 'programming', + extensions: ['.whiley'] + }, + Wikitext: { + type: 'prose', + extensions: ['.mediawiki', '.wiki', '.wikitext'], + aliases: ['mediawiki', 'wiki'] + }, + 'Win32 Message File': { + type: 'data', + extensions: ['.mc'] + }, + 'Windows Registry Entries': { + type: 'data', + extensions: ['.reg'] + }, + wisp: { + type: 'programming', + extensions: ['.wisp'] + }, + 'Witcher Script': { + type: 'programming', + extensions: ['.ws'] + }, + Wollok: { + type: 'programming', + extensions: ['.wlk'] + }, + 'World of Warcraft Addon Data': { + type: 'data', + extensions: ['.toc'] + }, + Wren: { + type: 'programming', + extensions: ['.wren'], + aliases: ['wrenlang'] + }, + 'X BitMap': { + type: 'data', + extensions: ['.xbm'], + aliases: ['xbm'] + }, + 'X PixMap': { + type: 'data', + extensions: ['.xpm', '.pm'], + aliases: ['xpm'] + }, + X10: { + type: 'programming', + extensions: ['.x10'], + aliases: ['xten'] + }, + xBase: { + type: 'programming', + extensions: ['.prg', '.ch', '.prw'], + aliases: ['advpl', 'clipper', 'foxpro'] + }, + XC: { + type: 'programming', + extensions: ['.xc'] + }, + XML: { + type: 'data', + extensions: [ + '.xml', + '.adml', + '.admx', + '.ant', + '.axaml', + '.axml', + '.builds', + '.ccproj', + '.ccxml', + '.clixml', + '.cproject', + '.cscfg', + '.csdef', + '.csl', + '.csproj', + '.ct', + '.depproj', + '.dita', + '.ditamap', + '.ditaval', + '.dll.config', + '.dotsettings', + '.filters', + '.fsproj', + '.fxml', + '.glade', + '.gml', + '.gmx', + '.gpx', + '.grxml', + '.gst', + '.hzp', + '.iml', + '.ivy', + '.jelly', + '.jsproj', + '.kml', + '.launch', + '.mdpolicy', + '.mjml', + '.mm', + '.mod', + '.mojo', + '.mxml', + '.natvis', + '.ncl', + '.ndproj', + '.nproj', + '.nuspec', + '.odd', + '.osm', + '.pkgproj', + '.pluginspec', + '.proj', + '.props', + '.ps1xml', + '.psc1', + '.pt', + '.qhelp', + '.rdf', + '.res', + '.resx', + '.rs', + '.rss', + '.sch', + '.scxml', + '.sfproj', + '.shproj', + '.slnx', + '.srdf', + '.storyboard', + '.sublime-snippet', + '.sw', + '.targets', + '.tml', + '.ts', + '.tsx', + '.typ', + '.ui', + '.urdf', + '.ux', + '.vbproj', + '.vcxproj', + '.vsixmanifest', + '.vssettings', + '.vstemplate', + '.vxml', + '.wixproj', + '.workflow', + '.wsdl', + '.wsf', + '.wxi', + '.wxl', + '.wxs', + '.x3d', + '.xacro', + '.xaml', + '.xib', + '.xlf', + '.xliff', + '.xmi', + '.xml.dist', + '.xmp', + '.xproj', + '.xsd', + '.xspec', + '.xul', + '.zcml' + ], + aliases: ['rss', 'xsd', 'wsdl'] + }, + 'XML Property List': { + type: 'data', + extensions: ['.plist', '.stTheme', '.tmCommand', '.tmLanguage', '.tmPreferences', '.tmSnippet', '.tmTheme'] + }, + Xojo: { + type: 'programming', + extensions: ['.xojo_code', '.xojo_menu', '.xojo_report', '.xojo_script', '.xojo_toolbar', '.xojo_window'] + }, + Xonsh: { + type: 'programming', + extensions: ['.xsh'] + }, + XPages: { + type: 'data', + extensions: ['.xsp-config', '.xsp.metadata'] + }, + XProc: { + type: 'programming', + extensions: ['.xpl', '.xproc'] + }, + XQuery: { + type: 'programming', + extensions: ['.xquery', '.xq', '.xql', '.xqm', '.xqy'] + }, + XS: { + type: 'programming', + extensions: ['.xs'] + }, + XSLT: { + type: 'programming', + extensions: ['.xslt', '.xsl'], + aliases: ['xsl'] + }, + Xtend: { + type: 'programming', + extensions: ['.xtend'] + }, + Yacc: { + type: 'programming', + extensions: ['.y', '.yacc', '.yy'] + }, + YAML: { + type: 'data', + extensions: [ + '.yml', + '.mir', + '.reek', + '.rviz', + '.sublime-syntax', + '.syntax', + '.yaml', + '.yaml-tmlanguage', + '.yaml.sed', + '.yml.mysql' + ], + aliases: ['yml'] + }, + YANG: { + type: 'data', + extensions: ['.yang'] + }, + YARA: { + type: 'programming', + extensions: ['.yar', '.yara'] + }, + YASnippet: { + type: 'markup', + extensions: ['.yasnippet'], + aliases: ['snippet', 'yas'] + }, + Yul: { + type: 'programming', + extensions: ['.yul'] + }, + ZAP: { + type: 'programming', + extensions: ['.zap', '.xzap'] + }, + Zeek: { + type: 'programming', + extensions: ['.zeek', '.bro'], + aliases: ['bro'] + }, + ZenScript: { + type: 'programming', + extensions: ['.zs'] + }, + Zephir: { + type: 'programming', + extensions: ['.zep'] + }, + Zig: { + type: 'programming', + extensions: ['.zig', '.zig.zon'] + }, + ZIL: { + type: 'programming', + extensions: ['.zil', '.mud'] + }, + Zimpl: { + type: 'programming', + extensions: ['.zimpl', '.zmpl', '.zpl'] + }, + Zmodel: { + type: 'data', + extensions: ['.zmodel'] + } +} diff --git a/packages/ui/src/utils/codeLanguage.ts b/packages/ui/src/utils/codeLanguage.ts new file mode 100644 index 0000000000..8812714538 --- /dev/null +++ b/packages/ui/src/utils/codeLanguage.ts @@ -0,0 +1,39 @@ +import { languages } from '../config/languages' + +/** + * Get the file extension of the language, by language name + * - First, exact match + * - Then, case-insensitive match + * - Finally, match aliases + * If there are multiple file extensions, only the first one will be returned + * @param language language name + * @returns file extension + */ +export function getExtensionByLanguage(language: string): string { + const lowerLanguage = language.toLowerCase() + + // Exact match language name + const directMatch = languages[language] + if (directMatch?.extensions?.[0]) { + return directMatch.extensions[0] + } + + const languageEntries = Object.entries(languages) + + // Case-insensitive match language name + for (const [langName, data] of languageEntries) { + if (langName.toLowerCase() === lowerLanguage && data.extensions?.[0]) { + return data.extensions[0] + } + } + + // Match aliases + for (const [, data] of languageEntries) { + if (data.aliases?.some((alias) => alias.toLowerCase() === lowerLanguage)) { + return data.extensions?.[0] || `.${language}` + } + } + + // Fallback to language name + return `.${language}` +} diff --git a/packages/ui/stories/components/interactive/CodeEditor.stories.tsx b/packages/ui/stories/components/interactive/CodeEditor.stories.tsx new file mode 100644 index 0000000000..ca8097c28c --- /dev/null +++ b/packages/ui/stories/components/interactive/CodeEditor.stories.tsx @@ -0,0 +1,109 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { action } from 'storybook/actions' + +import CodeEditor, { getCmThemeByName, getCmThemeNames } from '../../../src/components/interactive/CodeEditor' + +const meta: Meta = { + title: 'Interactive/CodeEditor', + component: CodeEditor, + parameters: { layout: 'centered' }, + tags: ['autodocs'], + argTypes: { + language: { + control: 'select', + options: ['typescript', 'javascript', 'json', 'markdown', 'python', 'dot', 'mmd'] + }, + theme: { + control: 'select', + options: getCmThemeNames() + }, + fontSize: { control: { type: 'range', min: 12, max: 22, step: 1 } }, + editable: { control: 'boolean' }, + expanded: { control: 'boolean' }, + wrapped: { control: 'boolean' }, + height: { control: 'text' }, + maxHeight: { control: 'text' }, + minHeight: { control: 'text' } + } +} + +export default meta +type Story = StoryObj + +// 基础示例(非流式) +export const Default: Story = { + args: { + language: 'typescript', + theme: 'light', + value: `function greet(name: string) {\n return 'Hello ' + name\n}`, + fontSize: 16, + editable: true, + expanded: true, + wrapped: true + }, + render: (args) => ( +
+ +
+ ) +} + +// JSON + Lint(非流式) +export const JSONLint: Story = { + args: { + language: 'json', + theme: 'light', + value: `{\n "valid": true,\n "missingComma": true\n "another": 123\n}`, + wrapped: true + }, + render: (args) => ( +
+ +
+ ) +} + +// 保存快捷键(Mod/Ctrl + S 触发 onSave) +export const SaveShortcut: Story = { + args: { + language: 'markdown', + theme: 'light', + value: `# Press Mod/Ctrl + S to fire onSave`, + wrapped: true + }, + render: (args) => ( +
+ +

使用 Mod/Ctrl + S 触发保存事件。

+
+ ) +} diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx index 9453866f20..e176d227e4 100644 --- a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx +++ b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx @@ -1,6 +1,8 @@ -import CodeEditor, { CodeEditorHandles } from '@renderer/components/CodeEditor' +import { CodeEditor, type CodeEditorHandles } from '@cherrystudio/ui' +import { usePreference } from '@data/hooks/usePreference' import { CopyIcon, FilePngIcon } from '@renderer/components/Icons' import { isMac } from '@renderer/config/constant' +import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue' import { classNames } from '@renderer/utils' import { extractHtmlTitle, getFileNameFromHtmlTitle } from '@renderer/utils/formats' @@ -23,6 +25,8 @@ type ViewMode = 'split' | 'code' | 'preview' const HtmlArtifactsPopup: React.FC = ({ open, title, html, onSave, onClose }) => { const { t } = useTranslation() + const [fontSize] = usePreference('chat.message.font_size') + const { activeCmTheme } = useCodeStyle() const [viewMode, setViewMode] = useState('split') const [isFullscreen, setIsFullscreen] = useState(false) const [saved, setSaved] = useTemporaryValue(false, 2000) @@ -141,6 +145,8 @@ const HtmlArtifactsPopup: React.FC = ({ open, title, ht = memo(({ children, language, onSave }) => { const { t } = useTranslation() - const [codeEditorEnabled] = usePreference('chat.code.editor.enabled') const [codeExecutionEnabled] = usePreference('chat.code.execution.enabled') const [codeExecutionTimeoutMinutes] = usePreference('chat.code.execution.timeout_minutes') const [codeCollapsible] = usePreference('chat.code.collapsible') const [codeWrappable] = usePreference('chat.code.wrappable') const [codeImageTools] = usePreference('chat.code.image_tools') + const [fontSize] = usePreference('chat.message.font_size') + const [codeShowLineNumbers] = usePreference('chat.code.show_line_numbers') + const [codeEditor] = useMultiplePreferences({ + enabled: 'chat.code.editor.enabled', + autocompletion: 'chat.code.editor.autocompletion', + foldGutter: 'chat.code.editor.fold_gutter', + highlightActiveLine: 'chat.code.editor.highlight_active_line', + keymap: 'chat.code.editor.keymap', + themeLight: 'chat.code.editor.theme_light', + themeDark: 'chat.code.editor.theme_dark' + }) + + const { activeCmTheme } = useCodeStyle() const [viewState, setViewState] = useState({ mode: 'special' as ViewMode, @@ -196,7 +210,7 @@ export const CodeBlockView: React.FC = memo(({ children, language, onSave // 特殊视图的编辑/查看源码按钮,在分屏模式下不可用 useViewSourceTool({ enabled: hasSpecialView, - editable: codeEditorEnabled, + editable: codeEditor.enabled, viewMode, onViewModeChange: setViewMode, setTools @@ -238,7 +252,7 @@ export const CodeBlockView: React.FC = memo(({ children, language, onSave // 代码编辑器的保存按钮 useSaveTool({ - enabled: codeEditorEnabled && !isInSpecialView, + enabled: codeEditor.enabled && !isInSpecialView, sourceViewRef, setTools }) @@ -246,16 +260,18 @@ export const CodeBlockView: React.FC = memo(({ children, language, onSave // 源代码视图组件 const sourceView = useMemo( () => - codeEditorEnabled ? ( + codeEditor.enabled ? ( @@ -270,7 +286,18 @@ export const CodeBlockView: React.FC = memo(({ children, language, onSave maxHeight={`${MAX_COLLAPSED_CODE_HEIGHT}px`} /> ), - [children, codeEditorEnabled, handleHeightChange, language, onSave, shouldExpand, shouldWrap] + [ + activeCmTheme, + children, + codeEditor, + codeShowLineNumbers, + fontSize, + handleHeightChange, + language, + onSave, + shouldExpand, + shouldWrap + ] ) // 特殊视图组件映射 diff --git a/src/renderer/src/components/CodeEditor/CodeEditor.tsx b/src/renderer/src/components/CodeEditor/CodeEditor.tsx new file mode 100644 index 0000000000..0fae63ffab --- /dev/null +++ b/src/renderer/src/components/CodeEditor/CodeEditor.tsx @@ -0,0 +1,136 @@ +import CodeMirror, { Annotation, BasicSetupOptions, EditorView } from '@uiw/react-codemirror' +import { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react' +import { memo } from 'react' + +import { useBlurHandler, useHeightListener, useLanguageExtensions, useSaveKeymap } from './hooks' +import { CodeEditorProps } from './types' +import { prepareCodeChanges } from './utils' + +/** + * A code editor component based on CodeMirror. + * This is a wrapper of ReactCodeMirror. + * @deprecated Import CodeEditor from @cherrystudio/ui instead. + */ +const CodeEditor = ({ + ref, + value, + placeholder, + language, + onSave, + onChange, + onBlur, + onHeightChange, + height, + maxHeight, + minHeight, + options, + extensions, + theme = 'light', + fontSize = 16, + style, + className, + editable = true, + expanded = true, + wrapped = true +}: CodeEditorProps) => { + const basicSetup = useMemo(() => { + return { + dropCursor: true, + allowMultipleSelections: true, + indentOnInput: true, + bracketMatching: true, + closeBrackets: true, + rectangularSelection: true, + crosshairCursor: true, + highlightActiveLineGutter: false, + highlightSelectionMatches: true, + closeBracketsKeymap: options?.keymap, + searchKeymap: options?.keymap, + foldKeymap: options?.keymap, + completionKeymap: options?.keymap, + lintKeymap: options?.keymap, + ...(options as BasicSetupOptions) + } + }, [options]) + + const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? '')) + const editorViewRef = useRef(null) + + const langExtensions = useLanguageExtensions(language, options?.lint) + + const handleSave = useCallback(() => { + const currentDoc = editorViewRef.current?.state.doc.toString() ?? '' + onSave?.(currentDoc) + }, [onSave]) + + // Calculate changes during streaming response to update EditorView + // Cannot handle user editing code during streaming response (and probably doesn't need to) + useEffect(() => { + if (!editorViewRef.current) return + + const newContent = options?.stream ? (value ?? '').trimEnd() : (value ?? '') + const currentDoc = editorViewRef.current.state.doc.toString() + + const changes = prepareCodeChanges(currentDoc, newContent) + + if (changes && changes.length > 0) { + editorViewRef.current.dispatch({ + changes, + annotations: [Annotation.define().of(true)] + }) + } + }, [options?.stream, value]) + + const saveKeymapExtension = useSaveKeymap({ onSave, enabled: options?.keymap }) + const blurExtension = useBlurHandler({ onBlur }) + const heightListenerExtension = useHeightListener({ onHeightChange }) + + const customExtensions = useMemo(() => { + return [ + ...(extensions ?? []), + ...langExtensions, + ...(wrapped ? [EditorView.lineWrapping] : []), + saveKeymapExtension, + blurExtension, + heightListenerExtension + ].flat() + }, [extensions, langExtensions, wrapped, saveKeymapExtension, blurExtension, heightListenerExtension]) + + useImperativeHandle(ref, () => ({ + save: handleSave + })) + + return ( + { + editorViewRef.current = view + onHeightChange?.(view.scrollDOM?.scrollHeight ?? 0) + }} + onChange={(value, viewUpdate) => { + if (onChange && viewUpdate.docChanged) onChange(value) + }} + basicSetup={basicSetup} + style={{ + fontSize, + marginTop: 0, + borderRadius: 'inherit', + ...style + }} + className={`code-editor ${className ?? ''}`} + /> + ) +} + +CodeEditor.displayName = 'CodeEditor' + +export default memo(CodeEditor) diff --git a/src/renderer/src/components/CodeEditor/__tests__/utils.test.ts b/src/renderer/src/components/CodeEditor/__tests__/utils.test.ts index b02d10e152..ebc9a3d7d3 100644 --- a/src/renderer/src/components/CodeEditor/__tests__/utils.test.ts +++ b/src/renderer/src/components/CodeEditor/__tests__/utils.test.ts @@ -2,12 +2,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { getNormalizedExtension } from '../utils' -const mocks = vi.hoisted(() => ({ - getExtensionByLanguage: vi.fn() +const hoisted = vi.hoisted(() => ({ + languages: { + svg: { extensions: ['.svg'] }, + TypeScript: { extensions: ['.ts'] } + } })) -vi.mock('@renderer/utils/code-language', () => ({ - getExtensionByLanguage: mocks.getExtensionByLanguage +vi.mock('@shared/config/languages', () => ({ + languages: hoisted.languages })) describe('getNormalizedExtension', () => { @@ -16,28 +19,23 @@ describe('getNormalizedExtension', () => { }) 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/index.ts b/src/renderer/src/components/CodeEditor/index.ts new file mode 100644 index 0000000000..4a2e55f9fb --- /dev/null +++ b/src/renderer/src/components/CodeEditor/index.ts @@ -0,0 +1,3 @@ +export { default } from './CodeEditor' +export * from './types' +export { getCmThemeByName, getCmThemeNames } from './utils' diff --git a/src/renderer/src/components/CodeEditor/index.tsx b/src/renderer/src/components/CodeEditor/index.tsx deleted file mode 100644 index 85d874b766..0000000000 --- a/src/renderer/src/components/CodeEditor/index.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import { useMultiplePreferences, usePreference } from '@data/hooks/usePreference' -import { useCodeStyle } from '@renderer/context/CodeStyleProvider' -import CodeMirror, { Annotation, BasicSetupOptions, EditorView, Extension } from '@uiw/react-codemirror' -import diff from 'fast-diff' -import { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react' -import { memo } from 'react' - -import { useBlurHandler, useHeightListener, useLanguageExtensions, useSaveKeymap } from './hooks' - -// 标记非用户编辑的变更 -const External = Annotation.define() - -export interface CodeEditorHandles { - save?: () => void -} - -export interface CodeEditorProps { - ref?: React.RefObject - /** Value used in controlled mode, e.g., code blocks. */ - value: string - /** Placeholder when the editor content is empty. */ - placeholder?: string | HTMLElement - /** - * 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 - /** Fired when the editor content changes. */ - onChange?: (newContent: string) => void - /** Fired when the editor loses focus. */ - onBlur?: (newContent: string) => void - /** Fired when the editor height changes. */ - onHeightChange?: (scrollHeight: number) => void - /** - * Fixed editor height, not exceeding maxHeight. - * Only works when expanded is false. - */ - height?: string - /** - * Maximum editor height. - * Only works when expanded is false. - */ - maxHeight?: string - /** Minimum editor height. */ - minHeight?: string - /** Editor options that extend BasicSetupOptions. */ - options?: { - /** - * Whether to enable special treatment for stream response. - * @default false - */ - stream?: boolean - /** - * Whether to enable linting. - * @default false - */ - lint?: boolean - /** - * Whether to enable keymap. - * @default false - */ - keymap?: boolean - } & BasicSetupOptions - /** Additional extensions for CodeMirror. */ - extensions?: Extension[] - /** Font size that overrides the app setting. */ - fontSize?: number - /** Style overrides for the editor, passed directly to CodeMirror's style property. */ - style?: React.CSSProperties - /** CSS class name appended to the default `code-editor` class. */ - className?: string - /** - * Whether the editor is editable. - * @default true - */ - editable?: boolean - /** - * Whether the editor is expanded. - * If true, the height and maxHeight props are ignored. - * @default true - */ - expanded?: boolean - /** - * Whether the code lines are wrapped. - * @default true - */ - wrapped?: boolean -} - -/** - * A code editor component based on CodeMirror. - * This is a wrapper of ReactCodeMirror. - */ -const CodeEditor = ({ - ref, - value, - placeholder, - language, - onSave, - onChange, - onBlur, - onHeightChange, - height, - maxHeight, - minHeight, - options, - extensions, - fontSize: customFontSize, - style, - className, - editable = true, - expanded = true, - wrapped = true -}: CodeEditorProps) => { - const [_fontSize] = usePreference('chat.message.font_size') - const [_lineNumbers] = usePreference('chat.code.show_line_numbers') - const [codeEditor] = useMultiplePreferences({ - autocompletion: 'chat.code.editor.autocompletion', - foldGutter: 'chat.code.editor.fold_gutter', - highlightActiveLine: 'chat.code.editor.highlight_active_line', - keymap: 'chat.code.editor.keymap', - themeLight: 'chat.code.editor.theme_light', - themeDark: 'chat.code.editor.theme_dark' - }) - - const enableKeymap = useMemo(() => options?.keymap ?? codeEditor.keymap, [options?.keymap, codeEditor.keymap]) - - // 合并 codeEditor 和 options 的 basicSetup,options 优先 - const basicSetup = useMemo(() => { - return { - lineNumbers: _lineNumbers, - ...(codeEditor as BasicSetupOptions), - ...(options as BasicSetupOptions) - } - }, [codeEditor, _lineNumbers, options]) - - const fontSize = useMemo(() => customFontSize ?? _fontSize - 1, [customFontSize, _fontSize]) - - const { activeCmTheme } = useCodeStyle() - const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? '')) - const editorViewRef = useRef(null) - - const langExtensions = useLanguageExtensions(language, options?.lint) - - const handleSave = useCallback(() => { - const currentDoc = editorViewRef.current?.state.doc.toString() ?? '' - onSave?.(currentDoc) - }, [onSave]) - - // 流式响应过程中计算 changes 来更新 EditorView - // 无法处理用户在流式响应过程中编辑代码的情况(应该也不必处理) - useEffect(() => { - if (!editorViewRef.current) return - - const newContent = options?.stream ? (value ?? '').trimEnd() : (value ?? '') - const currentDoc = editorViewRef.current.state.doc.toString() - - const changes = prepareCodeChanges(currentDoc, newContent) - - if (changes && changes.length > 0) { - editorViewRef.current.dispatch({ - changes, - annotations: [External.of(true)] - }) - } - }, [options?.stream, value]) - - const saveKeymapExtension = useSaveKeymap({ onSave, enabled: enableKeymap }) - const blurExtension = useBlurHandler({ onBlur }) - const heightListenerExtension = useHeightListener({ onHeightChange }) - - const customExtensions = useMemo(() => { - return [ - ...(extensions ?? []), - ...langExtensions, - ...(wrapped ? [EditorView.lineWrapping] : []), - saveKeymapExtension, - blurExtension, - heightListenerExtension - ].flat() - }, [extensions, langExtensions, wrapped, saveKeymapExtension, blurExtension, heightListenerExtension]) - - useImperativeHandle(ref, () => ({ - save: handleSave - })) - - return ( - { - editorViewRef.current = view - onHeightChange?.(view.scrollDOM?.scrollHeight ?? 0) - }} - onChange={(value, viewUpdate) => { - if (onChange && viewUpdate.docChanged) onChange(value) - }} - basicSetup={{ - dropCursor: true, - allowMultipleSelections: true, - indentOnInput: true, - bracketMatching: true, - closeBrackets: true, - rectangularSelection: true, - crosshairCursor: true, - highlightActiveLineGutter: false, - highlightSelectionMatches: true, - closeBracketsKeymap: enableKeymap, - searchKeymap: enableKeymap, - foldKeymap: enableKeymap, - completionKeymap: enableKeymap, - lintKeymap: enableKeymap, - ...basicSetup // override basicSetup - }} - style={{ - fontSize, - marginTop: 0, - borderRadius: 'inherit', - ...style - }} - className={`code-editor ${className ?? ''}`} - /> - ) -} - -CodeEditor.displayName = 'CodeEditor' - -/** - * 使用 fast-diff 计算代码变更,再转换为 CodeMirror 的 changes。 - * 可以处理所有类型的变更,不过流式响应过程中多是插入操作。 - * @param oldCode 旧的代码内容 - * @param newCode 新的代码内容 - * @returns 用于 EditorView.dispatch 的 changes 数组 - */ -function prepareCodeChanges(oldCode: string, newCode: string) { - const diffResult = diff(oldCode, newCode) - - const changes: { from: number; to: number; insert: string }[] = [] - let offset = 0 - - // operation: 1=插入, -1=删除, 0=相等 - for (const [operation, text] of diffResult) { - if (operation === 1) { - changes.push({ - from: offset, - to: offset, - insert: text - }) - } else if (operation === -1) { - changes.push({ - from: offset, - to: offset + text.length, - insert: '' - }) - offset += text.length - } else { - offset += text.length - } - } - - return changes -} - -export default memo(CodeEditor) diff --git a/src/renderer/src/components/CodeEditor/types.ts b/src/renderer/src/components/CodeEditor/types.ts new file mode 100644 index 0000000000..bc91377f70 --- /dev/null +++ b/src/renderer/src/components/CodeEditor/types.ts @@ -0,0 +1,93 @@ +import { BasicSetupOptions, Extension } from '@uiw/react-codemirror' + +export type CodeMirrorTheme = 'light' | 'dark' | 'none' | Extension + +export interface CodeEditorHandles { + save?: () => void +} + +export interface CodeEditorProps { + ref?: React.RefObject + /** Value used in controlled mode, e.g., code blocks. */ + value: string + /** Placeholder when the editor content is empty. */ + placeholder?: string | HTMLElement + /** + * 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 + /** Fired when the editor content changes. */ + onChange?: (newContent: string) => void + /** Fired when the editor loses focus. */ + onBlur?: (newContent: string) => void + /** Fired when the editor height changes. */ + onHeightChange?: (scrollHeight: number) => void + /** + * Fixed editor height, not exceeding maxHeight. + * Only works when expanded is false. + */ + height?: string + /** + * Maximum editor height. + * Only works when expanded is false. + */ + maxHeight?: string + /** Minimum editor height. */ + minHeight?: string + /** Editor options that extend BasicSetupOptions. */ + options?: { + /** + * Whether to enable special treatment for stream response. + * @default false + */ + stream?: boolean + /** + * Whether to enable linting. + * @default false + */ + lint?: boolean + /** + * Whether to enable keymap. + * @default false + */ + keymap?: boolean + } & BasicSetupOptions + /** Additional extensions for CodeMirror. */ + extensions?: Extension[] + /** + * CodeMirror theme name: 'light', 'dark', 'none', Extension. + * @default 'light' + */ + theme?: CodeMirrorTheme + /** + * Font size that overrides the app setting. + * @default 16 + */ + fontSize?: number + /** Style overrides for the editor, passed directly to CodeMirror's style property. */ + style?: React.CSSProperties + /** CSS class name appended to the default `code-editor` class. */ + className?: string + /** + * Whether the editor is editable. + * @default true + */ + editable?: boolean + /** + * Whether the editor is expanded. + * If true, the height and maxHeight props are ignored. + * @default true + */ + expanded?: boolean + /** + * Whether the code lines are wrapped. + * @default true + */ + wrapped?: boolean +} diff --git a/src/renderer/src/components/CodeEditor/utils.ts b/src/renderer/src/components/CodeEditor/utils.ts index ef5941720e..b31890d344 100644 --- a/src/renderer/src/components/CodeEditor/utils.ts +++ b/src/renderer/src/components/CodeEditor/utils.ts @@ -1,8 +1,49 @@ -import { getExtensionByLanguage } from '@renderer/utils/code-language' +import { languages } from '@shared/config/languages' +import * as cmThemes from '@uiw/codemirror-themes-all' +import { Extension } from '@uiw/react-codemirror' +import diff from 'fast-diff' -// 自定义语言文件扩展名映射 -// key: 语言名小写 -// value: 扩展名 +import { CodeMirrorTheme } from './types' + +/** + * Computes code changes using fast-diff and converts them to CodeMirror changes. + * Could handle all types of changes, though insertions are most common during streaming responses. + * @param oldCode The old code content + * @param newCode The new code content + * @returns An array of changes for EditorView.dispatch + */ +export function prepareCodeChanges(oldCode: string, newCode: string) { + const diffResult = diff(oldCode, newCode) + + const changes: { from: number; to: number; insert: string }[] = [] + let offset = 0 + + // operation: 1=insert, -1=delete, 0=equal + for (const [operation, text] of diffResult) { + if (operation === 1) { + changes.push({ + from: offset, + to: offset, + insert: text + }) + } else if (operation === -1) { + changes.push({ + from: offset, + to: offset + text.length, + insert: '' + }) + offset += text.length + } else { + offset += text.length + } + } + + return changes +} + +// Custom language file extension mapping +// key: language name in lowercase +// value: file extension const _customLanguageExtensions: Record = { svg: 'xml', vab: 'vb', @@ -10,31 +51,112 @@ const _customLanguageExtensions: Record = { } /** - * 获取语言的扩展名,用于 @uiw/codemirror-extensions-langs - * - 先搜索自定义扩展名 - * - 再搜索 github linguist 扩展名 - * - 最后假定名称已经是扩展名 - * @param language 语言名称 - * @returns 扩展名(不包含 `.`) + * Get the file extension of the language, for @uiw/codemirror-extensions-langs + * - First, search for custom extensions + * - Then, search for github linguist extensions + * - Finally, assume the name is already an extension + * @param language language name + * @returns file extension (without `.` prefix) */ export async function getNormalizedExtension(language: string) { - const lowerLanguage = language.toLowerCase() + let lang = language + // If the language name looks like an extension, remove the dot + if (language.startsWith('.') && language.length > 1) { + lang = language.slice(1) + } + + const lowerLanguage = lang.toLowerCase() + + // 1. Search for custom extensions const customExt = _customLanguageExtensions[lowerLanguage] if (customExt) { return customExt } - const linguistExt = getExtensionByLanguage(language) + // 2. Search for github linguist extensions + const linguistExt = getExtensionByLanguage(lang) if (linguistExt) { return linguistExt.slice(1) } - // 如果语言名称像扩展名 - if (language.startsWith('.') && language.length > 1) { - return language.slice(1) + // Fallback to language name + return lang +} + +/** + * Get the file extension of the language, by language name + * - First, exact match + * - Then, case-insensitive match + * - Finally, match aliases + * If there are multiple file extensions, only the first one will be returned + * @param language language name + * @returns file extension + */ +export function getExtensionByLanguage(language: string): string { + const lowerLanguage = language.toLowerCase() + + // Exact match language name + const directMatch = languages[language] + if (directMatch?.extensions?.[0]) { + return directMatch.extensions[0] } - // 回退到语言名称 - return language + // Case-insensitive match language name + for (const [langName, data] of Object.entries(languages)) { + if (langName.toLowerCase() === lowerLanguage && data.extensions?.[0]) { + return data.extensions[0] + } + } + + // Match aliases + for (const [, data] of Object.entries(languages)) { + if (data.aliases?.some((alias) => alias.toLowerCase() === lowerLanguage)) { + return data.extensions?.[0] || `.${language}` + } + } + + // Fallback to language name + return `.${language}` +} + +/** + * Get the list of CodeMirror theme names + * - Include auto, light, dark + * - Include all themes in @uiw/codemirror-themes-all + * + * A more robust approach might be to hardcode the theme list + * @returns theme name list + */ +export function getCmThemeNames(): string[] { + return ['auto', 'light', 'dark'] + .concat(Object.keys(cmThemes)) + .filter((item) => typeof cmThemes[item as keyof typeof cmThemes] !== 'function') + .filter((item) => !/^(defaultSettings)/.test(item as string) && !/(Style)$/.test(item as string)) +} + +/** + * Get the CodeMirror theme object by theme name + * @param name theme name + * @returns theme object + */ +export function getCmThemeByName(name: string): CodeMirrorTheme { + // 1. Search for the extension of the corresponding theme in @uiw/codemirror-themes-all + const candidate = (cmThemes as Record)[name] + if ( + Object.prototype.hasOwnProperty.call(cmThemes, name) && + typeof candidate !== 'function' && + !/^defaultSettings/i.test(name) && + !/(Style)$/.test(name) + ) { + return candidate as Extension + } + + // 2. Basic string theme + if (name === 'light' || name === 'dark' || name === 'none') { + return name + } + + // 3. If not found, fallback to light + return 'light' } diff --git a/src/renderer/src/components/CodeToolbar/hooks/useSaveTool.tsx b/src/renderer/src/components/CodeToolbar/hooks/useSaveTool.tsx index c847b6ca90..f7542b14f2 100644 --- a/src/renderer/src/components/CodeToolbar/hooks/useSaveTool.tsx +++ b/src/renderer/src/components/CodeToolbar/hooks/useSaveTool.tsx @@ -1,5 +1,5 @@ import { ActionTool, TOOL_SPECS, useToolManager } from '@renderer/components/ActionTools' -import { CodeEditorHandles } from '@renderer/components/CodeEditor' +import { type CodeEditorHandles } from '@renderer/components/CodeEditor' import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue' import { Check, SaveIcon } from 'lucide-react' import { useCallback, useEffect } from 'react' diff --git a/src/renderer/src/components/Popups/TextFilePreview.tsx b/src/renderer/src/components/Popups/TextFilePreview.tsx index f5fa787d0b..cf07502da7 100644 --- a/src/renderer/src/components/Popups/TextFilePreview.tsx +++ b/src/renderer/src/components/Popups/TextFilePreview.tsx @@ -1,8 +1,10 @@ +import { CodeEditor } from '@cherrystudio/ui' +import { usePreference } from '@data/hooks/usePreference' +import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { Modal } from 'antd' import { useState } from 'react' import styled from 'styled-components' -import CodeEditor from '../CodeEditor' import { TopView } from '../TopView' interface Props { @@ -14,6 +16,8 @@ interface Props { const PopupContainer: React.FC = ({ text, title, extension, resolve }) => { const [open, setOpen] = useState(true) + const [fontSize] = usePreference('chat.message.font_size') + const { activeCmTheme } = useCodeStyle() const onOk = () => { setOpen(false) @@ -55,6 +59,8 @@ const PopupContainer: React.FC = ({ text, title, extension, resolve }) => footer={null}> {extension !== undefined ? ( (defaultCodeStyleContext) @@ -60,10 +60,7 @@ export const CodeStyleProvider: React.FC = ({ children }) => // CodeMirror 主题 // 更保险的做法可能是硬编码主题列表 if (codeEditorEnabled) { - return ['auto', 'light', 'dark'] - .concat(Object.keys(cmThemes)) - .filter((item) => typeof cmThemes[item as keyof typeof cmThemes] !== 'function') - .filter((item) => !/^(defaultSettings)/.test(item as string) && !/(Style)$/.test(item as string)) + return getCmThemeNames() } // Shiki 主题,取出所有 BundledThemeInfo 的 id 作为主题名 @@ -92,7 +89,7 @@ export const CodeStyleProvider: React.FC = ({ children }) => if (!themeName || themeName === 'auto' || !themeNames.includes(themeName)) { themeName = theme === ThemeMode.light ? 'materialLight' : 'dark' } - return cmThemes[themeName as keyof typeof cmThemes] || themeName + return getCmThemeByName(themeName) }, [theme, codeEditorThemeLight, codeEditorThemeDark, themeNames]) // 自定义 shiki 语言别名 diff --git a/src/renderer/src/pages/notes/NotesEditor.tsx b/src/renderer/src/pages/notes/NotesEditor.tsx index a9ed8f592f..6acc8e57a7 100644 --- a/src/renderer/src/pages/notes/NotesEditor.tsx +++ b/src/renderer/src/pages/notes/NotesEditor.tsx @@ -1,8 +1,9 @@ -import CodeEditor from '@renderer/components/CodeEditor' +import { CodeEditor } from '@cherrystudio/ui' import { HSpaceBetweenStack } from '@renderer/components/Layout' import RichEditor from '@renderer/components/RichEditor' import { RichEditorRef } from '@renderer/components/RichEditor/types' import Selector from '@renderer/components/Selector' +import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useNotesSettings } from '@renderer/hooks/useNotesSettings' import { EditorView } from '@renderer/types' import { Empty, Spin } from 'antd' @@ -23,6 +24,7 @@ const NotesEditor: FC = memo( ({ activeNodeId, currentContent, tokenCount, isLoading, onMarkdownChange, editorRef }) => { const { t } = useTranslation() const { settings } = useNotesSettings() + const { activeCmTheme } = useCodeStyle() const currentViewMode = useMemo(() => { if (settings.defaultViewMode === 'edit') { return settings.defaultEditMode @@ -61,14 +63,15 @@ const NotesEditor: FC = memo( {tmpViewMode === 'source' ? ( diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx index 1315f8d7bc..e0e53c4b47 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx @@ -1,10 +1,12 @@ import 'emoji-picker-element' import { CloseCircleFilled } from '@ant-design/icons' -import CodeEditor from '@renderer/components/CodeEditor' +import { CodeEditor } from '@cherrystudio/ui' +import { usePreference } from '@data/hooks/usePreference' import EmojiPicker from '@renderer/components/EmojiPicker' import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout' import { RichEditorRef } from '@renderer/components/RichEditor/types' +import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { usePromptProcessor } from '@renderer/hooks/usePromptProcessor' import { estimateTextTokens } from '@renderer/services/TokenService' import { Assistant, AssistantSettings } from '@renderer/types' @@ -26,6 +28,8 @@ interface Props { } const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant }) => { + const [fontSize] = usePreference('chat.message.font_size') + const { activeCmTheme } = useCodeStyle() const [emoji, setEmoji] = useState(getLeadingEmoji(assistant.name) || assistant.emoji) const [name, setName] = useState(assistant.name.replace(getLeadingEmoji(assistant.name) || '', '').trim()) const [prompt, setPrompt] = useState(assistant.prompt) @@ -132,6 +136,8 @@ const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant } ) : ( { const [pinTopicsToTop, setPinTopicsToTop] = usePreference('topic.tab.pin_to_top') const [showTopicTime, setShowTopicTime] = usePreference('topic.tab.show_time') const [assistantIconType, setAssistantIconType] = usePreference('assistant.icon_type') + const [fontSize] = usePreference('chat.message.font_size') const { navbarPosition, setNavbarPosition } = useNavbarPosition() const { theme, settedTheme, setTheme } = useTheme() const { t } = useTranslation() const [currentZoom, setCurrentZoom] = useState(1.0) const { userTheme, setUserTheme } = useUserTheme() + const { activeCmTheme } = useCodeStyle() // const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS) // const [disabledIcons, setDisabledIcons] = useState(sidebarIcons?.disabled || []) const [fontList, setFontList] = useState([]) @@ -414,6 +417,8 @@ const DisplaySettings: FC = () => { = ({ initialImportMethod = 'json' }) => { const { t } = useTranslation() + const [fontSize] = usePreference('chat.message.font_size') + const { activeCmTheme } = useCodeStyle() const [form] = Form.useForm() const [loading, setLoading] = useState(false) const [importMethod, setImportMethod] = useState<'json' | 'dxt'>(initialImportMethod) @@ -321,7 +325,8 @@ const AddMcpServerModal: FC = ({ label={t('settings.mcp.addServer.importFrom.tooltip')} rules={[{ required: true, message: t('settings.mcp.addServer.importFrom.placeholder') }]}> = ({ resolve }) => { const [jsonError, setJsonError] = useState('') const [isLoading, setIsLoading] = useState(true) const mcpServers = useAppSelector((state) => state.mcp.servers) - + const [fontSize] = usePreference('chat.message.font_size') + const { activeCmTheme } = useCodeStyle() const dispatch = useAppDispatch() const { t } = useTranslation() @@ -131,6 +134,8 @@ const PopupContainer: React.FC = ({ resolve }) => { ) : ( setJsonConfig(value)} diff --git a/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx index 80324fd997..5058ee3483 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx @@ -1,5 +1,7 @@ -import CodeEditor from '@renderer/components/CodeEditor' +import { CodeEditor } from '@cherrystudio/ui' +import { usePreference } from '@data/hooks/usePreference' import { TopView } from '@renderer/components/TopView' +import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useCopilot } from '@renderer/hooks/useCopilot' import { useProvider } from '@renderer/hooks/useProvider' import { Provider } from '@renderer/types' @@ -22,6 +24,8 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { const { t } = useTranslation() const { updateProvider } = useProvider(provider.id) const { defaultHeaders, updateDefaultHeaders } = useCopilot() + const [fontSize] = usePreference('chat.message.font_size') + const { activeCmTheme } = useCodeStyle() const headers = provider.id === 'copilot' @@ -74,6 +78,8 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { {t('settings.provider.copilot.headers_description')} setHeaderText(value)} diff --git a/yarn.lock b/yarn.lock index 9ee84c87cb..850e79dccd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2721,16 +2721,21 @@ __metadata: "@types/react": "npm:^19.0.12" "@types/react-dom": "npm:^19.0.4" "@types/styled-components": "npm:^5.1.34" + "@uiw/codemirror-extensions-langs": "npm:^4.25.1" + "@uiw/codemirror-themes-all": "npm:^4.25.1" + "@uiw/react-codemirror": "npm:^4.25.1" antd: "npm:^5.22.5" clsx: "npm:^2.1.1" eslint-plugin-storybook: "npm:9.1.6" framer-motion: "npm:^12.23.12" + linguist-languages: "npm:^9.0.0" lucide-react: "npm:^0.525.0" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" storybook: "npm:^9.1.6" styled-components: "npm:^6.1.15" tsdown: "npm:^0.12.9" + tsx: "npm:^4.20.5" typescript: "npm:^5.6.2" vitest: "npm:^3.2.4" peerDependencies: @@ -22715,6 +22720,13 @@ __metadata: languageName: node linkType: hard +"linguist-languages@npm:^9.0.0": + version: 9.0.0 + resolution: "linguist-languages@npm:9.0.0" + checksum: 10c0/c3fe0f158d0e4a68b925324ea3f69f229daf7438d7206b0775b818e6e84317770bc9742c6ce68711e9ca772f996a897f8f22c8a775f07dbdb7fb4b28d0c8c509 + languageName: node + linkType: hard + "linkify-it@npm:^5.0.0": version: 5.0.0 resolution: "linkify-it@npm:5.0.0" @@ -30032,6 +30044,22 @@ __metadata: languageName: node linkType: hard +"tsx@npm:^4.20.5": + version: 4.20.5 + resolution: "tsx@npm:4.20.5" + dependencies: + esbuild: "npm:~0.25.0" + fsevents: "npm:~2.3.3" + get-tsconfig: "npm:^4.7.5" + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: 10c0/70f9bf746be69281312a369c712902dbf9bcbdd9db9184a4859eb4859c36ef0c5a6d79b935c1ec429158ee73fd6584089400ae8790345dae34c5b0222bdb94f3 + languageName: node + linkType: hard + "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0"