mirror of
https://github.com/qingjian-team/qingjian.git
synced 2026-11-06 16:13:55 +00:00
fix(windows): 云联想请求带上 DLL 报来的光标前文 (#313)
Server 收到的应用前文只喂给了本地整句重排,云联想的 before 一直为空;现在随请求带上,前文晚到时重发一次。 Closes #283
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
mod state;
|
||||
|
||||
use qingjian_core::{Candidate, CandidateLayout, CandidateList, CloudWord, Query};
|
||||
use qingjian_core::{Candidate, CandidateLayout, CandidateList, CloudWord, Query, SurroundingText};
|
||||
use qingjian_platform::protocol::{Frame, PROTOCOL_VERSION, PreeditKind, PreeditSegment};
|
||||
|
||||
pub(super) use self::state::{Composed, TypedKeys};
|
||||
@@ -30,7 +30,8 @@ impl Router {
|
||||
let layout =
|
||||
CandidateLayout::new(items, self.config.page_size, self.config.cloud_slots);
|
||||
if self.engine.prediction_enabled() {
|
||||
self.engine.request_prediction(None, layout.local());
|
||||
let surrounding = self.surrounding_for_prediction();
|
||||
self.engine.request_prediction(surrounding, layout.local());
|
||||
}
|
||||
Composed::Candidates {
|
||||
preedit,
|
||||
@@ -89,6 +90,24 @@ impl Router {
|
||||
}
|
||||
}
|
||||
|
||||
/// DLL 送来新的光标前文后重发一次云联想:作废在飞的空上下文请求,让云端拿得到 before。
|
||||
pub(super) fn refresh_prediction(&mut self) {
|
||||
let Some(Composed::Candidates { layout, .. }) = &self.composed else {
|
||||
return;
|
||||
};
|
||||
let local = layout.local().to_vec();
|
||||
let surrounding = self.surrounding_for_prediction();
|
||||
self.engine.request_prediction(surrounding, &local);
|
||||
}
|
||||
|
||||
/// DLL 报来的光标前文包成云联想要的形状;没有就 `None`(Engine 只靠拼音猜)。
|
||||
pub(super) fn surrounding_for_prediction(&self) -> Option<SurroundingText> {
|
||||
(!self.surrounding_before.is_empty()).then(|| SurroundingText {
|
||||
before: self.surrounding_before.clone(),
|
||||
after: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 高亮移动 `delta`,夹在 `[0, 末尾]`,到页边自然换页。
|
||||
pub(super) fn move_highlight(&mut self, delta: isize) {
|
||||
let count = self.candidate_count();
|
||||
@@ -259,3 +278,80 @@ pub(super) fn marked_parts(query: &Query) -> (Vec<PreeditSegment>, usize, Option
|
||||
});
|
||||
(preedit, query.segments_cursor(), typed_keys)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dispatch::{Router, RouterConfig};
|
||||
use qingjian_core::Engine;
|
||||
use qingjian_dictionary::Dictionary;
|
||||
use qingjian_platform::protocol::{ClientMessage, KeyEvent, KeyModifiers, SessionId};
|
||||
|
||||
fn router() -> Router {
|
||||
let mut router = Router::new(
|
||||
Engine::new(Dictionary::parse("你\tni\t100\n").unwrap()),
|
||||
RouterConfig::default(),
|
||||
);
|
||||
router.handle(ClientMessage::OpenSession {
|
||||
session: SessionId(1),
|
||||
app: None,
|
||||
protocol: PROTOCOL_VERSION,
|
||||
});
|
||||
router
|
||||
}
|
||||
|
||||
fn compose(router: &mut Router, text: &str) {
|
||||
for c in text.chars() {
|
||||
router
|
||||
.handle(ClientMessage::Key {
|
||||
session: SessionId(1),
|
||||
event: KeyEvent::new(c as u32, Some(c), KeyModifiers::default()),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// 回归 #283:DLL 报来的光标前文要成为云联想的 `before` 上下文;
|
||||
/// 没前文时不带上下文,空文本不算上下文,组句结束即作废。
|
||||
#[test]
|
||||
fn surrounding_context_flows_to_prediction_and_clears_on_reset() {
|
||||
let mut router = router();
|
||||
compose(&mut router, "ni");
|
||||
assert!(router.surrounding_for_prediction().is_none());
|
||||
|
||||
router.handle(ClientMessage::Surrounding {
|
||||
session: SessionId(1),
|
||||
text: "这个文件权限有问题,".to_owned(),
|
||||
});
|
||||
let surrounding = router.surrounding_for_prediction().expect("应带上前文");
|
||||
assert_eq!(surrounding.before, "这个文件权限有问题,");
|
||||
assert_eq!(surrounding.after, "");
|
||||
|
||||
// 空文本不算上下文(Engine 只靠拼音猜,不猜错的)
|
||||
router.handle(ClientMessage::Surrounding {
|
||||
session: SessionId(1),
|
||||
text: String::new(),
|
||||
});
|
||||
assert!(router.surrounding_for_prediction().is_none());
|
||||
|
||||
// 组句结束:前文作废,等下一段组句 DLL 再送
|
||||
router.handle(ClientMessage::Surrounding {
|
||||
session: SessionId(1),
|
||||
text: "别的内容".to_owned(),
|
||||
});
|
||||
router.stop_rescoring();
|
||||
assert!(router.surrounding_for_prediction().is_none());
|
||||
}
|
||||
|
||||
/// 不是聚焦会话的前文不收,防止换应用后把 A 应用的文字带进 B 的联想。
|
||||
#[test]
|
||||
fn surrounding_from_unfocused_session_is_dropped() {
|
||||
let mut router = router();
|
||||
compose(&mut router, "ni");
|
||||
router.handle(ClientMessage::Surrounding {
|
||||
session: SessionId(99),
|
||||
text: "别的应用的前文".to_owned(),
|
||||
});
|
||||
assert!(router.surrounding_for_prediction().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,10 @@ pub struct Router {
|
||||
/// 上次套用的 `[model]`,变了才重载 / 卸载。
|
||||
applied_model: LocalModelConfig,
|
||||
|
||||
/// DLL 报来的聚焦会话光标前文:云联想按它带 `before` 上下文。TSF 只读光标前文,
|
||||
/// `after` 恒为空;组句结束 / 焦点切换即作废(新的组句 DLL 会再送)。
|
||||
surrounding_before: String,
|
||||
|
||||
/// 重排的防抖 / 轮询进行态。
|
||||
rescore: RescoreState,
|
||||
}
|
||||
@@ -154,6 +158,7 @@ impl Router {
|
||||
code_table: None,
|
||||
model_loader: None,
|
||||
applied_model: LocalModelConfig::default(),
|
||||
surrounding_before: String::new(),
|
||||
rescore: RescoreState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ impl Router {
|
||||
/// 组句结束:什么都不等了;应用前文也作废(下一段组句 DLL 会再送)。
|
||||
pub(super) fn stop_rescoring(&mut self) {
|
||||
self.rescore.stop();
|
||||
self.surrounding_before.clear();
|
||||
self.engine.set_rescoring_context(None);
|
||||
}
|
||||
|
||||
@@ -119,12 +120,15 @@ impl Router {
|
||||
if self.focused != Some(session) || self.engine.composition().is_empty() {
|
||||
return;
|
||||
}
|
||||
self.surrounding_before = text.clone();
|
||||
self.engine
|
||||
.set_rescoring_context((!text.is_empty()).then_some(text));
|
||||
if matches!(self.composed, Some(Composed::Candidates { .. })) {
|
||||
// 查一次只为按新前文重新记下要打分的文本,候选顺序此刻不变
|
||||
let _ = self.engine.query();
|
||||
self.schedule_rescoring();
|
||||
// 云联想也在等这份前文:带着新上下文重发,作废在飞的空上下文请求(worker 端有防抖)
|
||||
self.refresh_prediction();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user