diff --git a/.agents/notes/implemented/feature/2026-10-28-macos-global-editor-appearance.md b/.agents/notes/implemented/feature/2026-10-28-macos-global-editor-appearance.md new file mode 100644 index 0000000000..1f04001367 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-28-macos-global-editor-appearance.md @@ -0,0 +1,96 @@ +# Agent 笔记:macOS 全局源码编辑器样式 + +状态:已实现 + +## 先说结论 + +所有项目的源码编辑器现在共享一组可持久化的样式偏好,包括已有字体、字号,以及新增的 +行间距、配色、行号、空白符、光标和常用显示开关。修改时更新现有编辑器的选项,不重建 +文档,因此光标、内容和撤销历史能够保留。设置不涉及 VS Code 或 IDEA 的检测与导入。 + +## 问题 + +此前 macOS 只暴露部分编辑器选项,高亮使用固定色表。将来接收其他 IDE 的配置前,产品 +需要先能保存和应用相应偏好;仅在设置页增加控件会遗漏分栏和完整源码预览。 + +## 决策 + +- 复用 `AppSettings` 和既有 `KeyValueStore`。字体、字号、软换行、缩略图继续使用原键, + 新选项由 `EditorAppearanceSettings` 保存为一份属性字典。每个字段独立校验和回退, + 一个未知枚举值不会清空其他有效设置。字号限制为 6–72,行间距用字号倍数 1–3; + 0 表示沿用 Monaco 的自动行高,不用新的默认值改变老用户排版。 +- 设置使用现有原生控件与共享 i18n 词条,说明进入信息提示;这些是 Lithe 自有的全局偏好, + 所有值即时保存,不引入第二个“应用”阶段。保留新版的编辑器软换行菜单与按平台确定的行号栏宽度。 +- `MonacoWorkbenchDisplayConfiguration` 的值比较包含全部新选项;将配置传入当前及 + 后续创建的编辑器。正确做法是调用 `updateOptions`;不要为了换主题重新创建模型, + 否则会丢失撤销栈,也可能破坏多个视图共享同一文档的关系。Git 作者栏使用自定义行号 + 渲染器;配置更新后要重新安装作者栏,关闭作者栏或切换标签时恢复用户选择的行号模式, + 不能硬编码回绝对行号。相对行号继续由 Monaco 在光标换行时重新绘制,不增加轮询。 +- macOS 隐藏行号时只隐藏数字,继续保留现有断点栏与鼠标操作区域。Monaco 的 `off` + 会连同行号层的断点装饰一起关闭,因此原生行号优先布局改用返回零宽字符的行号渲染器, + 让没有断点的行也保留可点击元素;不增加第二个断点栏。这个渲染器与 Git 作者栏的渲染器 + 分开识别,不能再把所有自定义行号函数都当成作者栏而禁用断点操作。Windows 继续使用 + 原有独立图标栏;持久化设置仍为 `off`,内部转换仅发生在编辑器适配层。 +- 高亮方案与工作台主题独立,提供 Lithe、GitHub、One、VS Code、IntelliJ IDEA,按当前深浅外观选对应色表。 + GitHub 与 One 直接复用原 Windows JSON,移到 `frontend/editor/themes/`,Windows + 只改变导入路径。共享主题生成器同时处理词法和语义角色别名,避免 Java 字段、方法等 + 仍沿用旧颜色。注释斜体可单独设置,取消时显式清除样式。 +- VS Code 使用官方 `b6bfa95ebee8b183d65dee4997c0527eadfe5384` 的 Dark/Light 2026 + 及其 Modern/Plus/VS 继承链;相同作用域取后定义,具体作用域优先。IDEA 使用 Community + `74dbd42b7a8787b0707437cb857c68e78199f611` 的 Islands Dark 与 Light 编辑器色表。 + `themes/vscode.json` 与 `themes/idea.json` 保存固定来源链接;仅映射现有词法/语义角色, + 不承诺完整复现两种 IDE 的语言分析或所有 TextMate scope。注释斜体仍由用户设置决定。 + MIT/Apache 许可证及 JetBrains NOTICE 随编辑器资源打包,运行时不新增下载或可写目录。 +- 彩色括号由配色方案决定:IDEA 关闭,其余当前内置方案启用;语法高亮分组显示只读状态, + 不提供单独开关。历史 `bracketPairColorization` 值不再读取或写出,避免旧偏好覆盖方案。 +- Java 方法颜色不能只看通用 function:IDEA 的方法调用继承正文色,普通声明使用声明色, + Darcula 父方案的 `STATIC_METHOD_ATTRIBUTES` 没有前景色,因此静态方法使用正文色。 + 依据为同一上游提交的 `DefaultColorSchemesManager.xml` 与 `JavaHighlightingColors.java`, + 并核对了本机 Islands Dark 配置。共享语义编码保留 declaration/static(在既有修饰符末尾 + 追加,保留原 bit 位置);词法 `function.java` 在 IDEA 中先用正文色,由语义信息细分。 + 不能把所有方法都改成蓝色,也不能为了截图把其他方案的 function 颜色一并改掉。 +- 字体枚举和字体缺失回退继续遵循现有 + [编程字体决策](2026-10-07-macos-editor-programming-font-family.md)。 +- 作用范围是主源码编辑器、分栏、独立文件窗口与完整源码预览。原生 Git Diff、SQL + 控制台、终端、输出窗口和结果行片段保留各自的排版规则;不把所有等宽文字绑定到同一 + 组编辑器选项。界面提示明确说明应用于所有项目的源码编辑器。 + +## 考虑过的备选方案 + +- 复制 Windows 色表到 macOS:会产生两个需要同步的版本,改为移动原文件并共享读取。 +- 将整个工作台主题与源码配色绑定:不能表达用户保留界面主题、只更换代码颜色的需求。 +- 导入外部配置后再补设置:导入结果缺少可见且可修改的落点。本次先完成设置本身, + 不提前引入外部格式解析或插件兼容层。 + +## 后果 + +新增偏好在原有应用设置存储中保存,生命周期和所有权均不变。色表属于构建输入,随 +编辑器资源打包;运行时只读,不下载字体或主题,不产生新的缓存目录,不改变签名包 +字节,也不影响 Sparkle 增量更新基线。 + +方案只覆盖当前列出的预设与选项,尚不等价于完整的 VS Code/IDEA 主题和配置格式。 +将来增加导入时必须转换单位、颜色角色与配置作用域,不能直接透传外部编辑器选项对象。 + +## 验证 + +- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'AppSettingsTests|MonacoWorkbenchThemeTests|SettingsSearchVocabularyTests|ContextMenuCoverageTests|EditorFontFamilyTests'` +- `./scripts/macos/probe/probe-macos-monaco.sh --workbench-tests`:真实 WebKit 验证新旧分栏、字号和行高、 + 显示选项、深浅配色、斜体撤销、内容和撤销栈保留;隐藏行号后的断点图标、已有断点与空白行的鼠标点击、恢复相对行号。 +- `node scripts/i18n/generate-i18n.mjs --check` +- `node scripts/i18n/verify-app-language-localization.mjs` +- `./scripts/macos/verify/verify-runtime-bundle-immutability.sh` +- `node scripts/architecture/verify-editor-boundaries.mjs` +- `./scripts/platform-feature-matrix/verify-platform-feature-matrix.sh` + +自动化不替代完整产品的设置布局、键盘操作、多项目与重启验收;矩阵单独记录待验证状态。 + +## 适用范围 + +- `macos/Sources/Lithe/Models/Settings/EditorAppearanceSettings.swift` +- `macos/Sources/Lithe/Models/Settings/AppSettings.swift` +- `macos/Sources/Lithe/Views/App/SettingsView.swift` +- `macos/Sources/Lithe/Platform/MacOS/MonacoWorkbenchEditor.swift` +- `frontend/editor/src/theme.ts` +- `frontend/editor/src/workbench.ts` +- `frontend/editor/themes/` +- `windows/tauri/src/extensions/themes/theme-loader.ts` diff --git a/docs/ci-builds.md b/docs/ci-builds.md index 9768d86460..ca607cc962 100644 --- a/docs/ci-builds.md +++ b/docs/ci-builds.md @@ -705,7 +705,11 @@ Inter 4.1 的 18 个静态 OTF(内部版本 4.001)及许可、JetBrains Mono - `.artifacts/bun-tmp/`、下载或解压过程中的临时目录; - `.artifacts/jdtls/`、`.artifacts/jdk-*` 等可以由已验证下载重新生成的解压输出; -- `.artifacts/editor/macos/` 和官方插件等尚未写入构建身份 stamp 的生成资源; +- `.artifacts/editor/macos/`、Monaco probe/workbench 资源与其中的主题许可证、NOTICE, + 由 `editor-theme-assets` 排除路由拒绝复用。它们与当前源码、上游色表固定提交、Monaco/Bun + 版本绑定,没有可靠构建身份 stamp;各工作树构建时一起生成 JS 并复制许可证,不在任何阶段 + 跨树复制。运行时只读,不改变签名和 Sparkle delta。官方插件等尚未写入构建身份 stamp 的 + 生成资源同样不得复用; - `.build` 中的 SwiftPM 构建状态; - `rust/target/` 中与当前源码、编译器或构建参数绑定的构建输出; - LSP workspace `-data`、运行时数据库、测试报告和其他会被进程修改的状态。 diff --git a/frontend/editor/build.ts b/frontend/editor/build.ts index de0da73755..a4a1a7f7c4 100644 --- a/frontend/editor/build.ts +++ b/frontend/editor/build.ts @@ -51,6 +51,8 @@ export async function buildEditorAssets(options: EditorAssetBuild): Promise [role, value[appearance]])); + // Reuse the same bundled palettes as Windows. These only replace syntax + // colors; workbench surfaces continue to follow the native interface theme. + const scheme = input.syntaxScheme === "github" ? github : input.syntaxScheme === "one" ? one + : input.syntaxScheme === "vscode" ? vscode : input.syntaxScheme === "idea" ? idea : undefined; + const variant = scheme?.themes.find(value => value.id === `${input.syntaxScheme}-${appearance}`); + if (variant) { + const syntax: Record = variant.syntax; + const aliases: Record = { + annotation: "attribute", documentationComment: "comment", field: "property", + functionCall: "function", functionDeclaration: "function", parameter: "variable", typeParameter: "type", + }; + for (const role of syntaxColors.keys()) { + const color = syntax[aliases[role] ?? role]; + if (color) syntaxColors.set(role, color); + } + for (const [role, color] of Object.entries(syntax)) syntaxColors.set(role, color); + } const tokens = new Map(syntaxColors); for (const [token, role] of MONACO_TOKEN_SYNTAX_ROLES) { const color = syntaxColors.get(token) ?? syntaxColors.get(nativeRoles[role] ?? role); if (color) tokens.set(token, color); } + if (input.syntaxScheme === "idea") { + // Java METHOD_CALL inherits the identifier color. Darcula's STATIC_METHOD + // has font attributes but no foreground; declarations use FUNCTION_DECLARATION. + // Keep lexical Java names neutral until the language server classifies them. + const text = syntaxColors.get("text")!; + tokens.set("function.java", text); + for (const prefix of ["method", "method.readonly", "method.deprecated", "method.readonly.deprecated"]) { + tokens.set(prefix, text); + tokens.set(`${prefix}.declaration`, syntaxColors.get("functionDeclaration")!); + tokens.set(`${prefix}.static`, text); + tokens.set(`${prefix}.declaration.static`, text); + } + } const colors: Record = { "editor.foreground": input.colors?.foreground ?? syntaxColors.get("text")!, // Keep the minimap shadow and overview ruler border from forming vertical separators. @@ -70,7 +107,9 @@ export function defineWorkbenchTheme(input: WorkbenchThemeInput, palette: Syntax monaco.editor.defineTheme(name, { base: input.dark ? "vs-dark" : "vs", inherit: true, colors, rules: [...[...tokens].map(([token, foreground]) => ({ token, foreground: foreground.slice(1) })), - ...createMonacoTokenStyleRules(false)], + { token: "comment", fontStyle: input.italicComments ? "italic" : "" }, + { token: "documentationComment", fontStyle: input.italicComments ? "italic" : "" }, + ...createMonacoTokenStyleRules(input.italicComments === true)], }); return name; } diff --git a/frontend/editor/src/workbench.ts b/frontend/editor/src/workbench.ts index 1ed8a86462..4993aa35bb 100644 --- a/frontend/editor/src/workbench.ts +++ b/frontend/editor/src/workbench.ts @@ -112,6 +112,9 @@ export function mountWorkbench(host: WorkbenchHost) { let suspendedViews: { role: string; id: string | undefined; state: monaco.editor.ICodeEditorViewState | null; find: FindSnapshot | undefined; focused: boolean }[] | undefined; let displayOptions: monaco.editor.IStandaloneEditorConstructionOptions = {}; + // Keep a rendered, hit-testable gutter on lines without breakpoints too. + // Monaco Off suppresses lineNumberClassName decorations along with the digits. + const hiddenLineNumbers = () => "\u200b"; const allEditors = () => [editor, ...[...surfaces.values()].map(surface => surface.editor)].filter(Boolean); function applyReadOnly(entry: Entry) { for (const view of allEditors()) { @@ -458,14 +461,22 @@ export function mountWorkbench(host: WorkbenchHost) { const show = value && (line === firstVisibleLine || byLine.get(line - 1)?.commit !== value.commit); const label = value ? `${value.author} · ${value.date}` : ""; const metadata = value ? `${show ? escapeHTML(label) : "​"}` : ""; - return `${metadata}${line}`; - } } : { lineNumbers: "on", lineNumbersMinChars: host.lineNumbersFirst ? 1 : 5 }); + const mode = displayOptions.lineNumbers === hiddenLineNumbers ? "off" : displayOptions.lineNumbers ?? "on"; + const activeLine = view.getPosition()?.lineNumber ?? line; + const number = mode === "off" ? "" : mode === "relative" && line !== activeLine ? Math.abs(line - activeLine) : line; + return `${metadata}${number}`; + } } : { lineNumbers: displayOptions.lineNumbers ?? "on", lineNumbersMinChars: host.lineNumbersFirst ? 1 : 5 }); } const value = current(); for (const action of ["show", "stage", "unstage", "discard"] as const) keys[action].set(!!value && (action === "show" || (!value.entry.readonly && value.marker[action]))); }; - gitContextUpdates.set(view, refresh); + gitContextUpdates.set(view, () => { + // Display updates replace Monaco's line-number option. Reinstall active + // blame without losing the user's relative/hidden line-number preference. + renderedBlame = {}; + refresh(); + }); const run = async (action: string, line?: number) => { const value = current(line); if (!value) return; @@ -713,7 +724,8 @@ export function mountWorkbench(host: WorkbenchHost) { const entry = [...entries.values()].find(entry => entry.model === view.getModel()); view.getDomNode()?.toggleAttribute("data-lithe-breakpoints-editable", !!entry && entry.canToggleBreakpoints === true && !entry.readonly && !entry.frozen && !entry.closingHolds && !failed && - !view.getOption(monaco.editor.EditorOption.readOnly) && typeof view.getRawOptions().lineNumbers !== "function"); + !view.getOption(monaco.editor.EditorOption.readOnly) && + (typeof view.getRawOptions().lineNumbers !== "function" || view.getRawOptions().lineNumbers === hiddenLineNumbers)); } const value = navigation(); for (const direction of ["up", "down"] as const) navigationKeys[direction].set(!!value?.markers.some(marker => marker.direction === direction)); @@ -793,7 +805,8 @@ export function mountWorkbench(host: WorkbenchHost) { if (host.gitBlameMenu && host.lineNumbersFirst && (event.event.rightButton || isMacintosh && event.event.ctrlKey) && event.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_NUMBERS) return; if (event.target.element?.closest(".lithe-java-navigation, .lithe-java-run, .lithe-blame") || - host.lineNumbersFirst && typeof view.getRawOptions().lineNumbers === "function") return; + host.lineNumbersFirst && typeof view.getRawOptions().lineNumbers === "function" && + view.getRawOptions().lineNumbers !== hiddenLineNumbers) return; if ((event.target.type !== monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN && !(host.lineNumbersFirst && event.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_NUMBERS)) || !event.target.position || (!event.event.leftButton && !event.event.rightButton)) return; @@ -1068,6 +1081,7 @@ export function mountWorkbench(host: WorkbenchHost) { const name = theme ? defineWorkbenchTheme(theme, host.palette) : payload.dark ? "lithe-dark" : "lithe-light"; Object.assign(displayOptions, { + ...payload.appearance, // Standalone updateOptions and newly created split editors both apply // this construction theme globally, so it must track the host theme. theme: name, @@ -1076,9 +1090,11 @@ export function mountWorkbench(host: WorkbenchHost) { wordWrap: payload.wrap ? "on" : "off", minimap: { enabled: payload.minimap !== false }, }); + if (host.lineNumbersFirst && displayOptions.lineNumbers === "off") displayOptions.lineNumbers = hiddenLineNumbers; for (const view of allEditors()) { view.updateOptions(displayOptions); navigationContexts.get(view)?.(); + gitContextUpdates.get(view)?.(); } monaco.editor.setTheme(name); }, diff --git a/windows/tauri/src/extensions/themes/builtin/github.json b/frontend/editor/themes/github.json similarity index 100% rename from windows/tauri/src/extensions/themes/builtin/github.json rename to frontend/editor/themes/github.json diff --git a/frontend/editor/themes/idea-LICENSE.txt b/frontend/editor/themes/idea-LICENSE.txt new file mode 100644 index 0000000000..60fd1a0a36 --- /dev/null +++ b/frontend/editor/themes/idea-LICENSE.txt @@ -0,0 +1,91 @@ +JETBRAINS OPEN-SOURCE BUILD TERMS + +Version 1.3, effective as of June 15, 2026 + +IMPORTANT! READ CAREFULLY: + +THESE TERMS APPLY TO THE OPEN-SOURCE BUILDS OF THE JETBRAINS INTEGRATED DEVELOPMENT ENVIRONMENT TOOLS +CALLED ‘INTELLIJ IDEA’ AND ‘PYCHARM’ (SUCH TOOLS, “OPEN-SOURCE BUILD” PRODUCTS) WHICH CONSIST OF OPEN SOURCE SOFTWARE +SUBJECT TO THE APACHE 2.0 LICENSE (AVAILABLE HERE: https://www.apache.org/licenses/LICENSE-2.0). + +"JetBrains" or "we" means JetBrains s.r.o., with its principal place of business +at Na Hrebenech II 1718/8, Prague, 14000, Czech Republic, registered in the Commercial Register +maintained by the Municipal Court of Prague, Section C, File 86211, ID No.: 265 02 275. + +"You" means any Organization or natural person using an Open-Source Build product in accordance with these terms, +where “Organization” includes any corporation, company, partnership, association, or other entity that controls, +is controlled by, or is under common control with you. For the purposes of this definition, “control” means +(i) the power, directly or indirectly, to direct or manage such entity, whether by contract or otherwise, or +(ii) ownership of fifty percent (50%) or more of the outstanding shares or beneficial ownership of such entity. + +Personal Data — In connection with your use of Open-Source Build products, we and our associated companies will +process your personal data, including but not limited to your contact and identification details, and data about +your usage of our software and services, in order to (i) provide you with software, services, or information; +(ii) protect us from piracy and unlawful use of our software or services; (iii) improve our offerings based on usage; +(iv) create and maintain our internal records and to protect our rights and interests and those of other users; +(v) promote and market our software and services; and +(vi) to fulfil legal duties stipulated by accounting, taxation, and other laws. You may object to the processing +of your personal data for the purposes of (i) through (v) at any time. More detailed information can be found in +our Privacy Notice, available here: https://www.jetbrains.com/legal/docs/privacy/privacy.html. + +Personal Data Collected — For the above purposes, we may collect, among other things, your IP address, +JetBrains Account username, JetBrains Account password, first name, last name, and email address. + +Anonymous Data — On installation and execution, the Open-Source Build product may send us certain information, +which will not contain any personal data, including product version, product edition, and information about +the operating system where the Open-Source Build product is installed. A unique ID, which +does not contain any personal data, is also used to distinguish instances. The Open-Source Build product +can check for available updates, as well as available updates for plugins or components. +In addition, you can opt in to further anonymous data processing. If you do so, the Open-Source Build product +may electronically send anonymous information to us related to your usage of the product features. +This further information may include, but is not limited to, frameworks, file templates being used in the IDEs, +actions invoked, and other interactions with product features. This information will not contain your source code, +your personal data, information about your JetBrains Account, or subscription information. + +Accidentally Sent Information — We are not responsible for any processing of personal data you accidentally send to us. + +Feedback — You have no obligation to provide us with ideas, guidance, suggestions, proposals, or bug reports (“Feedback”). +However, if you submit Feedback to us, then you grant us a non-exclusive, worldwide, royalty-free license that is +sub-licensable and transferable, to make, use, sell, have made, offer to sell, import, reproduce, publicly display, +distribute, modify, or publicly perform the Feedback in any manner without any obligation, royalty, or restriction +based on intellectual property rights or otherwise. + +Third Party Software — Open-Source Build includes code and libraries licensed to us by third parties, including +open source software (“Third-Party Software”). A list of Third-Party Software included in each Open-Source Build product +is available in the product documentation. All Third-Party Software is provided to you under the respective terms +stipulated in the Open-Source Build product documentation. + +DISCLAIMER OF DAMAGES AND EXCLUSION OF LIABILITY — ALL OPEN-SOURCE BUILD PRODUCTS (INCLUDING THIRD-PARTY SOFTWARE) +ARE PROVIDED TO YOU ON AN “AS IS” AND “AS AVAILABLE” BASIS. USE OF THE OPEN-SOURCE BUILD PRODUCTS IS AT YOUR OWN RISK +AND WITHOUT WARRANTIES, EXPRESS OR IMPLIED (TO THE MAXIMUM EXTENT PERMITTED BY LAW). FURTHER DETAILS MAY BE FOUND +IN THE APPLICABLE OPEN-SOURCE BUILD TERMS OR THIRD-PARTY SOFTWARE TERMS. IN NO EVENT WILL WE BE LIABLE TO YOU +OR ANYONE ELSE FOR ANY LOSS OF USE, DATA, GOODWILL, OR PROFITS, WHETHER OR NOT FORESEEABLE, OR ANY SPECIAL, +INCIDENTAL, INDIRECT, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES OR LOSSES WHATSOEVER INCURRED IN CONNECTION +WITH YOUR USAGE OF ANY OPEN-SOURCE BUILD PRODUCT. THIS LIMITATION WILL APPLY EVEN IF THE JETBRAINS PARTIES +HAVE BEEN ADVISED OF THE POSSIBILITY OF LIABILITY. + +Export Compliance — You must comply with all applicable laws and regulations with regard to economic sanctions, +export controls, import regulations, restrictive measures, and trade embargoes, including those of the European Union +and the United States of America. + +Reservation of Rights — We reserve the right at any time to cease provision of or alter features, specifications, +capabilities, functions, terms of use (including this document), release dates, general availability, +or other characteristics of Open-Source Build products. + +Governing Law — These terms are governed by the laws of the Czech Republic, without reference to conflict of laws principles, +and specifically excluding the United Nations Convention on Contracts for the International Sale of Goods. +The parties to this Agreement undertake to use the best commercial efforts to amicably settle any disputes +arising hereunder (“Dispute”). Should the parties to this Agreement fail to settle a Dispute amicably, the Dispute +will be excluded from the jurisdiction of general courts and all such Disputes will be finally decided +by the Arbitration Court attached to the Czech Chamber of Commerce and the Agricultural Chamber of the Czech Republic +by three arbitrators in accordance with the Rules of that Arbitration Court, and the language of the proceedings +will be English; provided that if you are a consumer, you and JetBrains agree that any Dispute-related litigation +may only be brought in, and shall be subject to the jurisdiction of, any competent court of the Czech Republic, +unless provided otherwise by applicable consumer law. +Consumer Disputes can also be settled out of court through the Czech Trade Inspection Authority (www.coi.cz) +or the European Commission online platform for dispute resolution (ec.europa.eu/consumers/odr). + +Opportunity to review — You declare that you have had sufficient opportunity to review these terms, +understand the content of all of its clauses, negotiate its terms, and seek independent professional legal advice +in that respect before entering into it. Consequently, any statutory “form contract” (“adhesion contract”) regulations +shall not be applicable to these terms. diff --git a/frontend/editor/themes/idea-NOTICE.txt b/frontend/editor/themes/idea-NOTICE.txt new file mode 100644 index 0000000000..7297ca08d4 --- /dev/null +++ b/frontend/editor/themes/idea-NOTICE.txt @@ -0,0 +1,3 @@ +This software includes code from IntelliJ IDEA +Copyright (C) JetBrains s.r.o. +https://www.jetbrains.com/idea/ diff --git a/frontend/editor/themes/idea.json b/frontend/editor/themes/idea.json new file mode 100644 index 0000000000..2a35f05d0a --- /dev/null +++ b/frontend/editor/themes/idea.json @@ -0,0 +1,65 @@ +{ + "name": "IntelliJ IDEA", + "themes": [ + { + "id": "idea-dark", + "syntax": { + "text": "#bcbec4", + "keyword": "#cf8e6d", + "string": "#6aab73", + "string.escape": "#cf8e6d", + "number": "#2aacb8", + "comment": "#7a7e85", + "documentationComment": "#5f826b", + "variable": "#bcbec4", + "function": "#56a8f5", + "functionCall": "#bcbec4", + "function.call": "#bcbec4", + "function.method.call": "#bcbec4", + "constant": "#c77dbb", + "property": "#c77dbb", + "type": "#bcbec4", + "operator": "#bcbec4", + "punctuation": "#bcbec4", + "boolean": "#cf8e6d", + "null": "#cf8e6d", + "regex": "#6aab73", + "tag": "#d5b778", + "attribute": "#b3ae60", + "comment.documentation": "#5f826b" + } + }, + { + "id": "idea-light", + "syntax": { + "text": "#080808", + "keyword": "#0033b3", + "string": "#067d17", + "string.escape": "#0037a6", + "number": "#1750eb", + "comment": "#8c8c8c", + "documentationComment": "#8c8c8c", + "variable": "#080808", + "function": "#00627a", + "functionCall": "#080808", + "function.call": "#080808", + "function.method.call": "#080808", + "constant": "#871094", + "property": "#871094", + "type": "#080808", + "operator": "#080808", + "punctuation": "#080808", + "boolean": "#0033b3", + "null": "#0033b3", + "regex": "#067d17", + "tag": "#080808", + "attribute": "#9e880d", + "comment.documentation": "#8c8c8c" + } + } + ], + "sources": [ + "https://github.com/JetBrains/intellij-community/blob/74dbd42b7a8787b0707437cb857c68e78199f611/platform/platform-resources/src/themes/islands/IslandSchemeDark.xml", + "https://github.com/JetBrains/intellij-community/blob/74dbd42b7a8787b0707437cb857c68e78199f611/platform/platform-resources/src/themes/expUI/expUI_lightScheme.xml" + ] +} diff --git a/windows/tauri/src/extensions/themes/builtin/one.json b/frontend/editor/themes/one.json similarity index 100% rename from windows/tauri/src/extensions/themes/builtin/one.json rename to frontend/editor/themes/one.json diff --git a/frontend/editor/themes/vscode-LICENSE.txt b/frontend/editor/themes/vscode-LICENSE.txt new file mode 100644 index 0000000000..0ac28ee234 --- /dev/null +++ b/frontend/editor/themes/vscode-LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015 - present Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/frontend/editor/themes/vscode.json b/frontend/editor/themes/vscode.json new file mode 100644 index 0000000000..43021bbf4a --- /dev/null +++ b/frontend/editor/themes/vscode.json @@ -0,0 +1,67 @@ +{ + "name": "VS Code", + "themes": [ + { + "id": "vscode-dark", + "syntax": { + "text": "#BBBEBF", + "keyword": "#ff7b72", + "keyword.control": "#C586C0", + "string": "#a5d6ff", + "string.escape": "#d7ba7d", + "number": "#b5cea8", + "comment": "#8b949e", + "documentationComment": "#8b949e", + "variable": "#c9d1d9", + "parameter": "#c9d1d9", + "function": "#d2a8ff", + "constant": "#79c0ff", + "property": "#79c0ff", + "type": "#4EC9B0", + "operator": "#d4d4d4", + "punctuation": "#BBBEBF", + "boolean": "#569cd6", + "null": "#569cd6", + "regex": "#a5d6ff", + "tag": "#7ee787", + "attribute": "#9cdcfe" + } + }, + { + "id": "vscode-light", + "syntax": { + "text": "#202020", + "keyword": "#cf222e", + "keyword.control": "#AF00DB", + "string": "#0a3069", + "string.escape": "#EE0000", + "number": "#098658", + "comment": "#6e7781", + "documentationComment": "#6e7781", + "variable": "#1f2328", + "parameter": "#1f2328", + "function": "#8250df", + "constant": "#0550ae", + "property": "#0550ae", + "type": "#267f99", + "operator": "#000000", + "punctuation": "#202020", + "boolean": "#0000ff", + "null": "#0000ff", + "regex": "#0a3069", + "tag": "#116329", + "attribute": "#e50000" + } + } + ], + "sources": [ + "https://github.com/microsoft/vscode/blob/b6bfa95ebee8b183d65dee4997c0527eadfe5384/extensions/theme-defaults/themes/2026-dark.json", + "https://github.com/microsoft/vscode/blob/b6bfa95ebee8b183d65dee4997c0527eadfe5384/extensions/theme-defaults/themes/2026-light.json", + "https://github.com/microsoft/vscode/blob/b6bfa95ebee8b183d65dee4997c0527eadfe5384/extensions/theme-defaults/themes/dark_modern.json", + "https://github.com/microsoft/vscode/blob/b6bfa95ebee8b183d65dee4997c0527eadfe5384/extensions/theme-defaults/themes/light_modern.json", + "https://github.com/microsoft/vscode/blob/b6bfa95ebee8b183d65dee4997c0527eadfe5384/extensions/theme-defaults/themes/dark_plus.json", + "https://github.com/microsoft/vscode/blob/b6bfa95ebee8b183d65dee4997c0527eadfe5384/extensions/theme-defaults/themes/light_plus.json", + "https://github.com/microsoft/vscode/blob/b6bfa95ebee8b183d65dee4997c0527eadfe5384/extensions/theme-defaults/themes/dark_vs.json", + "https://github.com/microsoft/vscode/blob/b6bfa95ebee8b183d65dee4997c0527eadfe5384/extensions/theme-defaults/themes/light_vs.json" + ] +} diff --git a/macos/Experiments/Monaco/workbench.integration.ts b/macos/Experiments/Monaco/workbench.integration.ts index 42b23b1ae6..f6b07cb3aa 100644 --- a/macos/Experiments/Monaco/workbench.integration.ts +++ b/macos/Experiments/Monaco/workbench.integration.ts @@ -1,3 +1,6 @@ +import { encodeMonacoSemanticTokens, MONACO_SEMANTIC_TOKEN_MODIFIERS } from "@lithe/editor/semantic-tokens"; +import { deepClone } from "monaco-editor/esm/vs/base/common/objects.js"; +import { EditorOptions } from "monaco-editor/esm/vs/editor/common/config/editorOptions.js"; import { LightBulbWidget } from "monaco-editor/esm/vs/editor/contrib/codeAction/browser/lightBulbWidget.js"; import { runEditorCommand } from "@lithe/editor/editor-commands"; import { ICommandService } from "monaco-editor/esm/vs/platform/commands/common/commands.js"; @@ -11,6 +14,8 @@ import { StandaloneServices } from "monaco-editor/esm/vs/editor/standalone/brows import { ILanguageFeaturesService } from "monaco-editor/esm/vs/editor/common/services/languageFeatures.js"; import { CancellationTokenSource, CancellationToken } from "monaco-editor/esm/vs/base/common/cancellation.js"; import { ready } from "./workbench"; +import { IStandaloneThemeService } from "monaco-editor/esm/vs/editor/standalone/common/standaloneTheme.js"; +import { TokenMetadata } from "monaco-editor/esm/vs/editor/common/encodedTokenAttributes.js"; import { acquireEditorModelSource, sourcePositionAt } from "@lithe/editor/model-source"; import { editor as monacoEditor, languages, Range, Selection, Uri } from "monaco-editor/esm/vs/editor/editor.api.js"; import { mouseInputCases } from "./mouse-input.integration"; @@ -545,6 +550,64 @@ async function verify() { container.remove(); } }); + await check("hidden line numbers retain breakpoint icons and clicks in existing and new splits", async () => { + const id = "hidden-number-breakpoints"; + const configure = (lineNumbers: string) => window.lithe.configure({ dark: true, + fontFamily: "monospace", fontSize: 13, wrap: false, minimap: true, appearance: { lineNumbers } }); + await window.lithe.activate({ id, text: "class Demo {\n int a = 1;\n int b = 2;\n}", + revision: 0, language: "java", readonly: false }); + try { + await window.lithe.debugState(id, { muted: false, canToggleBreakpoints: true, revision: 0, + breakpoints: [{ line: 2, enabled: true, verified: true, logpoint: false }] }); + configure("off"); + await window.lithe.showSecondary({ id, readonly: false }); + const secondary = monacoEditor.getEditors().find(view => view !== editor)!; + for (const view of [editor, secondary]) { + view.render(true); + const root = view.getDomNode()!; + assert(root.hasAttribute("data-lithe-breakpoints-editable"), "hidden numbers disabled breakpoint hover"); + assert(!!root.querySelector(".line-numbers.lithe-breakpoint"), "hidden numbers removed the breakpoint icon"); + const numbers = [...root.querySelectorAll(".line-numbers")]; + assert(numbers.length > 0 && numbers.every(node => !/[0-9]/.test(node.textContent ?? "")), + "hidden numbers still display digits"); + for (const line of [2, 3]) { + view.render(true); + const node = [...root.querySelectorAll(".line-numbers")].find(node => { + const rect = node.getBoundingClientRect(); + return view.getTargetAtClientPoint(rect.right - 4, rect.top + rect.height / 2)?.position?.lineNumber === line; + }); + assert(!!node, `line ${line} has no clickable breakpoint region`); + const rect = node!.getBoundingClientRect(); + const event = { bubbles: true, button: 0, buttons: 1, + clientX: rect.right - 4, clientY: rect.top + rect.height / 2 }; + assert(view.getTargetAtClientPoint(event.clientX, event.clientY)?.type === monacoEditor.MouseTargetType.GUTTER_LINE_NUMBERS, + "hidden gutter no longer receives breakpoint clicks"); + const before = (await send({ type: "debugRequests" })).requests.length; + let listener: { dispose(): void } | undefined; + try { + const observed = new Promise(resolve => { listener = view.onMouseDown(() => resolve()); }); + node!.dispatchEvent(new MouseEvent("mousedown", event)); + document.dispatchEvent(new MouseEvent("mouseup", { ...event, buttons: 0 })); + await withDeadline(observed, "hidden gutter click did not reach Monaco"); + const requests = (await send({ type: "debugRequests" })).requests; + assert(requests.length === before + 1 && requests.at(-1)?.type === "toggleBreakpoint" + && requests.at(-1)?.id === id && requests.at(-1)?.line === line, + "hidden gutter did not toggle the existing or new breakpoint"); + } finally { listener?.dispose(); } + } + } + configure("relative"); + for (const view of [editor, secondary]) { + view.render(true); + assert(view.getRawOptions().lineNumbers === "relative", "restoring numbers lost relative mode"); + assert(!!view.getDomNode()!.querySelector(".line-numbers.lithe-breakpoint"), "restoring numbers removed the breakpoint"); + } + } finally { + window.lithe.hideSecondary(); + configure("on"); + await window.lithe.retain([]); + } + }); await check("native gutter places line numbers before Run icons and preserves hit targets", async () => { const id = "gutter-order"; const folding = languages.registerFoldingRangeProvider("java", { @@ -932,6 +995,121 @@ async function verify() { await window.lithe.retain([]); } }); + await check("global appearance updates live and future splits without replacing documents or undo", async () => { + const text = "const sample = 1;\n // comment\n"; + await send({ type: "open", text }); + await window.lithe.activate({ id: "appearance", text, revision: 0, language: "javascript", readonly: false }); + const model = editor.getModel()!; + const appearance = { + lineHeight: 1.8, lineNumbers: "relative", renderWhitespace: "all", fontLigatures: true, + cursorStyle: "block", cursorBlinking: "solid", guides: { indentation: false, highlightActiveIndentation: false }, + bracketPairColorization: { enabled: false }, stickyScroll: { enabled: false }, + smoothScrolling: true, scrollBeyondLastLine: true, renderLineHighlight: "none", + }; + // Monaco mutates raw options in place. Snapshot only touched display fields, + // including defaults, so later input tests cannot inherit this test's layout. + const previous = Object.fromEntries([...Object.keys(appearance), "fontFamily", "fontSize"].map(key => + [key, deepClone(editor.getRawOptions()[key] ?? Object.values(EditorOptions).find(option => option.name === key)!.defaultValue)])); + try { + editor.pushUndoStop(); + editor.executeEdits("appearance-test", [{ range: new Range(1, 16, 1, 17), text: "2" }]); + editor.pushUndoStop(); + const position = editor.getPosition(); + const configure = (dark: boolean, syntaxScheme: string, italicComments: boolean) => window.lithe.configure({ + fontFamily: "monospace", fontSize: 18, wrap: true, minimap: false, + appearance: { ...appearance, bracketPairColorization: { enabled: syntaxScheme !== "idea" } }, + theme: { id: `appearance-${syntaxScheme}-${dark}-${italicComments}`, dark, syntaxScheme, italicComments }, + }); + configure(true, "github", true); + await window.lithe.showSecondary({ id: "appearance", readonly: false }); + const secondary = monacoEditor.getEditors().find(view => view !== editor)!; + for (const view of [editor, secondary]) { + const option = monacoEditor.EditorOption; + assert(view.getOption(option.fontSize) === 18 && view.getOption(option.lineHeight) === 32, + "font size or multiplier did not reach a live/new view"); + assert(view.getRawOptions().lineNumbers === "relative" && view.getOption(option.renderWhitespace) === "all", + "line numbers or whitespace ignored the preference"); + assert(!view.getOption(option.guides).indentation && view.getOption(option.bracketPairColorization).enabled, + "guides or bracket colors ignored the preference"); + assert(view.getRawOptions().cursorStyle === "block" && view.getRawOptions().cursorBlinking === "solid", + "cursor preferences were not applied"); + assert(view.getRawOptions().fontLigatures === true && !view.getOption(option.stickyScroll).enabled, + "ligatures or sticky scroll were not applied"); + assert(view.getOption(option.smoothScrolling) && view.getOption(option.scrollBeyondLastLine) + && view.getOption(option.renderLineHighlight) === "none", "scroll/current-line preferences were not applied"); + assert(!view.getOption(option.readOnly), "appearance made an editable document read-only"); + } + const themeService = StandaloneServices.get(IStandaloneThemeService); + const tokenColor = (token: string) => { + const theme = themeService.getColorTheme().tokenTheme; + const color = theme.getColorMap()[TokenMetadata.getForeground(theme.match(0, token))].rgba; + return [color.r, color.g, color.b].map(value => value.toString(16).padStart(2, "0")).join("").toUpperCase(); + }; + assert(tokenColor("keyword") === "FF7B72" && tokenColor("functionCall") === "D2A8FF", + "GitHub dark did not map lexical/native token roles"); + assert(TokenMetadata.getFontStyle(themeService.getColorTheme().tokenTheme.match(0, "comment")) & 1, + "italic comments were not applied"); + configure(false, "github", false); + assert(tokenColor("keyword") === "CF222E" && tokenColor("typeParameter") === "8250DF", + "GitHub light did not update semantic aliases"); + assert(!(TokenMetadata.getFontStyle(themeService.getColorTheme().tokenTheme.match(0, "comment")) & 1), + "turning italic comments off retained italic style"); + configure(true, "one", false); + assert(tokenColor("keyword") === "C678DD", "One dark was not selected"); + configure(false, "one", false); + assert(tokenColor("keyword") === "A626A4", "One light was not selected"); + for (const [scheme, dark, keyword, method, field] of [ + ["vscode", true, "FF7B72", "D2A8FF", "79C0FF"], + ["vscode", false, "CF222E", "8250DF", "0550AE"], + ["idea", true, "CF8E6D", "56A8F5", "C77DBB"], + ["idea", false, "0033B3", "00627A", "871094"], + ] as const) { + configure(dark, scheme, false); + assert(tokenColor("keyword") === keyword && tokenColor("functionDeclaration") === method + && tokenColor("field") === field, `${scheme} ${dark} lost lexical/semantic palette colors`); + assert(editor.getOption(monacoEditor.EditorOption.bracketPairColorization).enabled === (scheme !== "idea"), + "bracket coloring did not follow the selected scheme"); + if (scheme === "idea") { + const foreground = dark ? "BCBEC4" : "080808"; + for (const token of ["functionCall", "function.java", "method", "method.static", "method.declaration.static", "method.deprecated.declaration.static"]) + assert(tokenColor(token) === foreground, `${token} incorrectly uses the declaration color`); + assert(tokenColor("method.declaration") === method, "instance declaration lost its distinct color"); + const java = monacoEditor.createModel("class Demo { static void main(String[] args) { new Demo().getOrder(); } }", "java"); + try { + (java as any).tokenization.forceTokenization(1); + const line = (java as any).tokenization.getLineTokens(1); + const colors = themeService.getColorTheme().tokenTheme.getColorMap(); + for (const name of ["main", "getOrder"]) { + const index = line.findTokenIndexAtOffset(java.getValue().indexOf(name)); + const color = colors[line.getForeground(index)].rgba; + const hex = [color.r, color.g, color.b].map(v => v.toString(16).padStart(2, "0")).join("").toUpperCase(); + assert(hex === foreground, `${name} lexical color differs from IDEA`); + } + const data = encodeMonacoSemanticTokens({ + tokenTypes: ["method"], tokenModifiers: ["static", "declaration"], + tokens: [{ line: 0, startChar: 25, length: 4, tokenType: 0, tokenModifiers: 3 }], + }, java); + const bits = (1 << MONACO_SEMANTIC_TOKEN_MODIFIERS.indexOf("static")) | + (1 << MONACO_SEMANTIC_TOKEN_MODIFIERS.indexOf("declaration")); + assert(data[4] === bits, "semantic adapter dropped method classification"); + } finally { java.dispose(); } + } + } + + assert(editor.getModel() === model && secondary.getModel() === model, "appearance replaced a model"); + assert(editor.getPosition()?.equals(position), "appearance moved the caret"); + await model.undo(); + assert(model.getValue() === text, "appearance discarded undo history"); + const snapshot = await window.lithe.freeze("appearance"); + await send({ type: "save", revision: snapshot.revision, expected: text }); + window.lithe.unlock("appearance"); + } finally { + window.lithe.hideSecondary(); + window.lithe.configure({ dark: false, fontFamily: previous.fontFamily, fontSize: previous.fontSize, + wrap: false, minimap: true, appearance: previous }); + await window.lithe.retain([]); + } + }); await check("host themes replace the Monaco surface and reveal the native wallpaper", async () => { await window.lithe.activate({ id: "theme-surface", text: "theme", revision: 0, language: "plaintext", readonly: true, @@ -1478,15 +1656,29 @@ async function verify() { await window.lithe.attachSurface("blame-split", container, { id: "blame" }); const split = monacoEditor.getEditors().find(value => value !== editor)!; assert(typeof split.getRawOptions().lineNumbers === "function", "new split did not inherit blame state"); + const configureNumbers = (lineNumbers: string) => window.lithe.configure({ dark: false, + fontFamily: "monospace", fontSize: 15, wrap: false, minimap: true, appearance: { lineNumbers } }); + configureNumbers("off"); + assert(typeof editor.getRawOptions().lineNumbers === "function", "font/display update removed blame metadata"); + repeated.innerHTML = (editor.getRawOptions().lineNumbers as (line: number) => string)(1); + assert(repeated.querySelector(".lithe-blame") && repeated.querySelector(".lithe-blame-line")?.textContent === "", + "hidden line numbers also hid blame or left numbers visible"); + configureNumbers("relative"); + editor.setPosition({ lineNumber: 3, column: 1 }); + repeated.innerHTML = (editor.getRawOptions().lineNumbers as (line: number) => string)(1); + assert(repeated.querySelector(".lithe-blame-line")?.textContent === "2", "blame ignored relative line numbers"); await window.lithe.activate({ id: "blame-other", text: "other", revision: 0, language: "plaintext", readonly: false }); - assert(editor.getRawOptions().lineNumbers === "on", "blame metadata leaked into another tab"); + assert(editor.getRawOptions().lineNumbers === "relative", "tab switch lost the line-number preference"); assert(typeof split.getRawOptions().lineNumbers === "function", "switching the primary cleared the split's blame"); await window.lithe.gitState("blame", { revision: 0, markers: [], blameVisible: false, blame: [] }); - assert(split.getRawOptions().lineNumbers === "on" && split.getRawOptions().lineNumbersMinChars === 1, "closing blame left a wide gutter"); + assert(split.getRawOptions().lineNumbers === "relative" && split.getRawOptions().lineNumbersMinChars === 1, + "closing blame lost the preference or left a wide gutter"); await split.getAction("lithe.git.blameCommit")!.run(); assert((await send({ type: "blameCommits" })).commits.length === 1, "hidden blame remained actionable"); } finally { window.lithe.detachSurface("blame-split"); container.remove(); + window.lithe.configure({ dark: false, fontFamily: "monospace", fontSize: 13, wrap: false, + minimap: true, appearance: { lineNumbers: "on" } }); await window.lithe.retain([]); } }); diff --git a/macos/Resources/en.lproj/Localizable.strings b/macos/Resources/en.lproj/Localizable.strings index 59ba30dcd7..a2b3c94f61 100644 --- a/macos/Resources/en.lproj/Localizable.strings +++ b/macos/Resources/en.lproj/Localizable.strings @@ -200,6 +200,7 @@ "Abort Rebase" = "Abort Rebase"; "Abort this rebase and restore its original branch?" = "Abort this rebase and restore its original branch?"; "Abort…" = "Abort…"; +"Absolute" = "Absolute"; "Accept" = "Accept"; "Accept edits" = "Accept edits"; "Accepts all permissions" = "Skip routine approval prompts; environment and managed policies still apply."; @@ -345,6 +346,7 @@ "Application-wide editor and terminal settings remain available in the other categories." = "Application-wide editor and terminal settings remain available in the other categories."; "Applications" = "Applications"; "Applies immediately" = "Applies immediately"; +"Applies immediately to source editors in all projects." = "Applies immediately to source editors in all projects."; "Applies the next time the language server starts." = "Applies the next time the language server starts."; "Apply" = "Apply"; "Apply %@ to the current branch." = "Apply %@ to the current branch."; @@ -449,6 +451,10 @@ "Big Endian" = "Big Endian"; "Binary" = "Binary"; "Binary files cannot be compared." = "Binary files cannot be compared."; +"Blink" = "Blink"; +"Block" = "Block"; +"Boundary" = "Boundary"; +"Bracket colors follow the selected editor color scheme." = "Bracket colors follow the selected editor color scheme."; "Branch" = "Branch"; "Branch '%@' tracks '%@', which has a different name. Choose which branch to update." = "Branch '%@' tracks '%@', which has a different name. Choose which branch to update."; "Branch context" = "Branch context"; @@ -768,6 +774,7 @@ "Collapsed" = "Collapsed"; "Collected logs cleared." = "Collected logs cleared."; "Color theme" = "Color theme"; +"Colorize bracket pairs" = "Colorize bracket pairs"; "Combine the messages when the rebase runs, including changes made at earlier Edit stops" = "Combine the messages when the rebase runs, including changes made at earlier Edit stops"; "Combined commit message" = "Combined commit message"; "Combines the pull request into one commit on the base branch." = "Combines the pull request into one commit on the base branch."; @@ -1058,6 +1065,9 @@ "Current version: %@ · Verified target: %@" = "Current version: %@ · Verified target: %@"; "Current: %@" = "Current: %@"; "Currently effective" = "Currently effective"; +"Cursor" = "Cursor"; +"Cursor blinking" = "Cursor blinking"; +"Cursor style" = "Cursor style"; "Custom" = "Custom"; "Custom API provider" = "Custom API provider"; "Custom Agent" = "Custom Agent"; @@ -1184,6 +1194,7 @@ "Disable breakpoint" = "Disable breakpoint"; "Disable soft wraps" = "Disable soft wraps"; "Disabled" = "Disabled"; +"Disabled by color scheme" = "Disabled by color scheme"; "Disabled in Agent" = "Disabled in Agent"; "Disabled in this workspace" = "Disabled in this workspace"; "Disassembly" = "Disassembly"; @@ -1292,6 +1303,9 @@ "Edited %lld files" = "Edited %lld files"; "Editor" = "Editor"; "Editor and language services" = "Editor and language services"; +"Editor color scheme" = "Editor color scheme"; +"Editor colors follow light and dark appearance independently of the interface theme." = "Editor colors follow light and dark appearance independently of the interface theme."; +"Editor display" = "Editor display"; "Editor resources are missing. Rebuild or reinstall Lithe." = "Editor resources are missing. Rebuild or reinstall Lithe."; "Editor tabs" = "Editor tabs"; "Edits" = "Edits"; @@ -1324,6 +1338,7 @@ "Enable for this project" = "Enable for this project"; "Enable skill" = "Enable skill"; "Enabled" = "Enabled"; +"Enabled by color scheme" = "Enabled by color scheme"; "Enabled in Agent" = "Enabled in Agent"; "Enabled plugins can also use this project grant." = "Enabled plugins can also use this project grant."; "End All" = "End All"; @@ -1525,6 +1540,7 @@ "Follow the instructions you define below" = "Follow the instructions you define below"; "Follows each remote's tag setting" = "Follows each remote's tag setting"; "Font" = "Font"; +"Font ligatures" = "Font ligatures"; "Font size" = "Font size"; "For example, notify" = "For example, notify"; "For example, notify me when a turn ends" = "For example, notify me when a turn ends"; @@ -1681,6 +1697,7 @@ "Hide visualization" = "Hide visualization"; "Hide worktree repositories" = "Hide worktree repositories"; "High" = "High"; +"Highlight current line" = "Highlight current line"; "Highlight words" = "Highlight words"; "Hint" = "Hint"; "History" = "History"; @@ -1801,6 +1818,7 @@ "Instructions" = "Instructions"; "Instructions saved." = "Instructions saved."; "Integrate incoming changes into current branch (M)" = "Integrate incoming changes into current branch (M)"; +"IntelliJ IDEA" = "IntelliJ IDEA"; "Interactive Rebase" = "Interactive Rebase"; "Interactive Rebase Aborted" = "Interactive Rebase Aborted"; "Interactive Rebase Completed" = "Interactive Rebase Completed"; @@ -1827,6 +1845,7 @@ "Invalid shared instruction request." = "Invalid shared instruction request."; "Invert" = "Invert"; "It can read files, run commands and edit code after asking for permission." = "It can read files, run commands and edit code after asking for permission."; +"Italic comments" = "Italic comments"; "Item %lld of %lld" = "Item %lld of %lld"; "Its details and actions will appear here." = "Its details and actions will appear here."; "Its session was closed or could not be restored." = "Its session was closed or could not be restored."; @@ -1934,6 +1953,8 @@ "Line" = "Line"; "Line Breakpoints" = "Line Breakpoints"; "Line breakpoint actions" = "Line breakpoint actions"; +"Line numbers" = "Line numbers"; +"Line spacing" = "Line spacing"; "Linear" = "Linear"; "Lines kept for scrolling back in new terminal sessions." = "Lines kept for scrolling back in new terminal sessions."; "Linked this conversation to a pull request" = "Linked this conversation to a pull request"; @@ -2933,6 +2954,7 @@ "Registry website" = "Registry website"; "Regular Expression" = "Regular Expression"; "Reinstall" = "Reinstall"; +"Relative" = "Relative"; "Release Date" = "Release Date"; "Release note" = "Release note"; "Release notes" = "Release notes"; @@ -3008,6 +3030,7 @@ "Rename conversation" = "Rename conversation"; "Renamed" = "Renamed"; "Rename…" = "Rename…"; +"Render whitespace" = "Render whitespace"; "Reopen Pull Request" = "Reopen Pull Request"; "Reopen pull request" = "Reopen pull request"; "Reopen pull request?" = "Reopen pull request?"; @@ -3268,6 +3291,7 @@ "Saving writes to" = "Saving writes to"; "Scala Support" = "Scala Support"; "Scope" = "Scope"; +"Scroll beyond last line" = "Scroll beyond last line"; "Scroll to new Git output" = "Scroll to new Git output"; "Scrollback" = "Scrollback"; "Search" = "Search"; @@ -3352,6 +3376,7 @@ "Selected directory" = "Selected directory"; "Selected from disk. The publisher and repository have not been verified." = "Selected from disk. The publisher and repository have not been verified."; "Selected runtime" = "Selected runtime"; +"Selection" = "Selection"; "Self-update is only available when Lithe is running from a packaged Lithe.app." = "Self-update is only available when Lithe is running from a packaged Lithe.app."; "Send" = "Send"; "Send a message to %@" = "Send a message to %@"; @@ -3444,6 +3469,7 @@ "Show conversation" = "Show conversation"; "Show file history" = "Show file history"; "Show in Finder" = "Show in Finder"; +"Show indent guides" = "Show indent guides"; "Show indexed Spring MVC routes" = "Show indexed Spring MVC routes"; "Show its own page in a tool window" = "Show its own page in a tool window"; "Show less" = "Show less"; @@ -3521,10 +3547,14 @@ "Smart Checkout stashes your changes, switches branch, then restores them. Force Checkout switches and discards them." = "Smart Checkout stashes your changes, switches branch, then restores them. Force Checkout switches and discards them."; "Smart Step Into" = "Smart Step Into"; "Smart step into" = "Smart step into"; +"Smooth" = "Smooth"; +"Smooth scrolling" = "Smooth scrolling"; "Soft Reset (Keep Changes Staged)" = "Soft Reset (Keep Changes Staged)"; "Soft Wraps" = "Soft Wraps"; +"Soft wrap" = "Soft wrap"; "Soft wrap is unavailable for very large files" = "Soft wrap is unavailable for very large files"; "Software Update" = "Software Update"; +"Solid" = "Solid"; "Solidity Support" = "Solidity Support"; "Some MCP configuration files could not be restored: %@. Refresh and check these files before retrying." = "Some MCP configuration files could not be restored: %@. Refresh and check these files before retrying."; "Some dependencies are not shown." = "Some dependencies are not shown."; @@ -3614,6 +3644,7 @@ "Step out" = "Step out"; "Step over" = "Step over"; "Stepping by instruction" = "Stepping by instruction"; +"Sticky scroll" = "Sticky scroll"; "Stop" = "Stop"; "Stop %@" = "Stop %@"; "Stop Camera" = "Stop Camera"; @@ -3675,6 +3706,7 @@ "Sync needed" = "Sync needed"; "Sync with Remote" = "Sync with Remote"; "Synchronize tags and remove local tags missing from the remote" = "Synchronize tags and remove local tags missing from the remote"; +"Syntax highlighting" = "Syntax highlighting"; "System" = "System"; "System Git" = "System Git"; "System default" = "System default"; @@ -4169,6 +4201,7 @@ "Total tokens: %@" = "Total tokens: %@"; "Tracking %@" = "Tracking %@"; "Tracking Branch" = "Tracking Branch"; +"Trailing" = "Trailing"; "Transport" = "Transport"; "Tree conflict" = "Tree conflict"; "Try Again" = "Try Again"; @@ -4211,6 +4244,7 @@ "Uncommitted changes block this revert" = "Uncommitted changes block this revert"; "Uncommitted changes stay in this worktree and are not included in the pull request." = "Uncommitted changes stay in this worktree and are not included in the pull request."; "Uncommitted submodule changes" = "Uncommitted submodule changes"; +"Underline" = "Underline"; "Undo" = "Undo"; "Undo Commit" = "Undo Last Commit (Keep Changes)"; "Undo Commit…" = "Undo Last Commit (Keep Changes)…"; @@ -4353,6 +4387,7 @@ "Usually the reversed domain name of your organization, such as com.example." = "Usually the reversed domain name of your organization, such as com.example."; "VM arguments" = "VM arguments"; "VM options" = "VM options"; +"VS Code" = "VS Code"; "Valid" = "Valid"; "Valid values are saved as you type." = "Valid values are saved as you type."; "Validating the credential stored in Keychain…" = "Validating the credential stored in Keychain…"; @@ -5800,6 +5835,41 @@ "native.editedLldFiles" = "Edited %1$lld files"; "native.editor" = "Editor"; "native.editorAndLanguageServices" = "Editor and language services"; +"native.editorAppearanceAbsolute" = "Absolute"; +"native.editorAppearanceAppliesImmediatelyToSourceEditorsInAllProjects" = "Applies immediately to source editors in all projects."; +"native.editorAppearanceBlink" = "Blink"; +"native.editorAppearanceBlock" = "Block"; +"native.editorAppearanceBoundary" = "Boundary"; +"native.editorAppearanceBracketSchemeDisabled" = "Disabled by color scheme"; +"native.editorAppearanceBracketSchemeEnabled" = "Enabled by color scheme"; +"native.editorAppearanceBracketSchemeInfo" = "Bracket colors follow the selected editor color scheme."; +"native.editorAppearanceColorizeBracketPairs" = "Colorize bracket pairs"; +"native.editorAppearanceCursor" = "Cursor"; +"native.editorAppearanceCursorBlinking" = "Cursor blinking"; +"native.editorAppearanceCursorStyle" = "Cursor style"; +"native.editorAppearanceEditorColorScheme" = "Editor color scheme"; +"native.editorAppearanceEditorColorsFollowLightAndDarkAppearanceIndependentlyOfTheInterfaceTheme" = "Editor colors follow light and dark appearance independently of the interface theme."; +"native.editorAppearanceEditorDisplay" = "Editor display"; +"native.editorAppearanceFontLigatures" = "Font ligatures"; +"native.editorAppearanceHighlightCurrentLine" = "Highlight current line"; +"native.editorAppearanceIntelliJIDEA" = "IntelliJ IDEA"; +"native.editorAppearanceItalicComments" = "Italic comments"; +"native.editorAppearanceLineNumbers" = "Line numbers"; +"native.editorAppearanceLineSpacing" = "Line spacing"; +"native.editorAppearanceRelative" = "Relative"; +"native.editorAppearanceRenderWhitespace" = "Render whitespace"; +"native.editorAppearanceScrollBeyondLastLine" = "Scroll beyond last line"; +"native.editorAppearanceSelection" = "Selection"; +"native.editorAppearanceShowIndentGuides" = "Show indent guides"; +"native.editorAppearanceSmooth" = "Smooth"; +"native.editorAppearanceSmoothScrolling" = "Smooth scrolling"; +"native.editorAppearanceSoftWrap" = "Soft wrap"; +"native.editorAppearanceSolid" = "Solid"; +"native.editorAppearanceStickyScroll" = "Sticky scroll"; +"native.editorAppearanceSyntaxHighlighting" = "Syntax highlighting"; +"native.editorAppearanceTrailing" = "Trailing"; +"native.editorAppearanceUnderline" = "Underline"; +"native.editorAppearanceVSCode" = "VS Code"; "native.editorResourcesAreMissingRebuildOrReinstallLithe" = "Editor resources are missing. Rebuild or reinstall Lithe."; "native.edits" = "Edits"; "native.effectiveFetchOptions" = "Effective Fetch options"; diff --git a/macos/Resources/zh-Hans.lproj/Localizable.strings b/macos/Resources/zh-Hans.lproj/Localizable.strings index 840a9eef8b..ea4b99b5e9 100644 --- a/macos/Resources/zh-Hans.lproj/Localizable.strings +++ b/macos/Resources/zh-Hans.lproj/Localizable.strings @@ -200,6 +200,7 @@ "Abort Rebase" = "中止变基"; "Abort this rebase and restore its original branch?" = "中止此次变基并恢复原分支?"; "Abort…" = "中止…"; +"Absolute" = "绝对行号"; "Accept" = "接受"; "Accept edits" = "接受编辑"; "Accepts all permissions" = "跳过常规权限审批,仍受环境和管理策略限制。"; @@ -345,6 +346,7 @@ "Application-wide editor and terminal settings remain available in the other categories." = "应用级编辑器和终端设置仍可在其他分类中使用。"; "Applications" = "应用"; "Applies immediately" = "立即生效"; +"Applies immediately to source editors in all projects." = "立即应用于所有项目的源码编辑器。"; "Applies the next time the language server starts." = "下次启动语言服务器时生效。"; "Apply" = "应用"; "Apply %@ to the current branch." = "将提交 %@ 应用到当前分支。"; @@ -449,6 +451,10 @@ "Big Endian" = "大端序"; "Binary" = "二进制"; "Binary files cannot be compared." = "无法比较二进制文件。"; +"Blink" = "闪烁"; +"Block" = "方块"; +"Boundary" = "边界空白"; +"Bracket colors follow the selected editor color scheme." = "括号颜色跟随所选编辑器配色方案。"; "Branch" = "分支"; "Branch '%@' tracks '%@', which has a different name. Choose which branch to update." = "分支“%@”跟踪的是名称不同的“%@”。请选择要更新哪个分支。"; "Branch context" = "分支上下文"; @@ -768,6 +774,7 @@ "Collapsed" = "已折叠"; "Collected logs cleared." = "已清空收集的日志。"; "Color theme" = "配色主题"; +"Colorize bracket pairs" = "彩色配对括号"; "Combine the messages when the rebase runs, including changes made at earlier Edit stops" = "变基执行时合并消息,包含此前暂停修订时更新的消息"; "Combined commit message" = "合并后的提交消息"; "Combines the pull request into one commit on the base branch." = "将拉取请求压缩为目标分支上的一个提交。"; @@ -1058,6 +1065,9 @@ "Current version: %@ · Verified target: %@" = "当前版本:%@ · 已核验目标:%@"; "Current: %@" = "当前:%@"; "Currently effective" = "当前生效"; +"Cursor" = "光标"; +"Cursor blinking" = "光标闪烁"; +"Cursor style" = "光标样式"; "Custom" = "自定义"; "Custom API provider" = "自定义 API 服务"; "Custom Agent" = "自定义 Agent"; @@ -1184,6 +1194,7 @@ "Disable breakpoint" = "禁用断点"; "Disable soft wraps" = "禁用自动换行"; "Disabled" = "已禁用"; +"Disabled by color scheme" = "配色方案已关闭"; "Disabled in Agent" = "Agent 已禁用"; "Disabled in this workspace" = "此工作区已停用"; "Disassembly" = "反汇编"; @@ -1292,6 +1303,9 @@ "Edited %lld files" = "编辑了 %lld 个文件"; "Editor" = "编辑器"; "Editor and language services" = "编辑器与语言服务"; +"Editor color scheme" = "编辑器配色方案"; +"Editor colors follow light and dark appearance independently of the interface theme." = "代码配色自动适配深浅外观,可独立于界面主题选择。"; +"Editor display" = "编辑器显示"; "Editor resources are missing. Rebuild or reinstall Lithe." = "缺少编辑器资源。请重新构建或重新安装 Lithe。"; "Editor tabs" = "编辑器标签栏"; "Edits" = "编辑"; @@ -1324,6 +1338,7 @@ "Enable for this project" = "在本项目启用"; "Enable skill" = "启用技能"; "Enabled" = "已启用"; +"Enabled by color scheme" = "配色方案已启用"; "Enabled in Agent" = "Agent 已启用"; "Enabled plugins can also use this project grant." = "已启用的插件也可使用此项目授权。"; "End All" = "全部结束"; @@ -1525,6 +1540,7 @@ "Follow the instructions you define below" = "遵循你在下方定义的指令"; "Follows each remote's tag setting" = "按各远程的标签设置"; "Font" = "字体"; +"Font ligatures" = "字体连字"; "Font size" = "字体大小"; "For example, notify" = "例如 notify"; "For example, notify me when a turn ends" = "例如:一轮结束时提醒我"; @@ -1681,6 +1697,7 @@ "Hide visualization" = "隐藏可视化"; "Hide worktree repositories" = "隐藏工作树仓库"; "High" = "高"; +"Highlight current line" = "高亮当前行"; "Highlight words" = "高亮单词"; "Hint" = "提示"; "History" = "历史"; @@ -1801,6 +1818,7 @@ "Instructions" = "指令"; "Instructions saved." = "指令已保存。"; "Integrate incoming changes into current branch (M)" = "将传入的更改合并到当前分支(M)"; +"IntelliJ IDEA" = "IntelliJ IDEA"; "Interactive Rebase" = "交互式变基"; "Interactive Rebase Aborted" = "交互式变基已中止"; "Interactive Rebase Completed" = "交互式变基已完成"; @@ -1827,6 +1845,7 @@ "Invalid shared instruction request." = "共享指令请求无效。"; "Invert" = "反选"; "It can read files, run commands and edit code after asking for permission." = "它可以在征得你的许可后读取文件、运行命令和修改代码。"; +"Italic comments" = "注释使用斜体"; "Item %lld of %lld" = "第 %lld 项,共 %lld 项"; "Its details and actions will appear here." = "工作树详情和可用操作会显示在这里。"; "Its session was closed or could not be restored." = "其会话已关闭或无法恢复。"; @@ -1934,6 +1953,8 @@ "Line" = "行"; "Line Breakpoints" = "行断点"; "Line breakpoint actions" = "行断点操作"; +"Line numbers" = "行号"; +"Line spacing" = "行间距"; "Linear" = "Linear"; "Lines kept for scrolling back in new terminal sessions." = "新建终端会话保留的可回滚行数。"; "Linked this conversation to a pull request" = "已将此对话关联到拉取请求"; @@ -2933,6 +2954,7 @@ "Registry website" = "Registry 网站"; "Regular Expression" = "正则表达式"; "Reinstall" = "重新安装"; +"Relative" = "相对行号"; "Release Date" = "发布日期"; "Release note" = "发布说明"; "Release notes" = "发行说明"; @@ -3008,6 +3030,7 @@ "Rename conversation" = "重命名会话"; "Renamed" = "已重命名"; "Rename…" = "重命名…"; +"Render whitespace" = "显示空白符"; "Reopen Pull Request" = "重新打开拉取请求"; "Reopen pull request" = "重新打开拉取请求"; "Reopen pull request?" = "重新打开拉取请求?"; @@ -3268,6 +3291,7 @@ "Saving writes to" = "保存将写入"; "Scala Support" = "Scala 支持"; "Scope" = "范围"; +"Scroll beyond last line" = "允许滚动超过末行"; "Scroll to new Git output" = "滚动到最新 Git 输出"; "Scrollback" = "回滚"; "Search" = "搜索"; @@ -3352,6 +3376,7 @@ "Selected directory" = "当前选择的目录"; "Selected from disk. The publisher and repository have not been verified." = "从本地磁盘选择,尚未验证发布者与源码仓库。"; "Selected runtime" = "已选择"; +"Selection" = "选中区域"; "Self-update is only available when Lithe is running from a packaged Lithe.app." = "只有从打包后的 Lithe.app 运行时才支持自动覆盖更新。"; "Send" = "发送"; "Send a message to %@" = "向 %@ 发送消息"; @@ -3444,6 +3469,7 @@ "Show conversation" = "显示对话"; "Show file history" = "显示文件历史"; "Show in Finder" = "在 Finder 中显示"; +"Show indent guides" = "显示缩进引导线"; "Show indexed Spring MVC routes" = "显示已索引的 Spring MVC 路由"; "Show its own page in a tool window" = "在工具窗口中显示插件自带的页面"; "Show less" = "收起"; @@ -3521,10 +3547,14 @@ "Smart Checkout stashes your changes, switches branch, then restores them. Force Checkout switches and discards them." = "智能检出会先贮藏你的更改,切换分支后再恢复。强制检出会直接切换并丢弃这些更改。"; "Smart Step Into" = "智能步入"; "Smart step into" = "智能步入"; +"Smooth" = "平滑"; +"Smooth scrolling" = "平滑滚动"; "Soft Reset (Keep Changes Staged)" = "软重置(保留暂存更改)"; "Soft Wraps" = "自动换行"; +"Soft wrap" = "自动换行"; "Soft wrap is unavailable for very large files" = "文件过大,自动换行不可用"; "Software Update" = "软件更新"; +"Solid" = "常亮"; "Solidity Support" = "Solidity 支持"; "Some MCP configuration files could not be restored: %@. Refresh and check these files before retrying." = "部分 MCP 配置文件未能恢复:%@。请刷新并检查这些文件后重试。"; "Some dependencies are not shown." = "部分依赖未显示。"; @@ -3614,6 +3644,7 @@ "Step out" = "单步跳出"; "Step over" = "单步跳过"; "Stepping by instruction" = "按指令单步"; +"Sticky scroll" = "固定显示作用域标题"; "Stop" = "停止"; "Stop %@" = "停止 %@"; "Stop Camera" = "停止摄像头"; @@ -3675,6 +3706,7 @@ "Sync needed" = "待同步"; "Sync with Remote" = "同步远程"; "Synchronize tags and remove local tags missing from the remote" = "同步标签并删除远程已不存在的本地标签"; +"Syntax highlighting" = "语法高亮"; "System" = "跟随系统"; "System Git" = "系统 Git"; "System default" = "系统默认"; @@ -4169,6 +4201,7 @@ "Total tokens: %@" = "总 token:%@"; "Tracking %@" = "跟踪 %@"; "Tracking Branch" = "跟踪的分支"; +"Trailing" = "行尾空白"; "Transport" = "传输方式"; "Tree conflict" = "树冲突"; "Try Again" = "重试"; @@ -4211,6 +4244,7 @@ "Uncommitted changes block this revert" = "未提交的更改阻止了本次回滚"; "Uncommitted changes stay in this worktree and are not included in the pull request." = "未提交的更改会保留在当前工作树中,不会包含在拉取请求里。"; "Uncommitted submodule changes" = "子模块有未提交的修改"; +"Underline" = "下划线"; "Undo" = "撤销"; "Undo Commit" = "撤销最近一次提交(保留更改)"; "Undo Commit…" = "撤销最近一次提交(保留更改)…"; @@ -4353,6 +4387,7 @@ "Usually the reversed domain name of your organization, such as com.example." = "通常是组织域名的反写,例如 com.example。"; "VM arguments" = "VM 参数"; "VM options" = "VM 选项"; +"VS Code" = "VS Code"; "Valid" = "有效"; "Valid values are saved as you type." = "值合法时立即保存。"; "Validating the credential stored in Keychain…" = "正在验证钥匙串中保存的凭据…"; @@ -5800,6 +5835,41 @@ "native.editedLldFiles" = "编辑了 %1$lld 个文件"; "native.editor" = "编辑器"; "native.editorAndLanguageServices" = "编辑器与语言服务"; +"native.editorAppearanceAbsolute" = "绝对行号"; +"native.editorAppearanceAppliesImmediatelyToSourceEditorsInAllProjects" = "立即应用于所有项目的源码编辑器。"; +"native.editorAppearanceBlink" = "闪烁"; +"native.editorAppearanceBlock" = "方块"; +"native.editorAppearanceBoundary" = "边界空白"; +"native.editorAppearanceBracketSchemeDisabled" = "配色方案已关闭"; +"native.editorAppearanceBracketSchemeEnabled" = "配色方案已启用"; +"native.editorAppearanceBracketSchemeInfo" = "括号颜色跟随所选编辑器配色方案。"; +"native.editorAppearanceColorizeBracketPairs" = "彩色配对括号"; +"native.editorAppearanceCursor" = "光标"; +"native.editorAppearanceCursorBlinking" = "光标闪烁"; +"native.editorAppearanceCursorStyle" = "光标样式"; +"native.editorAppearanceEditorColorScheme" = "编辑器配色方案"; +"native.editorAppearanceEditorColorsFollowLightAndDarkAppearanceIndependentlyOfTheInterfaceTheme" = "代码配色自动适配深浅外观,可独立于界面主题选择。"; +"native.editorAppearanceEditorDisplay" = "编辑器显示"; +"native.editorAppearanceFontLigatures" = "字体连字"; +"native.editorAppearanceHighlightCurrentLine" = "高亮当前行"; +"native.editorAppearanceIntelliJIDEA" = "IntelliJ IDEA"; +"native.editorAppearanceItalicComments" = "注释使用斜体"; +"native.editorAppearanceLineNumbers" = "行号"; +"native.editorAppearanceLineSpacing" = "行间距"; +"native.editorAppearanceRelative" = "相对行号"; +"native.editorAppearanceRenderWhitespace" = "显示空白符"; +"native.editorAppearanceScrollBeyondLastLine" = "允许滚动超过末行"; +"native.editorAppearanceSelection" = "选中区域"; +"native.editorAppearanceShowIndentGuides" = "显示缩进引导线"; +"native.editorAppearanceSmooth" = "平滑"; +"native.editorAppearanceSmoothScrolling" = "平滑滚动"; +"native.editorAppearanceSoftWrap" = "自动换行"; +"native.editorAppearanceSolid" = "常亮"; +"native.editorAppearanceStickyScroll" = "固定显示作用域标题"; +"native.editorAppearanceSyntaxHighlighting" = "语法高亮"; +"native.editorAppearanceTrailing" = "行尾空白"; +"native.editorAppearanceUnderline" = "下划线"; +"native.editorAppearanceVSCode" = "VS Code"; "native.editorResourcesAreMissingRebuildOrReinstallLithe" = "缺少编辑器资源。请重新构建或重新安装 Lithe。"; "native.edits" = "编辑"; "native.effectiveFetchOptions" = "实际生效的 Fetch 选项"; diff --git a/macos/Sources/Lithe/Models/Settings/AppSettings.swift b/macos/Sources/Lithe/Models/Settings/AppSettings.swift index 09f7564fa0..26981a527e 100644 --- a/macos/Sources/Lithe/Models/Settings/AppSettings.swift +++ b/macos/Sources/Lithe/Models/Settings/AppSettings.swift @@ -29,6 +29,7 @@ final class AppSettings: ObservableObject { static let themePreference = "settings.themePreference" static let language = "settings.language" static let editorFontSize = "settings.editorFontSize" + static let editorAppearance = "settings.editorAppearance" static let editorFontFamily = "settings.editorFontFamily" static let editorSoftWrap = "settings.editorSoftWrap" static let editorMinimap = "settings.editorMinimap" @@ -103,7 +104,26 @@ final class AppSettings: ObservableObject { InterfaceLanguage.select(language.rawValue) } } - @Published var editorFontSize: Double { didSet { defaults.set(editorFontSize, forKey: Key.editorFontSize) } } + @Published var editorFontSize: Double { + didSet { + let normalized = EditorAppearanceSettings.normalizedFontSize(editorFontSize) + if editorFontSize != normalized { + editorFontSize = normalized + return + } + defaults.set(editorFontSize, forKey: Key.editorFontSize) + } + } + @Published var editorAppearance: EditorAppearanceSettings { + didSet { + let normalized = EditorAppearanceSettings(values: editorAppearance.storedValues) + if editorAppearance != normalized { + editorAppearance = normalized + return + } + defaults.set(editorAppearance.storedValues, forKey: Key.editorAppearance) + } + } /// 代码编辑器编程字体族。默认是随包分发的 JetBrains Mono,只有在用户 /// 显式选择系统字体后才偏离打包字体;终端、Output 工具窗和提交信息 /// 输入框继续使用打包字体,与 IDEA 的 Editor/Console 字体分工一致。 @@ -307,7 +327,12 @@ final class AppSettings: ObservableObject { let storedLanguage = AppLanguage(rawValue: defaults.string(forKey: Key.language) ?? "") ?? .defaultLanguage language = storedLanguage InterfaceLanguage.select(storedLanguage.rawValue) - editorFontSize = defaults.object(forKey: Key.editorFontSize) as? Double ?? 13 + editorFontSize = EditorAppearanceSettings.normalizedFontSize( + defaults.object(forKey: Key.editorFontSize) as? Double ?? 13 + ) + editorAppearance = EditorAppearanceSettings( + values: defaults.object(forKey: Key.editorAppearance) as? [String: Any] ?? [:] + ) editorFontFamily = EditorFontResolution.normalizedFamily( defaults.string(forKey: Key.editorFontFamily) ) @@ -507,6 +532,7 @@ final class AppSettings: ObservableObject { themePreference = .dark language = .defaultLanguage editorFontSize = 13 + editorAppearance = EditorAppearanceSettings() editorFontFamily = EditorFontDefaults.monospacedFamily editorSoftWrapEnabled = false editorMinimapEnabled = true diff --git a/macos/Sources/Lithe/Models/Settings/EditorAppearanceSettings.swift b/macos/Sources/Lithe/Models/Settings/EditorAppearanceSettings.swift new file mode 100644 index 0000000000..30bc6c5575 --- /dev/null +++ b/macos/Sources/Lithe/Models/Settings/EditorAppearanceSettings.swift @@ -0,0 +1,101 @@ +import Foundation + +/// Application-wide source-editor preferences. Stored values use stable names, +/// while invalid or missing fields fall back individually for older preferences. +struct EditorAppearanceSettings: Equatable { + enum ColorScheme: String, CaseIterable { + case lithe, github, one, vscode, idea + var bracketPairColorization: Bool { self != .idea } + var title: String { + switch self { case .lithe: "Lithe"; case .github: "GitHub"; case .one: "One"; case .vscode: "VS Code"; case .idea: "IntelliJ IDEA" } + } + } + enum LineNumbers: String, CaseIterable { + case on, relative, off + var title: String { + switch self { case .on: "Absolute"; case .relative: "Relative"; case .off: "Off" } + } + } + enum Whitespace: String, CaseIterable { + case none, boundary, selection, trailing, all + var title: String { + switch self { + case .none: "None" + case .boundary: "Boundary" + case .selection: "Selection" + case .trailing: "Trailing" + case .all: "All" + } + } + } + enum CursorStyle: String, CaseIterable { + case line, block, underline + var title: String { + switch self { case .line: "Line"; case .block: "Block"; case .underline: "Underline" } + } + } + enum CursorBlinking: String, CaseIterable { + case blink, smooth, solid + var title: String { + switch self { case .blink: "Blink"; case .smooth: "Smooth"; case .solid: "Solid" } + } + } + + /// Zero keeps Monaco's platform default; otherwise this is a font-size multiplier. + var lineHeight: Double + var colorScheme: ColorScheme + var lineNumbers: LineNumbers + var whitespace: Whitespace + var cursorStyle: CursorStyle + var cursorBlinking: CursorBlinking + var indentGuides: Bool + var fontLigatures: Bool + var italicComments: Bool + var stickyScroll: Bool + var smoothScrolling: Bool + var scrollBeyondLastLine: Bool + var highlightCurrentLine: Bool + + init(values: [String: Any] = [:]) { + let height = values["lineHeight"] as? Double ?? 0 + lineHeight = height.isFinite && height > 0 ? min(max(height, 1), 3) : 0 + colorScheme = ColorScheme(rawValue: values["colorScheme"] as? String ?? "") ?? .lithe + lineNumbers = LineNumbers(rawValue: values["lineNumbers"] as? String ?? "") ?? .on + whitespace = Whitespace(rawValue: values["whitespace"] as? String ?? "") ?? .selection + cursorStyle = CursorStyle(rawValue: values["cursorStyle"] as? String ?? "") ?? .line + cursorBlinking = CursorBlinking(rawValue: values["cursorBlinking"] as? String ?? "") ?? .blink + indentGuides = values["indentGuides"] as? Bool ?? true + fontLigatures = values["fontLigatures"] as? Bool ?? false + italicComments = values["italicComments"] as? Bool ?? false + stickyScroll = values["stickyScroll"] as? Bool ?? true + smoothScrolling = values["smoothScrolling"] as? Bool ?? false + scrollBeyondLastLine = values["scrollBeyondLastLine"] as? Bool ?? false + highlightCurrentLine = values["highlightCurrentLine"] as? Bool ?? true + } + + var storedValues: [String: Any] { + ["lineHeight": lineHeight, "colorScheme": colorScheme.rawValue, + "lineNumbers": lineNumbers.rawValue, "whitespace": whitespace.rawValue, + "cursorStyle": cursorStyle.rawValue, "cursorBlinking": cursorBlinking.rawValue, + "indentGuides": indentGuides, "fontLigatures": fontLigatures, + "italicComments": italicComments, + "stickyScroll": stickyScroll, "smoothScrolling": smoothScrolling, + "scrollBeyondLastLine": scrollBeyondLastLine, "highlightCurrentLine": highlightCurrentLine] + } + + /// Only display options cross this boundary; read-only/model lifecycle stays host-owned. + var monacoOptions: [String: Any] { + ["lineHeight": lineHeight, "lineNumbers": lineNumbers.rawValue, + "renderWhitespace": whitespace.rawValue, "cursorStyle": cursorStyle.rawValue, + "cursorBlinking": cursorBlinking.rawValue, "fontLigatures": fontLigatures, + "guides": ["indentation": indentGuides, "highlightActiveIndentation": indentGuides], + "bracketPairColorization": ["enabled": colorScheme.bracketPairColorization], + "stickyScroll": ["enabled": stickyScroll], "smoothScrolling": smoothScrolling, + "scrollBeyondLastLine": scrollBeyondLastLine, + "renderLineHighlight": highlightCurrentLine ? "line" : "none"] + } + + static func normalizedFontSize(_ value: Double) -> Double { + value.isFinite ? min(max(value, 6), 72) : 13 + } +} diff --git a/macos/Sources/Lithe/Platform/MacOS/MonacoWorkbenchEditor.swift b/macos/Sources/Lithe/Platform/MacOS/MonacoWorkbenchEditor.swift index 5db8f8f038..11fd9f6ae6 100644 --- a/macos/Sources/Lithe/Platform/MacOS/MonacoWorkbenchEditor.swift +++ b/macos/Sources/Lithe/Platform/MacOS/MonacoWorkbenchEditor.swift @@ -31,10 +31,14 @@ struct MonacoWorkbenchThemeConfiguration: Equatable { let id: String let dark: Bool let colors: [String: String] + let syntaxScheme: EditorAppearanceSettings.ColorScheme + let italicComments: Bool - init(colorTheme: AppColorTheme, isDark: Bool, revealsWorkbenchBackground: Bool) { + init(colorTheme: AppColorTheme, isDark: Bool, revealsWorkbenchBackground: Bool, syntaxScheme: EditorAppearanceSettings.ColorScheme = .lithe, italicComments: Bool = false) { + self.syntaxScheme = syntaxScheme + self.italicComments = italicComments let palette = CodeEditorPalette(isDark: isDark, theme: colorTheme) - id = "macos-\(colorTheme.rawValue)-\(isDark ? "dark" : "light")-\(revealsWorkbenchBackground ? "wallpaper" : "solid")" + id = "macos-\(colorTheme.rawValue)-\(isDark ? "dark" : "light")-\(revealsWorkbenchBackground ? "wallpaper" : "solid")-\(syntaxScheme.rawValue)-\(italicComments)" dark = isDark colors = [ "background": revealsWorkbenchBackground ? "#00000000" : Self.cssColor(palette.background), @@ -51,7 +55,7 @@ struct MonacoWorkbenchThemeConfiguration: Equatable { } var bridgePayload: [String: Any] { - ["id": id, "dark": dark, "colors": colors] + ["id": id, "dark": dark, "colors": colors, "syntaxScheme": syntaxScheme.rawValue, "italicComments": italicComments] } static func cssColor(_ color: NSColor) -> String { @@ -75,6 +79,7 @@ private struct MonacoWorkbenchDisplayConfiguration: Equatable { let wrap: Bool let minimap: Bool let theme: MonacoWorkbenchThemeConfiguration + let appearance: EditorAppearanceSettings var bridgePayload: [String: Any] { [ @@ -83,7 +88,8 @@ private struct MonacoWorkbenchDisplayConfiguration: Equatable { "wrap": wrap, "minimap": minimap, "dark": theme.dark, - "theme": theme.bridgePayload + "theme": theme.bridgePayload, + "appearance": appearance.monacoOptions ] } } @@ -139,10 +145,13 @@ private struct MonacoWorkbenchContent: View { theme: MonacoWorkbenchThemeConfiguration( colorTheme: settings.colorTheme, isDark: colorScheme == .dark, - revealsWorkbenchBackground: background.hasImage + revealsWorkbenchBackground: background.hasImage, + syntaxScheme: settings.editorAppearance.colorScheme, + italicComments: settings.editorAppearance.italicComments ), wrap: settings.editorSoftWrapEnabled, minimap: settings.editorMinimapEnabled, + appearance: settings.editorAppearance, markers: diagnostics.diagnostics(for: document.url), secondaryMarkers: secondaryDocument.map { diagnostics.diagnostics(for: $0.url) } ?? [], softWrapControl: EditorSoftWrapToggleConfiguration( @@ -173,6 +182,7 @@ private struct MonacoWorkbenchSurface: NSViewRepresentable { let theme: MonacoWorkbenchThemeConfiguration let wrap: Bool let minimap: Bool + let appearance: EditorAppearanceSettings let markers: [EditorDiagnostic] let secondaryMarkers: [EditorDiagnostic] let softWrapControl: EditorSoftWrapToggleConfiguration @@ -188,7 +198,7 @@ private struct MonacoWorkbenchSurface: NSViewRepresentable { coordinator.session.detachView(ownerID: coordinator.ownerID) } func updateNSView(_ view: NSView, context: Context) { - session.update(ownerID: context.coordinator.ownerID, document: document, secondaryDocument: secondaryDocument, preview: preview, markdownScrollPosition: markdownScrollPosition, model: model, fontSize: fontSize, fontFamily: fontFamily, theme: theme, wrap: wrap, minimap: minimap, markers: markers, secondaryMarkers: secondaryMarkers, softWrapControl: softWrapControl) + session.update(ownerID: context.coordinator.ownerID, document: document, secondaryDocument: secondaryDocument, preview: preview, markdownScrollPosition: markdownScrollPosition, model: model, fontSize: fontSize, fontFamily: fontFamily, theme: theme, wrap: wrap, minimap: minimap, appearance: appearance, markers: markers, secondaryMarkers: secondaryMarkers, softWrapControl: softWrapControl) } } @@ -424,7 +434,7 @@ final class MonacoWorkbenchSession: NSObject, ObservableObject, WKNavigationDele selectMount() } - func update(ownerID: UUID, document: EditorDocument, secondaryDocument: EditorDocument?, preview: MonacoPreviewConfiguration?, markdownScrollPosition: Binding?, model: AppModel, fontSize: Double, fontFamily: String, theme: MonacoWorkbenchThemeConfiguration, wrap: Bool, minimap: Bool, markers: [EditorDiagnostic], secondaryMarkers: [EditorDiagnostic], softWrapControl: EditorSoftWrapToggleConfiguration? = nil) { + func update(ownerID: UUID, document: EditorDocument, secondaryDocument: EditorDocument?, preview: MonacoPreviewConfiguration?, markdownScrollPosition: Binding?, model: AppModel, fontSize: Double, fontFamily: String, theme: MonacoWorkbenchThemeConfiguration, wrap: Bool, minimap: Bool, appearance: EditorAppearanceSettings = EditorAppearanceSettings(), markers: [EditorDiagnostic], secondaryMarkers: [EditorDiagnostic], softWrapControl: EditorSoftWrapToggleConfiguration? = nil) { guard mounts[ownerID] != nil else { return } self.model = model observeFind(model: model) @@ -436,11 +446,11 @@ final class MonacoWorkbenchSession: NSObject, ObservableObject, WKNavigationDele ) } mounts[ownerID]?.update = { [weak self, weak document, weak secondaryDocument, weak model] in guard let self, let document, let model else { return } - self.present(document: document, model: model, fontSize: fontSize, fontFamily: fontFamily, theme: theme, wrap: wrap, minimap: minimap, markers: markers) + self.present(document: document, model: model, fontSize: fontSize, fontFamily: fontFamily, theme: theme, wrap: wrap, minimap: minimap, appearance: appearance, markers: markers) self.presentMarkdownScroll(document: document, binding: markdownScrollPosition) if let secondaryDocument { self.hasSecondaryView = true - self.present(document: secondaryDocument, model: model, fontSize: fontSize, fontFamily: fontFamily, theme: theme, wrap: wrap, minimap: minimap, markers: secondaryMarkers, surface: "secondary") + self.present(document: secondaryDocument, model: model, fontSize: fontSize, fontFamily: fontFamily, theme: theme, wrap: wrap, minimap: minimap, appearance: appearance, markers: secondaryMarkers, surface: "secondary") } else if self.hasSecondaryView { self.hasSecondaryView = false self.activeIDs.removeValue(forKey: "secondary") @@ -804,7 +814,7 @@ final class MonacoWorkbenchSession: NSObject, ObservableObject, WKNavigationDele } } - private func present(document: EditorDocument, model: AppModel, fontSize: Double, fontFamily: String, theme: MonacoWorkbenchThemeConfiguration, wrap: Bool, minimap: Bool, markers: [EditorDiagnostic], surface: String = "primary") { + private func present(document: EditorDocument, model: AppModel, fontSize: Double, fontFamily: String, theme: MonacoWorkbenchThemeConfiguration, wrap: Bool, minimap: Bool, appearance: EditorAppearanceSettings = EditorAppearanceSettings(), markers: [EditorDiagnostic], surface: String = "primary") { guard viewOwnerID != nil else { return } let id = document.id.uuidString let groupDocuments: [EditorDocument] @@ -901,7 +911,8 @@ final class MonacoWorkbenchSession: NSObject, ObservableObject, WKNavigationDele fontFamily: MacEditorFontCatalog.editorFontStack(fontFamily), wrap: wrap, minimap: minimap, - theme: theme + theme: theme, + appearance: appearance ) if lastConfiguration != configuration { lastConfiguration = configuration diff --git a/macos/Sources/Lithe/Views/App/SettingsSearchVocabulary.swift b/macos/Sources/Lithe/Views/App/SettingsSearchVocabulary.swift index c2c677d372..5c6536a5eb 100644 --- a/macos/Sources/Lithe/Views/App/SettingsSearchVocabulary.swift +++ b/macos/Sources/Lithe/Views/App/SettingsSearchVocabulary.swift @@ -18,7 +18,7 @@ enum SettingsSearchVocabulary { case .general: ["General", "Appearance", "Color theme", "Appearance mode", "Language", "Projects", "Files", "Version control", "Hidden paths"] case .editor: - ["Editor", "Display", "Editor tabs", "tabs", "Font", "Font size", "Search fonts", "File tree row height", "Show minimap", "Minimap", "Indentation", "Tab width"] + ["Editor", "Display", "Editor tabs", "tabs", "Font", "Font size", "Search fonts", "File tree row height", "Show minimap", "Minimap", "Indentation", "Tab width", "Line spacing", "Font ligatures", "Syntax highlighting", "Editor color scheme", "Italic comments", "Colorize bracket pairs", "Line numbers", "Render whitespace", "Soft wrap", "Show indent guides", "Highlight current line", "Sticky scroll", "Smooth scrolling", "Scroll beyond last line", "Cursor style", "Cursor blinking"] case .keymap: ["Keymap", "Keyboard shortcuts", "Shortcuts", "Actions"] case .project: diff --git a/macos/Sources/Lithe/Views/App/SettingsView.swift b/macos/Sources/Lithe/Views/App/SettingsView.swift index 7f146b28e0..9fdf6dcc94 100644 --- a/macos/Sources/Lithe/Views/App/SettingsView.swift +++ b/macos/Sources/Lithe/Views/App/SettingsView.swift @@ -617,13 +617,33 @@ struct SettingsView: View { row("Font size") { LitheSettingsStepper( value: $settings.editorFontSize, - in: 8...22, + in: 6...72, step: 1, width: 126, accessibilityLabel: "Font size", title: { "\(Int($0)) pt" } ) } + row("Line spacing") { + HStack(spacing: 10) { + LitheSettingsCheckbox( + isOn: Binding( + get: { settings.editorAppearance.lineHeight == 0 }, + set: { settings.editorAppearance.lineHeight = $0 ? 0 : 1.5 } + ), + title: "Automatic" + ) + if settings.editorAppearance.lineHeight > 0 { + LitheSettingsStepper( + value: $settings.editorAppearance.lineHeight, + in: 1...3, step: 0.1, width: 126, + accessibilityLabel: "Line spacing", + title: { String(format: "%.1f ×", $0) } + ) + } + } + } + LitheSettingsCheckbox(isOn: $settings.editorAppearance.fontLigatures, title: "Font ligatures") row("File tree row height") { LitheSettingsStepper( value: $settings.projectTreeRowHeight, @@ -643,6 +663,7 @@ struct SettingsView: View { title: "Show minimap" ) } + editorAppearanceOptions group("Editor tabs") { row("Layout") { LitheSettingsSelect( @@ -728,6 +749,56 @@ struct SettingsView: View { } } + private var editorAppearanceOptions: some View { + VStack(alignment: .leading, spacing: 18) { + group("Syntax highlighting") { + row("Editor color scheme", info: "Editor colors follow light and dark appearance independently of the interface theme.") { + LitheSettingsSelect( + selection: $settings.editorAppearance.colorScheme, + options: EditorAppearanceSettings.ColorScheme.allCases, + width: 190, accessibilityLabel: "Editor color scheme", title: \.title + ) + } + LitheSettingsCheckbox(isOn: $settings.editorAppearance.italicComments, title: "Italic comments") + row("Colorize bracket pairs", info: "Bracket colors follow the selected editor color scheme.") { + Text(LocalizedStringKey(settings.editorAppearance.colorScheme.bracketPairColorization + ? "Enabled by color scheme" : "Disabled by color scheme")) + .foregroundStyle(LitheTheme.secondaryText) + } + } + group("Editor display", info: "Applies immediately to source editors in all projects.") { + row("Line numbers") { + LitheSettingsSelect(selection: $settings.editorAppearance.lineNumbers, + options: EditorAppearanceSettings.LineNumbers.allCases, + width: 190, accessibilityLabel: "Line numbers", title: \.title) + } + row("Render whitespace") { + LitheSettingsSelect(selection: $settings.editorAppearance.whitespace, + options: EditorAppearanceSettings.Whitespace.allCases, + width: 190, accessibilityLabel: "Render whitespace", title: \.title) + } + LitheSettingsCheckbox(isOn: $settings.editorSoftWrapEnabled, title: "Soft wrap") + LitheSettingsCheckbox(isOn: $settings.editorAppearance.indentGuides, title: "Show indent guides") + LitheSettingsCheckbox(isOn: $settings.editorAppearance.highlightCurrentLine, title: "Highlight current line") + LitheSettingsCheckbox(isOn: $settings.editorAppearance.stickyScroll, title: "Sticky scroll") + LitheSettingsCheckbox(isOn: $settings.editorAppearance.smoothScrolling, title: "Smooth scrolling") + LitheSettingsCheckbox(isOn: $settings.editorAppearance.scrollBeyondLastLine, title: "Scroll beyond last line") + } + group("Cursor") { + row("Cursor style") { + LitheSettingsSelect(selection: $settings.editorAppearance.cursorStyle, + options: EditorAppearanceSettings.CursorStyle.allCases, + width: 190, accessibilityLabel: "Cursor style", title: \.title) + } + row("Cursor blinking") { + LitheSettingsSelect(selection: $settings.editorAppearance.cursorBlinking, + options: EditorAppearanceSettings.CursorBlinking.allCases, + width: 190, accessibilityLabel: "Cursor blinking", title: \.title) + } + } + } + } + /// Monospaced families plus the stored selection. The stored family is kept /// in the list even when it is no longer installed, so the user can see what /// is configured instead of the control silently snapping to the default. diff --git a/macos/Tests/LitheTests/AppSettingsTests.swift b/macos/Tests/LitheTests/AppSettingsTests.swift index c70c6dd0a3..55128990d2 100644 --- a/macos/Tests/LitheTests/AppSettingsTests.swift +++ b/macos/Tests/LitheTests/AppSettingsTests.swift @@ -7,6 +7,67 @@ import Testing @Suite("App settings") @MainActor struct AppSettingsTests { + @Test + func editorAppearancePersistsNormalizesAndRestoresWithoutChangingOtherSettings() throws { + let store = AppSettingsTestStore() + let settings = AppSettings(store: store) + settings.editorFontFamily = "Fixture Mono" + settings.editorFontSize = 28 + settings.editorAppearance.lineHeight = 1.8 + settings.editorAppearance.colorScheme = .github + settings.editorAppearance.lineNumbers = .relative + settings.editorAppearance.whitespace = .all + settings.editorAppearance.cursorStyle = .block + settings.editorAppearance.cursorBlinking = .solid + settings.editorAppearance.indentGuides = false + settings.editorAppearance.fontLigatures = true + settings.editorAppearance.italicComments = true + settings.editorAppearance.stickyScroll = false + settings.editorAppearance.smoothScrolling = true + settings.editorAppearance.scrollBeyondLastLine = true + settings.editorAppearance.highlightCurrentLine = false + let reloaded = AppSettings(store: store) + #expect(reloaded.editorAppearance == settings.editorAppearance) + #expect(reloaded.editorFontSize == 28) + #expect(reloaded.editorFontFamily == "Fixture Mono") + #expect(AppSettings(store: AppSettingsTestStore()).editorAppearance == EditorAppearanceSettings()) + let options = reloaded.editorAppearance.monacoOptions + #expect(options["lineHeight"] as? Double == 1.8) + #expect(options["lineNumbers"] as? String == "relative") + #expect(options["readOnly"] == nil) + #expect(JSONSerialization.isValidJSONObject(options)) + + for scheme in EditorAppearanceSettings.ColorScheme.allCases { + settings.editorAppearance.colorScheme = scheme + #expect(AppSettings(store: store).editorAppearance.colorScheme == scheme) + let migrated = EditorAppearanceSettings(values: ["colorScheme": scheme.rawValue, "bracketPairColorization": scheme == .idea]) + #expect(migrated.monacoOptions["bracketPairColorization"] as? [String: Bool] == ["enabled": scheme != .idea]) + #expect(migrated.storedValues["bracketPairColorization"] == nil) + } + + // A malformed field must not erase neighboring valid preferences. + store.set(["lineHeight": Double.infinity, "colorScheme": "unknown", "lineNumbers": "off", + "italicComments": true, "cursorStyle": 42], forKey: "settings.editorAppearance") + store.set(Double.nan, forKey: "settings.editorFontSize") + let repaired = AppSettings(store: store) + #expect(repaired.editorAppearance.lineHeight == 0) + #expect(repaired.editorAppearance.colorScheme == .lithe) + #expect(repaired.editorAppearance.lineNumbers == .off) + #expect(repaired.editorAppearance.italicComments) + #expect(repaired.editorAppearance.cursorStyle == .line) + #expect(repaired.editorFontSize == 13) + repaired.editorAppearance.lineHeight = 20 + #expect(repaired.editorAppearance.lineHeight == 3) + repaired.editorAppearance.lineHeight = 0.2 + #expect(repaired.editorAppearance.lineHeight == 1) + repaired.editorFontSize = 100 + #expect(repaired.editorFontSize == 72) + #expect(AppSettings(store: store).editorFontSize == 72) + repaired.restoreDefaults() + #expect(AppSettings(store: store).editorAppearance == EditorAppearanceSettings()) + #expect(AppSettings(store: store).editorFontSize == 13) + } + @Test func gitExecutionPreferencesPersistAndRemainIsolatedBetweenSettingsInstances() { let store = AppSettingsTestStore() diff --git a/macos/Tests/LitheTests/MonacoWorkbenchThemeTests.swift b/macos/Tests/LitheTests/MonacoWorkbenchThemeTests.swift index ebfcbcda88..df529da61a 100644 --- a/macos/Tests/LitheTests/MonacoWorkbenchThemeTests.swift +++ b/macos/Tests/LitheTests/MonacoWorkbenchThemeTests.swift @@ -4,6 +4,17 @@ import Testing @Suite("Monaco workbench theme") struct MonacoWorkbenchThemeTests { + @Test func syntaxPreferencesCrossTheBridgeWithoutChangingWorkbenchSurfaces() { + let original = MonacoWorkbenchThemeConfiguration(colorTheme: .linear, isDark: true, + revealsWorkbenchBackground: true) + let customized = MonacoWorkbenchThemeConfiguration(colorTheme: .linear, isDark: true, + revealsWorkbenchBackground: true, syntaxScheme: .github, italicComments: true) + #expect(customized.colors == original.colors) + #expect(customized.id != original.id) + #expect(customized.bridgePayload["syntaxScheme"] as? String == "github") + #expect(customized.bridgePayload["italicComments"] as? Bool == true) + } + @Test func workbenchBackgroundMakesOnlyTheEditorSurfaceTransparent() { let solid = MonacoWorkbenchThemeConfiguration( colorTheme: .lithe, diff --git a/macos/Tests/LitheTests/SettingsSearchVocabularyTests.swift b/macos/Tests/LitheTests/SettingsSearchVocabularyTests.swift index 0a7a285737..267bc56310 100644 --- a/macos/Tests/LitheTests/SettingsSearchVocabularyTests.swift +++ b/macos/Tests/LitheTests/SettingsSearchVocabularyTests.swift @@ -126,6 +126,9 @@ struct SettingsSearchVocabularyTests { #expect(SettingsSearchVocabulary.matches(query: "字体", category: .editor, localize: localize)) #expect(SettingsSearchVocabulary.matches(query: "搜索字体", category: .editor, localize: localize)) #expect(SettingsSearchVocabulary.matches(query: "缩进", category: .editor, localize: localize)) + for query in ["行间距", "配色", "空白符", "光标", "连字", "滚动"] { + #expect(SettingsSearchVocabulary.matches(query: query, category: .editor, localize: localize)) + } #expect(!SettingsSearchVocabulary.matches(query: "字体", category: .terminal, localize: localize)) } diff --git a/scripts/resources/reuse-worktree-resources.mjs b/scripts/resources/reuse-worktree-resources.mjs index 787ffba75b..d2c910f8c9 100644 --- a/scripts/resources/reuse-worktree-resources.mjs +++ b/scripts/resources/reuse-worktree-resources.mjs @@ -422,6 +422,7 @@ async function main() { // Per-launch Codex helpers also own memory-only context replay tickets and // loopback capabilities; they stay on this exclusion route, never a copy stage. // Agent edit-and-paste state is user-owned runtime data, without a copy stage. + // editor-theme-assets rebuild palettes and license notices together; no build stamp permits reuse. if (excludedResources.some((resource) => resource.id === identifier)) { const excluded = excludedResources.find((resource) => resource.id === identifier); throw new Error(`Resource ${identifier} is isolated (${excluded.locations.join(", ")}): ${excluded.identity}; ${excluded.reason}; it cannot be reused across worktrees`); diff --git a/scripts/resources/test-reuse-worktree-resources.mjs b/scripts/resources/test-reuse-worktree-resources.mjs index f04772f2d8..67d8fc0c15 100644 --- a/scripts/resources/test-reuse-worktree-resources.mjs +++ b/scripts/resources/test-reuse-worktree-resources.mjs @@ -108,6 +108,15 @@ try { assert.match(diagnostics(refused), /swiftpm-build-state is isolated.*cannot be reused/); await assert.rejects(fs.access(targetRoot), { code: "ENOENT" }); }); + await test("editor palettes and licenses cannot be copied from another checkout", { timeout: 15000 }, async () => { + const registry = JSON.parse(await fs.readFile(path.join(scriptDirectory, "worktree-resources.json"), "utf8")); + const resource = registry.excludedResources.find(item => item.id === "editor-theme-assets"); + assert.equal(resource.reusable, false); + assert.ok(resource.locations.includes(".artifacts/editor/macos/")); + const refused = reuse(["--resource", resource.id]); + assert.notEqual(refused.status, 0); + assert.match(diagnostics(refused), /editor-theme-assets.*cannot be reused/); + }); await test("shared translations are regenerated locally rather than reused", { timeout: 15000 }, async () => { const registry = JSON.parse(await fs.readFile(path.join(scriptDirectory, "worktree-resources.json"), "utf8")); const resource = registry.excludedResources.find(item => item.id === "shared-i18n"); diff --git a/scripts/resources/worktree-resources.json b/scripts/resources/worktree-resources.json index 8960cad5c7..f181f344b5 100644 --- a/scripts/resources/worktree-resources.json +++ b/scripts/resources/worktree-resources.json @@ -429,6 +429,17 @@ ], "identity": "Current worktree shared locale manifest, message registry, per-locale translations and all generated native string-table bytes. Generated outputs have no sealed version/platform/architecture/toolchain reuse identity stamp.", "reason": "Git owns source and checked-in generated outputs. Regenerate and validate them in each target worktree with scripts/i18n/generate-i18n.mjs; no download, cache validator or cross-worktree copy stage is permitted. Packaging copies registered native tables into its own build destination before signing and converts only that copy to binary plist. Runtime only reads bundled tables; it creates no state and changes neither code-signature bytes nor the Sparkle delta baseline." + }, + { + "id": "editor-theme-assets", + "reusable": false, + "locations": [ + ".artifacts/editor/macos/", + ".artifacts/monaco-workbench/test-assets/", + ".artifacts/monaco-probe/assets/" + ], + "identity": "Current source palettes, upstream pinned theme revisions, license/NOTICE files, Monaco and Bun versions; no validated build identity stamp", + "reason": "Rebuild JavaScript and bundled theme licenses together from this checkout. No copy stage or runtime writes; packaged assets remain read-only and preserve code signatures and Sparkle delta baselines." } ] } diff --git a/shared/i18n/locales/en-US.json b/shared/i18n/locales/en-US.json index d6e7daa6ce..e2419a9031 100644 --- a/shared/i18n/locales/en-US.json +++ b/shared/i18n/locales/en-US.json @@ -9516,7 +9516,42 @@ "document.fileAccess.theSharedFileCouldNotBeFullySavedAllHardLinksMayContainPartialChangesYourEditorContentsAreRetainedCheckTheFileBeforeRetrying": "The shared file could not be fully saved. All hard links may contain partial changes. Your editor contents are retained; check the file before retrying.", "document.fileAccess.thisFileTypeCannotBeOpenedInThisEditor": "This file type cannot be opened in this editor", "document.fileAccess.updateAllHardLinks": "Update All Hard Links", - "document.fileAccess.savePermissionDenied": "Could not save {arg1}: permission denied. Check the file and parent folder permissions. Your edits are still unsaved." + "document.fileAccess.savePermissionDenied": "Could not save {arg1}: permission denied. Check the file and parent folder permissions. Your edits are still unsaved.", + "native.editorAppearanceAbsolute": "Absolute", + "native.editorAppearanceAppliesImmediatelyToSourceEditorsInAllProjects": "Applies immediately to source editors in all projects.", + "native.editorAppearanceBlink": "Blink", + "native.editorAppearanceBlock": "Block", + "native.editorAppearanceBoundary": "Boundary", + "native.editorAppearanceColorizeBracketPairs": "Colorize bracket pairs", + "native.editorAppearanceCursor": "Cursor", + "native.editorAppearanceCursorBlinking": "Cursor blinking", + "native.editorAppearanceCursorStyle": "Cursor style", + "native.editorAppearanceEditorColorScheme": "Editor color scheme", + "native.editorAppearanceEditorColorsFollowLightAndDarkAppearanceIndependentlyOfTheInterfaceTheme": "Editor colors follow light and dark appearance independently of the interface theme.", + "native.editorAppearanceEditorDisplay": "Editor display", + "native.editorAppearanceFontLigatures": "Font ligatures", + "native.editorAppearanceHighlightCurrentLine": "Highlight current line", + "native.editorAppearanceItalicComments": "Italic comments", + "native.editorAppearanceLineNumbers": "Line numbers", + "native.editorAppearanceLineSpacing": "Line spacing", + "native.editorAppearanceRelative": "Relative", + "native.editorAppearanceRenderWhitespace": "Render whitespace", + "native.editorAppearanceScrollBeyondLastLine": "Scroll beyond last line", + "native.editorAppearanceSelection": "Selection", + "native.editorAppearanceShowIndentGuides": "Show indent guides", + "native.editorAppearanceSmooth": "Smooth", + "native.editorAppearanceSmoothScrolling": "Smooth scrolling", + "native.editorAppearanceSoftWrap": "Soft wrap", + "native.editorAppearanceSolid": "Solid", + "native.editorAppearanceStickyScroll": "Sticky scroll", + "native.editorAppearanceSyntaxHighlighting": "Syntax highlighting", + "native.editorAppearanceTrailing": "Trailing", + "native.editorAppearanceUnderline": "Underline", + "native.editorAppearanceVSCode": "VS Code", + "native.editorAppearanceIntelliJIDEA": "IntelliJ IDEA", + "native.editorAppearanceBracketSchemeInfo": "Bracket colors follow the selected editor color scheme.", + "native.editorAppearanceBracketSchemeEnabled": "Enabled by color scheme", + "native.editorAppearanceBracketSchemeDisabled": "Disabled by color scheme" }, "overrides": { "windows": { diff --git a/shared/i18n/locales/zh-CN.json b/shared/i18n/locales/zh-CN.json index 4d980105d9..3b3eeabac5 100644 --- a/shared/i18n/locales/zh-CN.json +++ b/shared/i18n/locales/zh-CN.json @@ -9516,7 +9516,42 @@ "document.fileAccess.theSharedFileCouldNotBeFullySavedAllHardLinksMayContainPartialChangesYourEditorContentsAreRetainedCheckTheFileBeforeRetrying": "未能完整保存共享文件。所有硬链接可能已包含部分修改。编辑器中的内容已保留,请检查文件后再重试。", "document.fileAccess.thisFileTypeCannotBeOpenedInThisEditor": "此编辑器不支持打开这种文件类型", "document.fileAccess.updateAllHardLinks": "更新所有硬链接", - "document.fileAccess.savePermissionDenied": "无法保存 {arg1}:权限不足。请检查文件及其父文件夹的权限,编辑内容尚未保存。" + "document.fileAccess.savePermissionDenied": "无法保存 {arg1}:权限不足。请检查文件及其父文件夹的权限,编辑内容尚未保存。", + "native.editorAppearanceAbsolute": "绝对行号", + "native.editorAppearanceAppliesImmediatelyToSourceEditorsInAllProjects": "立即应用于所有项目的源码编辑器。", + "native.editorAppearanceBlink": "闪烁", + "native.editorAppearanceBlock": "方块", + "native.editorAppearanceBoundary": "边界空白", + "native.editorAppearanceColorizeBracketPairs": "彩色配对括号", + "native.editorAppearanceCursor": "光标", + "native.editorAppearanceCursorBlinking": "光标闪烁", + "native.editorAppearanceCursorStyle": "光标样式", + "native.editorAppearanceEditorColorScheme": "编辑器配色方案", + "native.editorAppearanceEditorColorsFollowLightAndDarkAppearanceIndependentlyOfTheInterfaceTheme": "代码配色自动适配深浅外观,可独立于界面主题选择。", + "native.editorAppearanceEditorDisplay": "编辑器显示", + "native.editorAppearanceFontLigatures": "字体连字", + "native.editorAppearanceHighlightCurrentLine": "高亮当前行", + "native.editorAppearanceItalicComments": "注释使用斜体", + "native.editorAppearanceLineNumbers": "行号", + "native.editorAppearanceLineSpacing": "行间距", + "native.editorAppearanceRelative": "相对行号", + "native.editorAppearanceRenderWhitespace": "显示空白符", + "native.editorAppearanceScrollBeyondLastLine": "允许滚动超过末行", + "native.editorAppearanceSelection": "选中区域", + "native.editorAppearanceShowIndentGuides": "显示缩进引导线", + "native.editorAppearanceSmooth": "平滑", + "native.editorAppearanceSmoothScrolling": "平滑滚动", + "native.editorAppearanceSoftWrap": "自动换行", + "native.editorAppearanceSolid": "常亮", + "native.editorAppearanceStickyScroll": "固定显示作用域标题", + "native.editorAppearanceSyntaxHighlighting": "语法高亮", + "native.editorAppearanceTrailing": "行尾空白", + "native.editorAppearanceUnderline": "下划线", + "native.editorAppearanceVSCode": "VS Code", + "native.editorAppearanceIntelliJIDEA": "IntelliJ IDEA", + "native.editorAppearanceBracketSchemeInfo": "括号颜色跟随所选编辑器配色方案。", + "native.editorAppearanceBracketSchemeEnabled": "配色方案已启用", + "native.editorAppearanceBracketSchemeDisabled": "配色方案已关闭" }, "overrides": { "macos": { diff --git a/shared/i18n/messages.json b/shared/i18n/messages.json index d8510bf2db..2f839c592b 100644 --- a/shared/i18n/messages.json +++ b/shared/i18n/messages.json @@ -90808,6 +90808,391 @@ "arg1" ] } + }, + { + "id": "native.editorAppearanceAbsolute", + "description": "Source editor appearance: Absolute", + "platforms": [ + "macos" + ], + "macos": { + "key": "Absolute", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceAppliesImmediatelyToSourceEditorsInAllProjects", + "description": "Source editor appearance: Applies immediately to source editors in all projects.", + "platforms": [ + "macos" + ], + "macos": { + "key": "Applies immediately to source editors in all projects.", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceBlink", + "description": "Source editor appearance: Blink", + "platforms": [ + "macos" + ], + "macos": { + "key": "Blink", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceBlock", + "description": "Source editor appearance: Block", + "platforms": [ + "macos" + ], + "macos": { + "key": "Block", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceBoundary", + "description": "Source editor appearance: Boundary", + "platforms": [ + "macos" + ], + "macos": { + "key": "Boundary", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceColorizeBracketPairs", + "description": "Source editor appearance: Colorize bracket pairs", + "platforms": [ + "macos" + ], + "macos": { + "key": "Colorize bracket pairs", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceCursor", + "description": "Source editor appearance: Cursor", + "platforms": [ + "macos" + ], + "macos": { + "key": "Cursor", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceCursorBlinking", + "description": "Source editor appearance: Cursor blinking", + "platforms": [ + "macos" + ], + "macos": { + "key": "Cursor blinking", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceCursorStyle", + "description": "Source editor appearance: Cursor style", + "platforms": [ + "macos" + ], + "macos": { + "key": "Cursor style", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceEditorColorScheme", + "description": "Source editor appearance: Editor color scheme", + "platforms": [ + "macos" + ], + "macos": { + "key": "Editor color scheme", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceEditorColorsFollowLightAndDarkAppearanceIndependentlyOfTheInterfaceTheme", + "description": "Source editor appearance: Editor colors follow light and dark appearance independently of the interface theme.", + "platforms": [ + "macos" + ], + "macos": { + "key": "Editor colors follow light and dark appearance independently of the interface theme.", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceEditorDisplay", + "description": "Source editor appearance: Editor display", + "platforms": [ + "macos" + ], + "macos": { + "key": "Editor display", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceFontLigatures", + "description": "Source editor appearance: Font ligatures", + "platforms": [ + "macos" + ], + "macos": { + "key": "Font ligatures", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceHighlightCurrentLine", + "description": "Source editor appearance: Highlight current line", + "platforms": [ + "macos" + ], + "macos": { + "key": "Highlight current line", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceItalicComments", + "description": "Source editor appearance: Italic comments", + "platforms": [ + "macos" + ], + "macos": { + "key": "Italic comments", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceLineNumbers", + "description": "Source editor appearance: Line numbers", + "platforms": [ + "macos" + ], + "macos": { + "key": "Line numbers", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceLineSpacing", + "description": "Source editor appearance: Line spacing", + "platforms": [ + "macos" + ], + "macos": { + "key": "Line spacing", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceRelative", + "description": "Source editor appearance: Relative", + "platforms": [ + "macos" + ], + "macos": { + "key": "Relative", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceRenderWhitespace", + "description": "Source editor appearance: Render whitespace", + "platforms": [ + "macos" + ], + "macos": { + "key": "Render whitespace", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceScrollBeyondLastLine", + "description": "Source editor appearance: Scroll beyond last line", + "platforms": [ + "macos" + ], + "macos": { + "key": "Scroll beyond last line", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceSelection", + "description": "Source editor appearance: Selection", + "platforms": [ + "macos" + ], + "macos": { + "key": "Selection", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceShowIndentGuides", + "description": "Source editor appearance: Show indent guides", + "platforms": [ + "macos" + ], + "macos": { + "key": "Show indent guides", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceSmooth", + "description": "Source editor appearance: Smooth", + "platforms": [ + "macos" + ], + "macos": { + "key": "Smooth", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceSmoothScrolling", + "description": "Source editor appearance: Smooth scrolling", + "platforms": [ + "macos" + ], + "macos": { + "key": "Smooth scrolling", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceSoftWrap", + "description": "Source editor appearance: Soft wrap", + "platforms": [ + "macos" + ], + "macos": { + "key": "Soft wrap", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceSolid", + "description": "Source editor appearance: Solid", + "platforms": [ + "macos" + ], + "macos": { + "key": "Solid", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceStickyScroll", + "description": "Source editor appearance: Sticky scroll", + "platforms": [ + "macos" + ], + "macos": { + "key": "Sticky scroll", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceSyntaxHighlighting", + "description": "Source editor appearance: Syntax highlighting", + "platforms": [ + "macos" + ], + "macos": { + "key": "Syntax highlighting", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceTrailing", + "description": "Source editor appearance: Trailing", + "platforms": [ + "macos" + ], + "macos": { + "key": "Trailing", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceUnderline", + "description": "Source editor appearance: Underline", + "platforms": [ + "macos" + ], + "macos": { + "key": "Underline", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceVSCode", + "description": "Bundled editor color scheme product name", + "platforms": [ + "macos" + ], + "macos": { + "key": "VS Code", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceIntelliJIDEA", + "description": "Bundled editor color scheme product name", + "platforms": [ + "macos" + ], + "macos": { + "key": "IntelliJ IDEA", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceBracketSchemeInfo", + "description": "Editor syntax scheme bracket behavior", + "platforms": [ + "macos" + ], + "macos": { + "key": "Bracket colors follow the selected editor color scheme.", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceBracketSchemeEnabled", + "description": "Editor syntax scheme bracket behavior", + "platforms": [ + "macos" + ], + "macos": { + "key": "Enabled by color scheme", + "arguments": [] + } + }, + { + "id": "native.editorAppearanceBracketSchemeDisabled", + "description": "Editor syntax scheme bracket behavior", + "platforms": [ + "macos" + ], + "macos": { + "key": "Disabled by color scheme", + "arguments": [] + } } ] } diff --git a/shared/platform-feature-matrix/features/editor-global-appearance.json b/shared/platform-feature-matrix/features/editor-global-appearance.json new file mode 100644 index 0000000000..954bb275ca --- /dev/null +++ b/shared/platform-feature-matrix/features/editor-global-appearance.json @@ -0,0 +1,40 @@ +{ + "id": "editor-global-appearance", + "area": "编辑器", + "group": "排版", + "capability": "所有项目的源码编辑器共享字体、字号、行间距、高亮配色和常用显示偏好,修改后即时生效并持久化", + "macos": { + "implementationStatus": "implemented", + "verificationStatus": "pending", + "evidence": [ + "macos/Sources/Lithe/Models/Settings/EditorAppearanceSettings.swift", + "macos/Sources/Lithe/Models/Settings/AppSettings.swift", + "macos/Sources/Lithe/Views/App/SettingsView.swift", + "macos/Sources/Lithe/Platform/MacOS/MonacoWorkbenchEditor.swift", + "frontend/editor/src/theme.ts", + "frontend/editor/src/workbench.ts", + "macos/Tests/LitheTests/AppSettingsTests.swift", + "macos/Tests/LitheTests/MonacoWorkbenchThemeTests.swift", + "macos/Experiments/Monaco/workbench.integration.ts", + "frontend/editor/themes/vscode.json", + "frontend/editor/themes/idea.json", + "frontend/editor/src/textmate-grammar.ts", + "frontend/editor/src/semantic-tokens.ts" + ] + }, + "windows": { + "implementationStatus": "partial", + "verificationStatus": "pending", + "evidence": [ + "windows/tauri/src/features/settings/types/settings.types.ts", + "windows/tauri/src/features/settings/components/tabs/editor-settings.tsx", + "windows/tauri/src/features/editor/engines/monaco/use-monaco-editor-settings.ts", + "windows/tauri/src/extensions/themes/theme-loader.ts", + "frontend/editor/themes/github.json", + "frontend/editor/themes/one.json" + ] + }, + "owner": "Editor / Settings", + "notes": "macOS 在既有字体、字号、软换行、缩略图设置上增加独立 Lithe/GitHub/One 高亮方案、注释斜体、字体连字、行间距、绝对/相对/隐藏行号、空白符模式、缩进引导线、当前行高亮、括号着色、固定作用域标题、滚动及光标偏好。配色自动适配界面明暗但不改界面主题。行间距为字号倍数,0 保留 Monaco 自动值;新字段逐项回退,旧设置保持默认渲染。作用于主编辑器、分栏、独立文件窗口和完整源码预览。原生 Diff、SQL 控制台、终端和结果行片段保留各自排版。本次不实现外部 IDE 检测和配置导入。Windows 已有多数选项与主题体系,尚未对齐独立源码配色选择;GitHub/One JSON 仅移到共享编辑器目录,Windows 内容与行为不变。 macOS 隐藏行号仅隐藏数字,保留断点图标与空白行的可点击断点区域,已有及新建分栏一致。Git 作者栏保留相对或隐藏行号,样式变更重新安装作者栏,关闭作者栏和切换标签后恢复所选行号模式。 新增 VS Code 当前默认 Dark/Light 2026 与 IntelliJ IDEA Islands Dark/Light 代码配色,按官方固定提交映射通用词法和语义角色,保留声明/调用差异;不加载外部配置。 彩色配对括号归属语法高亮方案,移除手动开关及旧持久化覆盖;IDEA 关闭,其余内置方案启用。语法高亮分组只读显示方案状态。IDEA 的 Java 词法方法名使用正文色,语义层保留 declaration/static 标记,区分普通声明、调用与静态方法。", + "verification": "自动化:运行 AppSettingsTests、MonacoWorkbenchThemeTests、SettingsSearchVocabularyTests,以及 scripts/macos/probe/probe-macos-monaco.sh --workbench-tests,检查持久化、逐字段坏值回退、恢复默认、双外观词法/语义配色、已有与新建分栏选项、模型/光标/撤销历史保留,以及隐藏行号后的断点图标、已有断点/空白行点击与相对行号恢复。产品验收:在中文和英文设置中检索行间距、配色、空白符和光标;在深浅外观及多个项目中逐项切换,检查主编辑器、分栏、独立窗口和完整源码预览实时更新。检查 6/72 字号与 1/3 倍行间距边界、键盘选择、窄设置窗口滚动、关闭设置与重启后的恢复,以及恢复默认。完整产品 UI 验收和 Windows WebView2 验收仍待完成;组件和 WebKit 探针通过不替代这些验收。" +} diff --git a/windows/tauri/src/extensions/themes/theme-loader.ts b/windows/tauri/src/extensions/themes/theme-loader.ts index 25e220a438..7e21db149d 100644 --- a/windows/tauri/src/extensions/themes/theme-loader.ts +++ b/windows/tauri/src/extensions/themes/theme-loader.ts @@ -7,9 +7,9 @@ import catppuccinThemes from "./builtin/catppuccin.json"; import christmasThemes from "./builtin/christmas.json"; import contrastThemes from "./builtin/contrast-themes.json"; import draculaThemes from "./builtin/dracula.json"; -import githubThemes from "./builtin/github.json"; +import githubThemes from "../../../../../frontend/editor/themes/github.json"; import nordThemes from "./builtin/nord.json"; -import oneThemes from "./builtin/one.json"; +import oneThemes from "../../../../../frontend/editor/themes/one.json"; import solarizedThemes from "./builtin/solarized.json"; import { parseThemeFile, toThemeDefinition } from "./theme-file"; import type { ThemeFile } from "./theme-schema"; diff --git a/windows/tauri/src/features/editor/engines/monaco/semantic-token-provider.test.ts b/windows/tauri/src/features/editor/engines/monaco/semantic-token-provider.test.ts index 6fdea168e4..57b03afdbb 100644 --- a/windows/tauri/src/features/editor/engines/monaco/semantic-token-provider.test.ts +++ b/windows/tauri/src/features/editor/engines/monaco/semantic-token-provider.test.ts @@ -58,8 +58,8 @@ test("flushes pending text before requesting colors and maps the shared Core fix }); try { const result = (await h.provide()) as Monaco.languages.SemanticTokens; - // Server legend indices class=0/method=1 become Monaco class=2/method=13. - expect([...result.data]).toEqual([0, 6, 5, 2, 0, 2, 4, 3, 13, 0, 0, 7, 2, 13, 0]); + // Server legend indices class=0/method=1 become Monaco class=2/method=13; declaration survives as bit 2. + expect([...result.data]).toEqual([0, 6, 5, 2, 4, 2, 4, 3, 13, 0, 0, 7, 2, 13, 0]); } finally { unregister(); }