feat: add language synonym mapping and canonicalization

Add LANGUAGE_SYNONYMS dictionary to map language aliases to canonical IDs,
_canonical_language_id() to normalize language identifiers, and
_language_guidance() to provide language-specific instructions for LLM
code generation. This improves language detection and ensures consistent
prompt context across different language format variations.
This commit is contained in:
2026-03-14 18:20:39 +08:00
parent c0d4bf8b2b
commit d452d1747e
3 changed files with 266 additions and 6 deletions
+49 -2
View File
@@ -18,7 +18,7 @@ interface CopilotState {
}
interface CopilotConfig {
fetchSuggestion: (prefix: string, suffix: string, signal?: AbortSignal) => Promise<string>
fetchSuggestion: (prefix: string, suffix: string, languageId: string, signal?: AbortSignal) => Promise<string>
debounceMs?: number
}
@@ -187,6 +187,52 @@ function normalizeSuggestionText(raw: string): string {
return text
}
function sanitizeLanguageId(value: string): string {
if (!value) return ''
const trimmed = value.trim()
if (!trimmed) return ''
let safe = ''
for (const ch of trimmed) {
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch === '-' || ch === '_' || ch === '+' || ch === '.') {
safe += ch
}
}
return safe.slice(0, 32).toLowerCase()
}
function getCodeBlockLanguage(node: ProseNode): string {
const candidates = [node.attrs?.language, node.attrs?.lang, node.attrs?.info]
for (const value of candidates) {
if (typeof value === 'string' && value.trim()) {
return value.trim()
}
}
return ''
}
function getCursorLanguageId(view: EditorView): string {
const fallback = 'markdown'
const { $from } = view.state.selection
for (let depth = $from.depth; depth > 0; depth -= 1) {
const node = $from.node(depth)
const typeName = node.type?.name || ''
if (typeName === 'math_inline' || typeName === 'math_block' || typeName === 'latex' || typeName === 'math') {
return 'latex'
}
if (typeName === 'code_block' || typeName === 'codeBlock' || typeName === 'code_fence' || typeName === 'fence') {
const lang = sanitizeLanguageId(getCodeBlockLanguage(node))
if (!lang) return fallback
if (lang === 'tex' || lang === 'latex' || lang === 'katex') return 'latex'
return lang
}
}
return fallback
}
async function insertGhostText(view: EditorView, suggestion: string, from: number, ctx: Ctx) {
if (!suggestion) return
@@ -303,7 +349,8 @@ function doFetchSuggestion(
const controller = new AbortController()
runtime.abortController = controller
config.fetchSuggestion(prefix, suffix, controller.signal)
const languageId = getCursorLanguageId(view)
config.fetchSuggestion(prefix, suffix, languageId, controller.signal)
.then((suggestion) => {
if (!runtime.enabled) return
if (runtime.requestSeq !== requestSeq) return
+12 -2
View File
@@ -60,7 +60,17 @@ async function getClientIP() {
}
}
export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL) {
export async function fetchSuggestion(prefix, suffix, languageId, signal, apiUrl = API_URL) {
let normalizedLanguageId = 'markdown'
if (typeof languageId === 'string' && languageId.trim()) {
normalizedLanguageId = languageId.trim()
} else if (languageId && typeof languageId === 'object' && 'aborted' in languageId) {
signal = languageId
}
if (typeof signal === 'string') {
apiUrl = signal
signal = undefined
}
const requestId = generateRequestId()
const cancelUrl = getCancelUrl(apiUrl)
@@ -94,7 +104,7 @@ export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL)
const body = {
prefix,
suffix,
languageId: 'markdown',
languageId: normalizedLanguageId,
model_thinking: settings.modelThinking,
privacy_mode: settings.privacyMode,
user_preferences: {