chore: normalize formatting and local commit policy
This commit is contained in:
1 parent
4332799ea4
commit
7119bd451a
18 files changed
+174
-222
No files matched your search
+2
-2
@@ -30,8 +30,8 @@ tocodex-docs/
|
||||
.codex/
|
||||
*.internal.md
|
||||
|
||||
# Local commit hooks are machine-specific policy and must never be published.
|
||||
/.local-hooks/
|
||||
# Local commit policy is machine-specific and must never be published.
|
||||
/.pre-commit-config.yaml
|
||||
|
||||
# `docs/` is private by default. Public documentation must be reviewed and
|
||||
# explicitly added to this file one path at a time; never allowlist another subtree.
|
||||
|
||||
@@ -727,7 +727,8 @@ fn parse_envelope(bytes: &[u8]) -> Option<AiHookEvent> {
|
||||
message: message.clone(),
|
||||
selection: context_string(&payload, &["selection", "selected_text", "selectedText"]),
|
||||
raw_context: sanitized_raw_context(&payload),
|
||||
}); Some(AiHookEvent {
|
||||
});
|
||||
Some(AiHookEvent {
|
||||
pane,
|
||||
source,
|
||||
kind,
|
||||
@@ -1250,11 +1251,15 @@ mod remote_tests {
|
||||
};
|
||||
let mut gate = AiHookEventGate::default();
|
||||
|
||||
let done = event("{\"kind\":\"done\",\"session_id\":\"s\",\"bridge_sequence\":3,\"event_id\":\"s:3\"}");
|
||||
let done = event(
|
||||
"{\"kind\":\"done\",\"session_id\":\"s\",\"bridge_sequence\":3,\"event_id\":\"s:3\"}",
|
||||
);
|
||||
assert_eq!(gate.verdict(&done, 3), GateVerdict::Accepted);
|
||||
assert_eq!(gate.verdict(&done, 3), GateVerdict::DuplicateEventId);
|
||||
|
||||
let older = event("{\"kind\":\"prompt\",\"session_id\":\"s\",\"bridge_sequence\":2,\"event_id\":\"s:2\"}");
|
||||
let older = event(
|
||||
"{\"kind\":\"prompt\",\"session_id\":\"s\",\"bridge_sequence\":2,\"event_id\":\"s:2\"}",
|
||||
);
|
||||
assert_eq!(gate.verdict(&older, 3), GateVerdict::StaleSequence);
|
||||
|
||||
// 无序号但带 provider 时间戳:走时间判据。
|
||||
|
||||
@@ -392,10 +392,7 @@ mod tests {
|
||||
let template_iterations = 200_000u128;
|
||||
let template_started = Instant::now();
|
||||
for _ in 0..template_iterations {
|
||||
black_box(UiLanguage::EnUs.tr_args(
|
||||
"provider.test.success",
|
||||
&[("status", "200")],
|
||||
));
|
||||
black_box(UiLanguage::EnUs.tr_args("provider.test.success", &[("status", "200")]));
|
||||
}
|
||||
let template_ns = template_started.elapsed().as_nanos() / template_iterations;
|
||||
|
||||
|
||||
@@ -235,8 +235,7 @@ pub(crate) fn suggest_update(
|
||||
// 远端目录是异步拉回来的,那一刻 cwd 与行都没变——少了这个代际,拉到的
|
||||
// 条目要等用户再多打一个字符才会显形。
|
||||
let remote_generation = crate::remote_dirs::generation();
|
||||
let key =
|
||||
format!("{}\u{0}{line}\u{0}{command_generation}\u{0}{remote_generation}", state.cwd);
|
||||
let key = format!("{}\u{0}{line}\u{0}{command_generation}\u{0}{remote_generation}", state.cwd);
|
||||
if state.completion_suppressed_line.as_deref() == Some(line.as_str()) {
|
||||
state.suggestion_key = key;
|
||||
state.suggestion.clear();
|
||||
|
||||
@@ -2947,10 +2947,9 @@ mod tests {
|
||||
}
|
||||
// 同一批内容的行内形态被硬换行拆开时保持原文,不再跨行拼接。
|
||||
assert!(
|
||||
scan_grid(&TextGrid::from_rows(&[
|
||||
r"• $\lim_{x\to0}\frac{\sin",
|
||||
r"x}{x}=1$,$PV=nRT$",
|
||||
]))
|
||||
scan_grid(&TextGrid::from_rows(
|
||||
&[r"• $\lim_{x\to0}\frac{\sin", r"x}{x}=1$,$PV=nRT$",]
|
||||
))
|
||||
.iter()
|
||||
.all(|overlay| !overlay.source.contains("lim")),
|
||||
"行内公式不得跨真实换行拼接"
|
||||
|
||||
@@ -92,8 +92,8 @@ mod ssh_session;
|
||||
mod ssh_sftp;
|
||||
mod string;
|
||||
mod svn_status;
|
||||
mod taskbar;
|
||||
mod sync;
|
||||
mod taskbar;
|
||||
mod terminal_profiles;
|
||||
mod tray;
|
||||
mod update_check;
|
||||
|
||||
@@ -25,9 +25,8 @@ pub fn attach_handler() {
|
||||
// MessageBox 的模态消息泵不能运行在发生 panic 的 GPUI 线程上,否则
|
||||
// 会重入 AppCell 并把原始 panic 升级成无诊断信息的 double panic。
|
||||
// 等待专用线程结束可保证主线程 unwind 退出前用户仍能看到并复制错误。
|
||||
if let Ok(dialog_thread) = thread::Builder::new()
|
||||
.name("panic-dialog".into())
|
||||
.spawn(move || unsafe {
|
||||
if let Ok(dialog_thread) =
|
||||
thread::Builder::new().name("panic-dialog".into()).spawn(move || unsafe {
|
||||
MessageBoxW(
|
||||
ptr::null_mut(),
|
||||
dialog_message.as_ptr(),
|
||||
|
||||
@@ -114,7 +114,10 @@ fn snapshot() -> Result<std::collections::HashMap<u32, (u32, String)>, String> {
|
||||
|
||||
let handle = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
|
||||
if handle == INVALID_HANDLE_VALUE {
|
||||
return Err(format!("CreateToolhelp32Snapshot failed: {}", std::io::Error::last_os_error()));
|
||||
return Err(format!(
|
||||
"CreateToolhelp32Snapshot failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
));
|
||||
}
|
||||
|
||||
let mut processes: HashMap<u32, (u32, String)> = HashMap::new();
|
||||
@@ -381,9 +384,7 @@ mod tests {
|
||||
|
||||
/// 带自定义可执行名的进程表,用来构造「helper 上面隔着一层 shell」的链。
|
||||
fn named_table(rows: &[(u32, u32, &str)]) -> HashMap<u32, (u32, String)> {
|
||||
rows.iter()
|
||||
.map(|&(pid, parent, exe)| (pid, (parent, exe.to_owned())))
|
||||
.collect()
|
||||
rows.iter().map(|&(pid, parent, exe)| (pid, (parent, exe.to_owned()))).collect()
|
||||
}
|
||||
|
||||
/// hook helper 的父进程往往不是 agent 本身:Windows 上 claude 通过
|
||||
|
||||
@@ -280,10 +280,11 @@ mod tests {
|
||||
assert!(!begin_fetch(&env, "/tmp/a"), "已经在拉了");
|
||||
assert!(begin_fetch(&env, "/tmp/b"), "另一个目录不受影响");
|
||||
|
||||
finish_fetch(&env, "/tmp/a", Some(vec![RemoteEntry {
|
||||
name: "x".to_owned(),
|
||||
is_dir: false,
|
||||
}]));
|
||||
finish_fetch(
|
||||
&env,
|
||||
"/tmp/a",
|
||||
Some(vec![RemoteEntry { name: "x".to_owned(), is_dir: false }]),
|
||||
);
|
||||
assert!(lookup(&env, "/tmp/a").is_some(), "落了缓存");
|
||||
assert!(begin_fetch(&env, "/tmp/a"), "拉完就解锁");
|
||||
}
|
||||
|
||||
@@ -38,10 +38,7 @@ pub(super) fn request_once(
|
||||
Ok(serde_json::from_str(&line)?)
|
||||
}
|
||||
|
||||
pub(super) fn print_response(
|
||||
response: &ApiResponse,
|
||||
pretty: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
pub(super) fn print_response(response: &ApiResponse, pretty: bool) -> Result<(), Box<dyn Error>> {
|
||||
if pretty {
|
||||
println!("{}", serde_json::to_string_pretty(response)?);
|
||||
} else {
|
||||
@@ -225,8 +222,7 @@ fn run_cli_inner(options: ControlOptions) -> Result<(), Box<dyn Error>> {
|
||||
print_response(&response, options.pretty)
|
||||
},
|
||||
CliCommand::CloseWindow { window } => {
|
||||
let response =
|
||||
request_once("window.close", json!({ "window_id": window }), timeout)?;
|
||||
let response = request_once("window.close", json!({ "window_id": window }), timeout)?;
|
||||
print_response(&response, options.pretty)
|
||||
},
|
||||
CliCommand::Focus { window, pane } => {
|
||||
|
||||
@@ -238,10 +238,7 @@ impl RuntimeCommand {
|
||||
},
|
||||
"tab.close" => {
|
||||
let params: TabTargetParams = parse_params(&request.params)?;
|
||||
Ok(Self::CloseTab {
|
||||
window_id: params.window_id,
|
||||
tab_index: params.tab_index,
|
||||
})
|
||||
Ok(Self::CloseTab { window_id: params.window_id, tab_index: params.tab_index })
|
||||
},
|
||||
"tab.rename" => {
|
||||
let params: RenameTabParams = parse_params(&request.params)?;
|
||||
@@ -555,9 +552,9 @@ fn validate_multiline_text(text: &str, label: &str) -> Result<(), ApiError> {
|
||||
)));
|
||||
}
|
||||
if text.chars().any(|ch| ch.is_control() && !matches!(ch, '\n' | '\r' | '\t')) {
|
||||
return Err(ApiError::invalid_params(
|
||||
format!("{label} contains unsupported terminal control characters"),
|
||||
));
|
||||
return Err(ApiError::invalid_params(format!(
|
||||
"{label} contains unsupported terminal control characters"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -34,9 +34,10 @@ use super::*;
|
||||
use crate::cli::{
|
||||
AgentCommand, AgentOptions, AgentPasteOptions, AgentReadOptions, AgentSendOptions,
|
||||
AgentWaitOptions, EnvOptions, ListOptions, PaneCloseOptions, PaneCommand, PaneExecOptions,
|
||||
PaneOptions, PanePasteOptions, PaneReadOptions, PaneResizeOptions, PaneSendOptions, PaneWaitOptions,
|
||||
PaneZoomOptions, PasteSourceOptions, TabCloseOptions, TabCommand, TabMoveOptions,
|
||||
TabRenameOptions, TabResourceOptions, WindowCloseOptions, WindowCommand, WindowResourceOptions,
|
||||
PaneOptions, PanePasteOptions, PaneReadOptions, PaneResizeOptions, PaneSendOptions,
|
||||
PaneWaitOptions, PaneZoomOptions, PasteSourceOptions, TabCloseOptions, TabCommand,
|
||||
TabMoveOptions, TabRenameOptions, TabResourceOptions, WindowCloseOptions, WindowCommand,
|
||||
WindowResourceOptions,
|
||||
};
|
||||
use std::fs::File;
|
||||
use std::io::Read as _;
|
||||
@@ -153,11 +154,7 @@ pub fn agent(options: AgentOptions) -> Result<(), Box<dyn Error>> {
|
||||
|
||||
fn window_close(options: WindowCloseOptions) -> Result<(), Box<dyn Error>> {
|
||||
let timeout = validated_timeout(options.timeout_ms)?;
|
||||
let response = request_once(
|
||||
"window.close",
|
||||
json!({ "window_id": options.window }),
|
||||
timeout,
|
||||
)?;
|
||||
let response = request_once("window.close", json!({ "window_id": options.window }), timeout)?;
|
||||
print_response(&response, options.output.pretty)
|
||||
}
|
||||
|
||||
@@ -776,19 +773,15 @@ fn read_paste_source(source: PasteSourceOptions) -> Result<String, Box<dyn Error
|
||||
} else {
|
||||
joined(&text)
|
||||
};
|
||||
validate_paste_text(&value)
|
||||
.map_err(|error| CliError::new("invalid_params", error.message))?;
|
||||
validate_paste_text(&value).map_err(|error| CliError::new("invalid_params", error.message))?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn read_paste_utf8(reader: impl Read, label: &str) -> Result<String, Box<dyn Error>> {
|
||||
let mut bytes = Vec::new();
|
||||
reader
|
||||
.take((MAX_PROMPT_BYTES + 1) as u64)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|error| {
|
||||
CliError::new("input_read_error", format!("could not read paste input {label}: {error}"))
|
||||
})?;
|
||||
reader.take((MAX_PROMPT_BYTES + 1) as u64).read_to_end(&mut bytes).map_err(|error| {
|
||||
CliError::new("input_read_error", format!("could not read paste input {label}: {error}"))
|
||||
})?;
|
||||
if bytes.len() > MAX_PROMPT_BYTES {
|
||||
return Err(CliError::new(
|
||||
"invalid_params",
|
||||
@@ -797,11 +790,8 @@ fn read_paste_utf8(reader: impl Read, label: &str) -> Result<String, Box<dyn Err
|
||||
.into());
|
||||
}
|
||||
String::from_utf8(bytes).map_err(|error| {
|
||||
CliError::new(
|
||||
"invalid_utf8",
|
||||
format!("paste input {label} is not valid UTF-8: {error}"),
|
||||
)
|
||||
.into()
|
||||
CliError::new("invalid_utf8", format!("paste input {label} is not valid UTF-8: {error}"))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -907,16 +897,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn paste_reader_is_strict_utf8_and_bounded() {
|
||||
let text = read_paste_utf8(std::io::Cursor::new("first\nsecond"), "memory")
|
||||
.expect("valid UTF-8");
|
||||
let text =
|
||||
read_paste_utf8(std::io::Cursor::new("first\nsecond"), "memory").expect("valid UTF-8");
|
||||
assert_eq!(text, "first\nsecond");
|
||||
assert!(read_paste_utf8(std::io::Cursor::new([0xff]), "memory").is_err());
|
||||
assert!(
|
||||
read_paste_utf8(
|
||||
std::io::Cursor::new(vec![b'x'; MAX_PROMPT_BYTES + 1]),
|
||||
"memory",
|
||||
)
|
||||
.is_err()
|
||||
read_paste_utf8(std::io::Cursor::new(vec![b'x'; MAX_PROMPT_BYTES + 1]), "memory",)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -310,11 +310,7 @@ impl WindowContext {
|
||||
Ok(self.close_pane(pane_id))
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_set_zoom(
|
||||
&mut self,
|
||||
pane_id: u64,
|
||||
zoomed: bool,
|
||||
) -> Result<(), ApiError> {
|
||||
pub(crate) fn runtime_set_zoom(&mut self, pane_id: u64, zoomed: bool) -> Result<(), ApiError> {
|
||||
let Some(tab_index) = self.tabs.iter().position(|tab| {
|
||||
let mut panes = Vec::new();
|
||||
tab.layout.leaves(&mut panes);
|
||||
@@ -344,11 +340,7 @@ impl WindowContext {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_resize_pane(
|
||||
&mut self,
|
||||
pane_id: u64,
|
||||
ratio: f32,
|
||||
) -> Result<(), ApiError> {
|
||||
pub(crate) fn runtime_resize_pane(&mut self, pane_id: u64, ratio: f32) -> Result<(), ApiError> {
|
||||
let Some(tab_index) = self.tabs.iter().position(|tab| {
|
||||
let mut panes = Vec::new();
|
||||
tab.layout.leaves(&mut panes);
|
||||
|
||||
@@ -133,15 +133,7 @@ impl Layout {
|
||||
/// 设置目标 pane 在最近父 Split 中占用的比例。内部 ratio 描述 first,
|
||||
/// 因此 second 侧目标要换算成 `1 - target_ratio`。
|
||||
pub(super) fn set_leaf_parent_ratio(&mut self, target: PaneId, target_ratio: f32) -> bool {
|
||||
let Layout::Split {
|
||||
ratio,
|
||||
preview_ratio,
|
||||
dragging,
|
||||
first,
|
||||
second,
|
||||
..
|
||||
} = self
|
||||
else {
|
||||
let Layout::Split { ratio, preview_ratio, dragging, first, second, .. } = self else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,118 +1,118 @@
|
||||
//! 文件行数预算守护(docs/project-constraints.md 第 1 条)。
|
||||
//!
|
||||
//! 单个 `.rs` 文件硬上限 2000 行;存量超标文件在 [`GRANDFATHERED`] 登记,
|
||||
//! 登记值只允许下调。测试失败的正确动作是拆文件,不是上调预算。
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// 新代码红线:超过它就必须拆(docs/project-constraints.md)。
|
||||
const MAX_LINES: usize = 2000;
|
||||
|
||||
/// 存量豁免清单(相对仓库根,正斜杠路径 → 行数上限)。
|
||||
///
|
||||
/// 登记值 = 收录当天的行数加不足百行的呼吸空间:小修小补不触线,成规模
|
||||
/// 新增必须伴随拆分。拆小后同步下调或删除对应条目。
|
||||
const GRANDFATHERED: &[(&str, usize)] = &[
|
||||
("nebula_app/src/display/mod.rs", 12000),
|
||||
("nebula_app/src/display/settings.rs", 8650),
|
||||
// 2026-08-17 上调(4850 → 5150):git 树 VS Code 化 + 源码 tab 接线;
|
||||
// 下一步应把 VCS 面板拆成独立文件后下调。
|
||||
("nebula_app/src/gpui_shell/workspace.rs", 5150),
|
||||
// 2026-08-17 下调(3600 → 3550):网络区拆到 network_settings.rs。
|
||||
("nebula_app/src/gpui_shell/settings_pane.rs", 3550),
|
||||
("nebula_app/src/display/command_palette.rs", 3980),
|
||||
("nebula_app/src/window_context.rs", 3800),
|
||||
("nebula_app/src/event.rs", 3780),
|
||||
("nebula_terminal/src/term/mod.rs", 3650),
|
||||
// 2026-08-17 上调(2900 → 2970):draw_overlays 几何抽成两壳共享的 plan。
|
||||
("nebula_app/src/display/terminal_math.rs", 2970),
|
||||
// 2026-08-17 上调(2680 → 2760):SVN wc.db 回退 + 单文件 stage/discard。
|
||||
("nebula_app/src/display/side_panel.rs", 2760),
|
||||
("nebula_app/src/gpui_shell/terminal/view.rs", 2540),
|
||||
("nebula_app/src/display/chrome.rs", 2550),
|
||||
("nebula_app/src/display/markdown_view.rs", 2150),
|
||||
];
|
||||
|
||||
/// 这些目录不受预算约束:构建产物、vendored 第三方代码、版本库内部。
|
||||
fn excluded(component: &str) -> bool {
|
||||
component == ".git"
|
||||
|| component == "third_party"
|
||||
|| component == "target"
|
||||
|| component.starts_with(".target")
|
||||
}
|
||||
|
||||
fn collect_rust_files(root: &Path, out: &mut Vec<PathBuf>) {
|
||||
let Ok(entries) = fs::read_dir(root) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
// 隐藏项(.git、.tmp_* 草稿等)不是源码,不受预算约束。
|
||||
if name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
if path.is_dir() {
|
||||
if !excluded(&name) {
|
||||
collect_rust_files(&path, out);
|
||||
}
|
||||
} else if path.extension().is_some_and(|extension| extension == "rs") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_source_file_respects_its_line_budget() {
|
||||
let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("nebula_app sits directly under the repo root")
|
||||
.to_path_buf();
|
||||
|
||||
let mut files = Vec::new();
|
||||
collect_rust_files(&repo_root, &mut files);
|
||||
assert!(
|
||||
files.len() > 100,
|
||||
"行数守护扫到的文件数异常({}),排除规则可能吞掉了源码目录",
|
||||
files.len()
|
||||
);
|
||||
|
||||
let mut violations = Vec::new();
|
||||
let mut stale_grants = Vec::new();
|
||||
for path in files {
|
||||
let relative = path
|
||||
.strip_prefix(&repo_root)
|
||||
.expect("collected files live under the repo root")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
let Ok(content) = fs::read(&path) else {
|
||||
panic!("无法读取 {relative}");
|
||||
};
|
||||
// 按换行字节计数:与编码无关,空文件为 0 行。
|
||||
let lines = content.iter().filter(|byte| **byte == b'\n').count()
|
||||
+ usize::from(!content.is_empty() && content.last() != Some(&b'\n'));
|
||||
let grant =
|
||||
GRANDFATHERED.iter().find(|(name, _)| *name == relative).map(|(_, budget)| *budget);
|
||||
let budget = grant.unwrap_or(MAX_LINES);
|
||||
if lines > budget {
|
||||
violations.push(format!("{relative}: {lines} 行 > 预算 {budget}"));
|
||||
}
|
||||
// 豁免文件瘦身到红线以内后,条目必须删除,防止预算复胖。
|
||||
if let Some(budget) = grant
|
||||
&& lines <= MAX_LINES
|
||||
{
|
||||
stale_grants.push(format!(
|
||||
"{relative}: 已降到 {lines} 行(≤{MAX_LINES}),豁免条目({budget})应删除"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"以下文件超出行数预算(docs/project-constraints.md 第 1 条,正确动作是拆文件):\n{}",
|
||||
violations.join("\n")
|
||||
);
|
||||
assert!(stale_grants.is_empty(), "以下豁免条目已过期:\n{}", stale_grants.join("\n"));
|
||||
}
|
||||
//! 文件行数预算守护(docs/project-constraints.md 第 1 条)。
|
||||
//!
|
||||
//! 单个 `.rs` 文件硬上限 2000 行;存量超标文件在 [`GRANDFATHERED`] 登记,
|
||||
//! 登记值只允许下调。测试失败的正确动作是拆文件,不是上调预算。
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// 新代码红线:超过它就必须拆(docs/project-constraints.md)。
|
||||
const MAX_LINES: usize = 2000;
|
||||
|
||||
/// 存量豁免清单(相对仓库根,正斜杠路径 → 行数上限)。
|
||||
///
|
||||
/// 登记值 = 收录当天的行数加不足百行的呼吸空间:小修小补不触线,成规模
|
||||
/// 新增必须伴随拆分。拆小后同步下调或删除对应条目。
|
||||
const GRANDFATHERED: &[(&str, usize)] = &[
|
||||
("nebula_app/src/display/mod.rs", 12000),
|
||||
("nebula_app/src/display/settings.rs", 8650),
|
||||
// 2026-08-17 上调(4850 → 5150):git 树 VS Code 化 + 源码 tab 接线;
|
||||
// 下一步应把 VCS 面板拆成独立文件后下调。
|
||||
("nebula_app/src/gpui_shell/workspace.rs", 5150),
|
||||
// 2026-08-17 下调(3600 → 3550):网络区拆到 network_settings.rs。
|
||||
("nebula_app/src/gpui_shell/settings_pane.rs", 3550),
|
||||
("nebula_app/src/display/command_palette.rs", 3980),
|
||||
("nebula_app/src/window_context.rs", 3800),
|
||||
("nebula_app/src/event.rs", 3780),
|
||||
("nebula_terminal/src/term/mod.rs", 3650),
|
||||
// 2026-08-17 上调(2900 → 2970):draw_overlays 几何抽成两壳共享的 plan。
|
||||
("nebula_app/src/display/terminal_math.rs", 2970),
|
||||
// 2026-08-17 上调(2680 → 2760):SVN wc.db 回退 + 单文件 stage/discard。
|
||||
("nebula_app/src/display/side_panel.rs", 2760),
|
||||
("nebula_app/src/gpui_shell/terminal/view.rs", 2540),
|
||||
("nebula_app/src/display/chrome.rs", 2550),
|
||||
("nebula_app/src/display/markdown_view.rs", 2150),
|
||||
];
|
||||
|
||||
/// 这些目录不受预算约束:构建产物、vendored 第三方代码、版本库内部。
|
||||
fn excluded(component: &str) -> bool {
|
||||
component == ".git"
|
||||
|| component == "third_party"
|
||||
|| component == "target"
|
||||
|| component.starts_with(".target")
|
||||
}
|
||||
|
||||
fn collect_rust_files(root: &Path, out: &mut Vec<PathBuf>) {
|
||||
let Ok(entries) = fs::read_dir(root) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
// 隐藏项(.git、.tmp_* 草稿等)不是源码,不受预算约束。
|
||||
if name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
if path.is_dir() {
|
||||
if !excluded(&name) {
|
||||
collect_rust_files(&path, out);
|
||||
}
|
||||
} else if path.extension().is_some_and(|extension| extension == "rs") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_source_file_respects_its_line_budget() {
|
||||
let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("nebula_app sits directly under the repo root")
|
||||
.to_path_buf();
|
||||
|
||||
let mut files = Vec::new();
|
||||
collect_rust_files(&repo_root, &mut files);
|
||||
assert!(
|
||||
files.len() > 100,
|
||||
"行数守护扫到的文件数异常({}),排除规则可能吞掉了源码目录",
|
||||
files.len()
|
||||
);
|
||||
|
||||
let mut violations = Vec::new();
|
||||
let mut stale_grants = Vec::new();
|
||||
for path in files {
|
||||
let relative = path
|
||||
.strip_prefix(&repo_root)
|
||||
.expect("collected files live under the repo root")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
let Ok(content) = fs::read(&path) else {
|
||||
panic!("无法读取 {relative}");
|
||||
};
|
||||
// 按换行字节计数:与编码无关,空文件为 0 行。
|
||||
let lines = content.iter().filter(|byte| **byte == b'\n').count()
|
||||
+ usize::from(!content.is_empty() && content.last() != Some(&b'\n'));
|
||||
let grant =
|
||||
GRANDFATHERED.iter().find(|(name, _)| *name == relative).map(|(_, budget)| *budget);
|
||||
let budget = grant.unwrap_or(MAX_LINES);
|
||||
if lines > budget {
|
||||
violations.push(format!("{relative}: {lines} 行 > 预算 {budget}"));
|
||||
}
|
||||
// 豁免文件瘦身到红线以内后,条目必须删除,防止预算复胖。
|
||||
if let Some(budget) = grant
|
||||
&& lines <= MAX_LINES
|
||||
{
|
||||
stale_grants.push(format!(
|
||||
"{relative}: 已降到 {lines} 行(≤{MAX_LINES}),豁免条目({budget})应删除"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"以下文件超出行数预算(docs/project-constraints.md 第 1 条,正确动作是拆文件):\n{}",
|
||||
violations.join("\n")
|
||||
);
|
||||
assert!(stale_grants.is_empty(), "以下豁免条目已过期:\n{}", stale_grants.join("\n"));
|
||||
}
|
||||
@@ -244,9 +244,7 @@ fn base64_encode(input: &[u8]) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
FOREIGN_HOOK_RUNNERS, Outcome, base64_encode, foreign_hook_runner, outcome_line,
|
||||
};
|
||||
use super::{FOREIGN_HOOK_RUNNERS, Outcome, base64_encode, foreign_hook_runner, outcome_line};
|
||||
|
||||
#[test]
|
||||
fn encodes_remote_hook_payload() {
|
||||
|
||||
@@ -443,11 +443,8 @@ impl ThemeName {
|
||||
/// 两块深色面板会糊成一片,竖线是这种形态下唯一的结构分界。
|
||||
pub fn card_geometry(self) -> ThemeCardGeometry {
|
||||
match self {
|
||||
Self::Nord => ThemeCardGeometry {
|
||||
radius: 0.0,
|
||||
gutter: 0.0,
|
||||
shadow: false,
|
||||
divider: 1.0,
|
||||
Self::Nord => {
|
||||
ThemeCardGeometry { radius: 0.0, gutter: 0.0, shadow: false, divider: 1.0 }
|
||||
},
|
||||
_ => ThemeCardGeometry {
|
||||
radius: DEFAULT_PANE_CARD_RADIUS,
|
||||
|
||||
@@ -231,15 +231,7 @@ impl<T: Copy + Eq> SplitTree<T> {
|
||||
/// pane;目标在第二侧时必须写成 `1 - target_ratio`。递归到最近父节点再
|
||||
/// 修改,避免嵌套 pane 的 resize 错改祖先 Split。
|
||||
pub fn set_leaf_parent_ratio(&mut self, target: T, target_ratio: f32) -> bool {
|
||||
let SplitTree::Split {
|
||||
ratio,
|
||||
preview_ratio,
|
||||
dragging,
|
||||
first,
|
||||
second,
|
||||
..
|
||||
} = self
|
||||
else {
|
||||
let SplitTree::Split { ratio, preview_ratio, dragging, first, second, .. } = self else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user