fix(输入法): 优化双端联想排序并消除文字刷新闪烁

这个提交包含在:
sydxky 于 2026-11-14 18:39:54 +08:00 提交
1 父节点 bce6f8083b
当前提交 b95e7caae7
31 个文件已更改
+504 -124

没有与搜索条件相匹配的文件

@@ -26,6 +26,8 @@ export function digitsOf(text) {
return result
}
export function chars(text){return Array.from(String(text||''))}
export function contextTail(text){const match=String(text||'').match(/[\u3400-\u9fff]{1,4}$/);return match?match[0]:''}
function predictiveBucket(value){let result=0;for(let i=0;i<value.length;i++)result=(result*31+value.charCodeAt(i))&65535;return (result%256+256).toString(16).slice(1)}
// Four columns and four row units. Phrases take more width/height, never ellipsis.
export function candidatePages(candidates) {
const pages=[];let rows=[],usedRows=0,row=null
@@ -93,15 +95,28 @@ export function createDictionary(read) {
})()
try{return await pending[name]}finally{delete pending[name]}
}
async function metadata(){if(!manifest){const meta=await read('/assets/ime-dict/meta.json');manifest=typeof meta==='string'?JSON.parse(meta):meta}return manifest}
async function predict(context){
const tail=contextTail(context);if(!tail)return []
const meta=await metadata(),result=[],seen={}
for(let length=tail.length;length>0&&result.length<8;length--){
const prefix=tail.slice(-length),name='a'+predictiveBucket(prefix)
if(!meta.association_shards||meta.association_shards.indexOf(name)<0)continue
const rows=(await load(name))[prefix]||[]
for(const row of rows)if(!seen[row[0]]){seen[row[0]]=true;result.push({word:row[0],pinyin:'',consume:0,frequency:row[1]});if(result.length===8)break}
}
return result
}
return {
async lookup(raw,t9,preferences) {
predict,
async lookup(raw,t9,preferences,context) {
const input=String(raw||'').toLowerCase()
const dual=t9==='14',ambiguous=!!t9
if(!(dual?/^[0-9a-d]{1,32}$/:t9?/^[2-9]{1,32}$/:/^[a-z]{1,32}$/).test(input))return []
const digits=ambiguous?input:digitsOf(input)
const merged={},frequencies={}
function merge(data){Object.assign(merged,data.entries);Object.assign(frequencies,data.frequencies)}
if(!manifest){const meta=await read('/assets/ime-dict/meta.json');manifest=typeof meta==='string'?JSON.parse(meta):meta}
await metadata()
const names=[]
const prefixLength=dual?3:4
for(let length=1;length<=Math.min(prefixLength-1,digits.length);length++)names.push((dual?'ds':'s')+digits.slice(0,length))
@@ -112,13 +127,32 @@ export function createDictionary(read) {
const result=[],seen={}
for(const key of matching)for(const word of merged[key]){
const identity=word+'@'+key.length
if(!seen[identity]){seen[identity]=true;result.push({word,pinyin:key,consume:key.length})}
if(!seen[identity]){seen[identity]=true;result.push({word,pinyin:key,consume:key.length,frequency:frequencies[word]||0})}
}
if(input.length>=2){
const mode=dual?'14':t9?'t9':'py',prefix=mode+':'+input,extras=[]
if(input.length<prefixLength){
const name='c'+predictiveBucket(prefix)
if(manifest.completion_shards&&manifest.completion_shards.indexOf(name)>=0){
for(const row of (await load(name))[prefix]||[])extras.push({word:row[0],pinyin:row[1],frequency:row[2],missing:row[1].length-input.length,consume:input.length,completion:true})
}
}else for(const key of Object.keys(merged)){
const code=dual?code14Of(key):t9?digitsOf(key):key
if(code.length<=input.length||code.length>input.length+4||code.indexOf(input)!==0)continue
for(const word of merged[key])extras.push({word,pinyin:key,frequency:frequencies[word]||0,missing:key.length-input.length,consume:input.length,completion:true})
}
extras.sort((a,b)=>b.frequency-a.frequency||a.missing-b.missing)
let added=0
for(const item of extras){const identity=item.word+'@'+item.consume;if(!seen[identity]){seen[identity]=true;result.push(item);if(++added===12)break}}
}
const preferenceKey=dual?'k14:'+input:input
const prefs=preferences&&Array.isArray(preferences[preferenceKey])?preferences[preferenceKey]:[]
const rank=item=>{const i=prefs.findIndex(x=>x.word===item.word&&x.consume===item.consume);return i<0?100:i}
// Rank only real dictionary results: a damaged preference cannot inject text.
return result.map((item,index)=>({item,index})).sort((a,b)=>rank(a.item)-rank(b.item)||(ambiguous?(b.item.consume-a.item.consume||(frequencies[b.item.word]||0)-(frequencies[a.item.word]||0)):0)||a.index-b.index).map(x=>x.item)
const predictions=await predict(context),contextRank=item=>{const i=predictions.findIndex(x=>x.word===item.word);return i<0?0:8-i}
const category=item=>item.completion?1:item.consume===input.length?0:2
// Preserve every exact candidate; complete unfinished syllables before
// shorter fallbacks, and use word frequency plus the editor's local context.
return result.map((item,index)=>({item,index})).sort((a,b)=>rank(a.item)-rank(b.item)||category(a.item)-category(b.item)||b.item.consume-a.item.consume||contextRank(b.item)-contextRank(a.item)||(b.item.frequency||0)-(a.item.frequency||0)||a.index-b.index).map(x=>x.item)
},
clear(){cache.length=0;manifest=null},
get cacheSize(){return cache.length}