Compare commits

..
Author SHA1 Message Date
l0ng-ai c5bccf020e fix(vcs): don't commit a file that was added to the index and then deleted
A file added with git add and then deleted from disk (AD) is no change
against HEAD, so the Commit view lists it nowhere. The commit plan skipped
it, but it stayed in the index copy the commit is made from, so committing
other files also committed the deleted file. Such staged entries are now
excluded like unchecked staged files.
2026-11-12 19:15:10 +08:00
49 changed files with 49 additions and 575 deletions

No files matched your search

Generated
-11
View File
@@ -4601,7 +4601,6 @@ dependencies = [
"tree-sitter-nix",
"tree-sitter-ocaml",
"tree-sitter-php",
"tree-sitter-prisma-io",
"tree-sitter-proto",
"tree-sitter-python",
"tree-sitter-ruby",
@@ -10438,16 +10437,6 @@ dependencies = [
"tree-sitter-language",
]
[[package]]
name = "tree-sitter-prisma-io"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c35912d65a6adb07f781db46d843bf0464935e267da6ed192705516adafc7c24"
dependencies = [
"cc",
"tree-sitter-language",
]
[[package]]
name = "tree-sitter-proto"
version = "0.6.0"
+3 -38
View File
@@ -283,16 +283,6 @@ fn mssql_pool_err(e: bb8::RunError<tiberius::error::Error>) -> IdeError {
}
}
/// Tiberius requires TLS when a TLS backend is enabled. `trust_cert` only skips
/// certificate checks, so Disable has to turn encryption off.
fn apply_sqlserver_ssl(config: &mut tiberius::Config, ssl: DbSslMode) {
match ssl {
DbSslMode::Disable => config.encryption(tiberius::EncryptionLevel::NotSupported),
DbSslMode::Prefer => config.trust_cert(),
DbSslMode::Require | DbSslMode::VerifyCa | DbSslMode::VerifyFull => {}
}
}
const DAYS_1900_01_01: i64 = 693_595;
/// Formats a proleptic Gregorian date given days since `0001-01-01`.
@@ -886,7 +876,9 @@ impl Pool {
if !source.database.is_empty() {
config.database(&source.database);
}
apply_sqlserver_ssl(&mut config, source.ssl);
if matches!(source.ssl, DbSslMode::Disable | DbSslMode::Prefer) {
config.trust_cert();
}
let manager = MssqlConnectionManager(config);
let pool = bb8::Pool::builder()
.max_size(MAX)
@@ -1339,33 +1331,6 @@ mod tests {
use super::*;
use crate::source::{DbOptions, DbProperty};
fn sqlserver_debug(ssl: DbSslMode) -> String {
let mut config = tiberius::Config::new();
apply_sqlserver_ssl(&mut config, ssl);
format!("{config:?}")
}
#[test]
fn sqlserver_disable_does_not_require_tls() {
let debug = sqlserver_debug(DbSslMode::Disable);
assert!(debug.contains("encryption: NotSupported"), "{debug}");
assert!(debug.contains("bypass: false"), "{debug}");
}
#[test]
fn sqlserver_prefer_trusts_the_server_certificate() {
let debug = sqlserver_debug(DbSslMode::Prefer);
assert!(debug.contains("encryption: Required"), "{debug}");
assert!(debug.contains("bypass: true"), "{debug}");
}
#[test]
fn sqlserver_require_keeps_validated_tls() {
let debug = sqlserver_debug(DbSslMode::Require);
assert!(debug.contains("encryption: Required"), "{debug}");
assert!(debug.contains("bypass: false"), "{debug}");
}
#[test]
fn discarded_sqlserver_connections_are_broken() {
let manager = MssqlConnectionManager(tiberius::Config::new());
-1
View File
@@ -55,7 +55,6 @@ tree-sitter-xml = "0.7.0"
tree-sitter-haskell = "0.24.1"
tree-sitter-dart = "0.2.0"
tree-sitter-graphql = "0.3.0"
tree-sitter-prisma-io = "1.6.0"
tree-sitter-gdscript = "6.1.0"
[dev-dependencies]
@@ -1,40 +0,0 @@
; Prisma schema highlighting (our captures, not the grammar crate's). Later patterns win.
(identifier) @variable
(developer_comment) @comment
(comment) @comment.doc
(number) @number
(string) @string
[(true) (false) (null)] @keyword
(variable) @property
(column_declaration . (identifier) @property)
(type_expression . (identifier) @variable.parameter)
(enumeral (identifier) @constant)
(column_type (identifier) @type)
(column_type (call_expression (identifier) @type))
(call_expression (identifier) @function)
(model_declaration (identifier) @type)
(view_declaration (identifier) @type)
(type_declaration (identifier) @type)
(type_declaration (type_declaration_type) @type)
(enum_declaration (identifier) @type)
(datasource_declaration (identifier) @type)
(generator_declaration (identifier) @type)
(attribute (identifier) @attribute)
(attribute (call_expression (identifier) @attribute))
(attribute (member_expression (identifier) @attribute (property_identifier) @attribute))
(attribute (call_expression (member_expression (identifier) @attribute (property_identifier) @attribute)))
(block_attribute_declaration (identifier) @attribute)
(block_attribute_declaration (call_expression (identifier) @attribute))
["@" "@@"] @attribute
["datasource" "enum" "generator" "model" "type" "view"] @keyword
["(" ")" "[" "]" "{" "}"] @punctuation.bracket
["," ":" "."] @punctuation.delimiter
(maybe) @operator
"=" @operator
@@ -1,8 +0,0 @@
(model_declaration (identifier) @name) @def.class
(view_declaration (identifier) @name) @def.class
(type_declaration . (identifier) @name (statement_block)) @def.struct
(enum_declaration (identifier) @name) @def.enum
(enumeral (identifier) @name) @def.enum_member
(datasource_declaration (identifier) @name) @outline.module
(generator_declaration (identifier) @name) @outline.module
(column_declaration . (identifier) @name) @def.field
-2
View File
@@ -67,7 +67,6 @@ const BUILTIN: &[(&str, &str)] = &[
("php", include_str!("../../../languages/php.toml")),
("plaintext", include_str!("../../../languages/plaintext.toml")),
("powershell", include_str!("../../../languages/powershell.toml")),
("prisma", include_str!("../../../languages/prisma.toml")),
("properties", include_str!("../../../languages/properties.toml")),
("proto", include_str!("../../../languages/proto.toml")),
("python", include_str!("../../../languages/python.toml")),
@@ -1027,7 +1026,6 @@ mod tests {
("CMakeLists.txt", "cmake"),
("api.proto", "proto"),
("schema.graphql", "graphql"),
("schema.prisma", "prisma"),
("main.tf", "terraform"),
("prod.tfvars", "terraform-vars"),
("job.nomad", "hcl"),
@@ -430,16 +430,6 @@ pub(crate) static SPECS: &[Spec] = &[
imports: NONE,
constructors: NONE,
},
Spec {
name: "prisma",
language: tree_sitter_prisma_io::LANGUAGE,
outline: outline!("prisma"),
highlights: Some(Highlights { query: highlights!("prisma"), first_wins: false }),
fold_nodes: NONE,
fold_bodies: NONE,
imports: NONE,
constructors: NONE,
},
Spec {
name: "make",
language: tree_sitter_make::LANGUAGE,
@@ -501,28 +501,6 @@ mod tests {
("NEWHOPE", K::Constant),
],
),
(
"prisma",
"// hi\n/// Doc\nmodel User {\n id Int @id @default(autoincrement())\n email String? @db.VarChar(255)\n role Role\n @@index([email])\n}\nenum Role { ADMIN }\ndatasource db {\n provider = \"postgresql\"\n}\n",
&[
("// hi", K::Comment),
("/// Doc", K::DocComment),
("model", K::Keyword),
("User", K::Type),
("id", K::Property),
("Int", K::Type),
("@id", K::Attribute),
("@default", K::Attribute),
("autoincrement", K::FunctionCall),
("VarChar", K::Attribute),
("255", K::Number),
("Role", K::Type),
("@@index", K::Attribute),
("ADMIN", K::Constant),
("provider", K::Property),
("\"postgresql\"", K::String),
],
),
(
"hcl",
"# hi\nresource \"aws_instance\" \"web\" {\n ami = var.ami\n count = 2\n tags = { Name = \"web-${var.env}\" }\n name = upper(\"x\")\n}\n",
-11
View File
@@ -886,17 +886,6 @@ fn outline_more_languages() {
Function GetUser
",
);
check_outline(
"prisma",
"datasource db {\n provider = \"sqlite\"\n}\nmodel User {\n id Int @id\n @@map(\"users\")\n}\nenum Role {\n ADMIN\n}\n",
"
Module db
Class User
Field id
Enum Role
EnumMember ADMIN
",
);
check_outline(
"nix",
"{ pkgs, ... }:\nlet\n foo = 1;\n bar = x: x;\nin { inherit foo; }\n",
-11
View File
@@ -407,17 +407,6 @@ pub static TOOLS: &[ToolSpec] = &[
homepage: "https://github.com/graphql/graphiql",
needs: &[],
},
ToolSpec {
name: "prisma-language-server",
display: "Prisma Language Server",
kind: LanguageServer,
commands: &["prisma-language-server"],
method: Method::Npm { packages: &["@prisma/language-server"] },
fallback: None,
version: V,
homepage: "https://github.com/prisma/language-tools",
needs: &[],
},
ToolSpec {
name: "intelephense",
display: "Intelephense (PHP)",
+1 -1
View File
@@ -67,7 +67,7 @@ All keys are kebab-case. Only `id` and `name` are required; a definition with no
| `formatter` | table | External formatter; see below. |
| `scratch-template` | string | Content of a new scratch file of this language. `$END$` marks where the caret goes. |
Bundled tree-sitter grammars: `bash`, `c`, `cmake`, `cpp`, `csharp`, `css`, `dart`, `elixir`, `gdscript`, `go`, `graphql`, `haskell`, `hcl`, `html`, `java`, `javascript`, `json`, `kotlin`, `lua`, `make`, `markdown`, `nix`, `ocaml`, `ocaml_interface`, `php`, `prisma`, `proto`, `python`, `ruby`, `rust`, `scala`, `swift`, `toml`, `tsx`, `typescript`, `xml`, `yaml`, `zig`. A new grammar has to be compiled into the IDE; a plugin can provide highlighting and structure for a language without a grammar instead (see [Language features](/en/plugins/guides/language-features)).
Bundled tree-sitter grammars: `bash`, `c`, `cmake`, `cpp`, `csharp`, `css`, `dart`, `elixir`, `gdscript`, `go`, `graphql`, `haskell`, `hcl`, `html`, `java`, `javascript`, `json`, `kotlin`, `lua`, `make`, `markdown`, `nix`, `ocaml`, `ocaml_interface`, `php`, `proto`, `python`, `ruby`, `rust`, `scala`, `swift`, `toml`, `tsx`, `typescript`, `xml`, `yaml`, `zig`. A new grammar has to be compiled into the IDE; a plugin can provide highlighting and structure for a language without a grammar instead (see [Language features](/en/plugins/guides/language-features)).
### `[[framework-ui-modes]]`
-1
View File
@@ -151,7 +151,6 @@ The table lists every built-in language definition. Server and debugger names in
| PHP | `php` | `.php`, `.phtml`, `.php3`, `.php4`, `.php5`, … | Yes | Yes | **intelephense** → phpactor + **laravel-lsp** | **php-debug** |
| Plain Text | `plaintext` | `.txt`, `.text` | — | — | — | — |
| PowerShell | `powershell` | `.ps1`, `.psm1`, `.psd1`, `.pssc`, `.psrc` | Yes | — | powershell-editor-services | — |
| Prisma | `prisma` | `.prisma` | Yes | Yes | **prisma-language-server** | — |
| Properties | `properties` | `.properties`, `.prefs` | Yes | — | — | — |
| Protocol Buffers | `proto` | `.proto` | Yes | Yes | **buf** | — |
| Python | `python` | `.py`, `.pyi`, `.pyw` | Yes | Yes | **basedpyright** → **pyright** → **ty** → **pyrefly** + **ruff** | **debugpy** |
+1 -2
View File
@@ -238,7 +238,7 @@ The full table is `UI_KEYS` in
| `--syn-rainbow-1` … `--syn-rainbow-5` (Semantic highlighting) | `RAINBOW_COLOR1` … `RAINBOW_COLOR5` |
| `--wave-error`, `--wave-warning` | `ERRORS_ATTRIBUTES`, `WARNING_ATTRIBUTES` (effect colour) |
| `--bg-matched-brace`, `--bg-identifier-under-caret` | `MATCHED_BRACE_ATTRIBUTES`, `IDENTIFIER_UNDER_CARET_ATTRIBUTES` |
| `--bg-text-search-result`, `--bg-search-result` | `TEXT_SEARCH_RESULT_ATTRIBUTES`, `SEARCH_RESULT_ATTRIBUTES` |
| `--bg-text-search-result`, `--bg-search-match` | `TEXT_SEARCH_RESULT_ATTRIBUTES`, `SEARCH_RESULT_ATTRIBUTES` |
| `--syn-self`, `--syn-keyword-arg`, `--syn-docstring`, `--syn-predefined` | `PY.SELF_PARAMETER`, `PY.KEYWORD_ARGUMENT`, `PY.DOC_COMMENT`, `PY.BUILTIN_NAME` |
| `--ansi-*` | `CONSOLE_*_OUTPUT` (`CONSOLE_DARKGRAY_OUTPUT` is bright black, `CONSOLE_GRAY_OUTPUT` white) |
@@ -504,7 +504,6 @@ conventionally go in the theme file, groups marked _editor scheme_ in the scheme
| `--fg-editor-selection` | `inherit` | `inherit` |
| `--bg-text-search-result` | `#114957` | `#fcd47e` |
| `--bg-text-search-result-current` | `#165e70` | `#f2bf57` |
| `--bg-search-result` | `#2d543f` | `#ccccff` |
| `--bg-matched-brace` | `#43454a` | `#93d9d9` |
| `--matched-brace-weight` | `var(--fw-code-bold)` | `inherit` |
| `--bg-matched-tag` | `#3f4045` | `#c9ecec` |
+1 -1
View File
@@ -67,7 +67,7 @@ install-hint = "Install CodeLLDB in Settings › Languages & Frameworks › Lang
| `formatter` | 表 | 外部格式化工具,见下文。 |
| `scratch-template` | 字符串 | 新建该语言草稿文件时的内容,`$END$` 表示光标位置。 |
内置的 tree-sitter 语法:`bash`、`c`、`cmake`、`cpp`、`csharp`、`css`、`dart`、`elixir`、`gdscript`、`go`、`graphql`、`haskell`、`hcl`、`html`、`java`、`javascript`、`json`、`kotlin`、`lua`、`make`、`markdown`、`nix`、`ocaml`、`ocaml_interface`、`php`、`prisma`、`proto`、`python`、`ruby`、`rust`、`scala`、`swift`、`toml`、`tsx`、`typescript`、`xml`、`yaml`、`zig`。新的语法需要编译进 IDE;没有语法的语言可以改由插件提供高亮和结构(见 [语言功能](/zh/plugins/guides/language-features))。
内置的 tree-sitter 语法:`bash`、`c`、`cmake`、`cpp`、`csharp`、`css`、`dart`、`elixir`、`gdscript`、`go`、`graphql`、`haskell`、`hcl`、`html`、`java`、`javascript`、`json`、`kotlin`、`lua`、`make`、`markdown`、`nix`、`ocaml`、`ocaml_interface`、`php`、`proto`、`python`、`ruby`、`rust`、`scala`、`swift`、`toml`、`tsx`、`typescript`、`xml`、`yaml`、`zig`。新的语法需要编译进 IDE;没有语法的语言可以改由插件提供高亮和结构(见 [语言功能](/zh/plugins/guides/language-features))。
### `[[framework-ui-modes]]`
-1
View File
@@ -146,7 +146,6 @@ IDE 安装工具从不需要 `sudo`。工具装在本地数据目录下 IDE 自
| PHP | `php` | `.php`, `.phtml`, `.php3`, `.php4`, `.php5`, … | 是 | 是 | **intelephense** → phpactor + **laravel-lsp** | **php-debug** |
| Plain Text | `plaintext` | `.txt`, `.text` | — | — | — | — |
| PowerShell | `powershell` | `.ps1`, `.psm1`, `.psd1`, `.pssc`, `.psrc` | 是 | — | powershell-editor-services | — |
| Prisma | `prisma` | `.prisma` | 是 | 是 | **prisma-language-server** | — |
| Properties | `properties` | `.properties`, `.prefs` | 是 | — | — | — |
| Protocol Buffers | `proto` | `.proto` | 是 | 是 | **buf** | — |
| Python | `python` | `.py`, `.pyi`, `.pyw` | 是 | 是 | **basedpyright** → **pyright** → **ty** → **pyrefly** + **ruff** | **debugpy** |
+1 -2
View File
@@ -210,7 +210,7 @@ pnpm theme:import-intellij path/to/MyTheme.theme.json path/to/MyTheme.icls --out
| `--syn-rainbow-1` … `--syn-rainbow-5`(语义高亮) | `RAINBOW_COLOR1` … `RAINBOW_COLOR5` |
| `--wave-error`, `--wave-warning` | `ERRORS_ATTRIBUTES`, `WARNING_ATTRIBUTES`(效果颜色) |
| `--bg-matched-brace`, `--bg-identifier-under-caret` | `MATCHED_BRACE_ATTRIBUTES`, `IDENTIFIER_UNDER_CARET_ATTRIBUTES` |
| `--bg-text-search-result`, `--bg-search-result` | `TEXT_SEARCH_RESULT_ATTRIBUTES`, `SEARCH_RESULT_ATTRIBUTES` |
| `--bg-text-search-result`, `--bg-search-match` | `TEXT_SEARCH_RESULT_ATTRIBUTES`, `SEARCH_RESULT_ATTRIBUTES` |
| `--syn-self`, `--syn-keyword-arg`, `--syn-docstring`, `--syn-predefined` | `PY.SELF_PARAMETER`, `PY.KEYWORD_ARGUMENT`, `PY.DOC_COMMENT`, `PY.BUILTIN_NAME` |
| `--ansi-*` | `CONSOLE_*_OUTPUT`(`CONSOLE_DARKGRAY_OUTPUT` 为亮黑,`CONSOLE_GRAY_OUTPUT` 为白色) |
@@ -469,7 +469,6 @@ VS Code 主题(扩展中的 `themes/*.json`)包含 `type`(`dark` / `light`
| `--fg-editor-selection` | `inherit` | `inherit` |
| `--bg-text-search-result` | `#114957` | `#fcd47e` |
| `--bg-text-search-result-current` | `#165e70` | `#f2bf57` |
| `--bg-search-result` | `#2d543f` | `#ccccff` |
| `--bg-matched-brace` | `#43454a` | `#93d9d9` |
| `--matched-brace-weight` | `var(--fw-code-bold)` | `inherit` |
| `--bg-matched-tag` | `#3f4045` | `#c9ecec` |
-47
View File
@@ -4,7 +4,6 @@
* by `crates/ide-lsp/tests`.
*/
import type { Locator, Page } from "@playwright/test";
import type { CompletionItem } from "../src/bindings";
import { expect, ipcCalls, openFromTree, stubDesktop, test } from "./fixtures";
const ROOT = "/dev/acme-api";
@@ -79,52 +78,6 @@ test("completion lists the server's items and inserts the chosen one", async ({
expect((await ipcCalls(page, "lsp_completion")).length).toBeGreaterThan(0);
});
for (const key of ["Enter", "Tab"]) {
for (const prefix of ["", "v"]) {
test(`${key} leaves the caret after a field snippet at root.${prefix}`, async ({ workbench: page }, info) => {
const editor = await openSample(page);
await page.evaluate(() => {
const item = (label: string): CompletionItem => ({
label,
labelDetail: null,
labelDescription: "int",
detail: "int",
kind: "field",
documentation: null,
sortText: null,
filterText: null,
preselect: label === "val",
deprecated: false,
insertText: label,
isSnippet: true,
replaceRange: null,
replaceRangeTab: null,
additionalEdits: [],
commitCharacters: [],
command: null,
data: null,
server: 1,
});
window.__ipc.handlers.lsp_completion = () => ({ isIncomplete: false, items: [item("val"), item("value")] });
});
await editor.locator(".cm-line", { hasText: "return order.toDto()" }).click();
await page.keyboard.press("End");
await page.keyboard.press("Enter");
await page.keyboard.type(`int a = root.${prefix}`);
await page.keyboard.press("Control+Space");
const popup = page.locator(".cm-tooltip-autocomplete");
await expect(popup.getByText("val", { exact: true })).toBeVisible();
// CodeMirror ignores acceptance during its 75 ms lookup interaction delay.
await page.waitForTimeout(100);
await page.keyboard.press(key);
await expect(popup).toBeHidden();
await page.screenshot({ path: info.outputPath("accepted-completion.png"), animations: "disabled" });
await page.keyboard.type(";");
await expect(editor.locator(".cm-activeLine")).toHaveText(/^\s*int a = root\.val;$/);
});
}
}
test("⌘B goes to the declaration in another file", async ({ workbench: page }) => {
const editor = await openSample(page);
await page.evaluate(
-14
View File
@@ -105,20 +105,6 @@ test("Find in Files streams matches by file and opens the chosen one at its line
});
const preview = dialog.getByRole("region", { name: "Preview", exact: true }).locator(".cm-content");
await expect(preview).toHaveAttribute("contenteditable", "true");
// Matches take the editor scheme's search result background (IDEA's SEARCH_RESULT_ATTRIBUTES) and keep the syntax colours.
const match = preview.locator(".cm-openidea-searchResult").first();
await expect(match).toHaveText("OrderService");
expect(
await match.evaluate((el) => {
const probe = document.createElement("span");
probe.style.backgroundColor = "var(--bg-search-result)";
el.parentElement!.append(probe);
const expected = getComputedStyle(probe).backgroundColor;
probe.remove();
const own = getComputedStyle(el);
return own.backgroundColor === expected && own.color === getComputedStyle(el.parentElement!).color;
}),
).toBe(true);
const originalPreview = await preview.textContent();
await preview.focus();
await page.keyboard.press("Meta+a");
-2
View File
@@ -41,5 +41,3 @@ nodePath = ""
problems = { shortenToSingleLine = false }
workingDirectory = { mode = "auto" }
codeAction = { disableRuleComment = { enable = true, location = "separateLine" }, showDocumentation = { enable = true } }
# vscode-eslint reads it for every source.fixAll request (Actions on Save, Code Cleanup).
codeActionOnSave = { mode = "all", rules = [] }
-2
View File
@@ -75,8 +75,6 @@ nodePath = ""
problems = { shortenToSingleLine = false }
workingDirectory = { mode = "auto" }
codeAction = { disableRuleComment = { enable = true, location = "separateLine" }, showDocumentation = { enable = true } }
# vscode-eslint reads it for every source.fixAll request (Actions on Save, Code Cleanup).
codeActionOnSave = { mode = "all", rules = [] }
[debugger]
adapter = "js-debug"
-2
View File
@@ -74,8 +74,6 @@ nodePath = ""
problems = { shortenToSingleLine = false }
workingDirectory = { mode = "auto" }
codeAction = { disableRuleComment = { enable = true, location = "separateLine" }, showDocumentation = { enable = true } }
# vscode-eslint reads it for every source.fixAll request (Actions on Save, Code Cleanup).
codeActionOnSave = { mode = "all", rules = [] }
[debugger]
adapter = "js-debug"
-17
View File
@@ -1,17 +0,0 @@
id = "prisma"
name = "Prisma"
extensions = ["prisma"]
line-comment = "//"
indent = { tab-size = 2, insert-spaces = true }
tree-sitter = "prisma"
[[language-servers]]
name = "prisma-language-server"
command = "prisma-language-server"
args = ["--stdio"]
root-markers = ["package.json", "schema.prisma"]
install-hint = "npm i -g @prisma/language-server"
# Asked for as section `prisma` before every validation; a missing section crashes the server.
[language-servers.settings.prisma]
enableDiagnostics = true
-2
View File
@@ -43,5 +43,3 @@ nodePath = ""
problems = { shortenToSingleLine = false }
workingDirectory = { mode = "auto" }
codeAction = { disableRuleComment = { enable = true, location = "separateLine" }, showDocumentation = { enable = true } }
# vscode-eslint reads it for every source.fixAll request (Actions on Save, Code Cleanup).
codeActionOnSave = { mode = "all", rules = [] }
-2
View File
@@ -89,8 +89,6 @@ nodePath = ""
problems = { shortenToSingleLine = false }
workingDirectory = { mode = "auto" }
codeAction = { disableRuleComment = { enable = true, location = "separateLine" }, showDocumentation = { enable = true } }
# vscode-eslint reads it for every source.fixAll request (Actions on Save, Code Cleanup).
codeActionOnSave = { mode = "all", rules = [] }
[debugger]
adapter = "js-debug"
-2
View File
@@ -74,8 +74,6 @@ nodePath = ""
problems = { shortenToSingleLine = false }
workingDirectory = { mode = "auto" }
codeAction = { disableRuleComment = { enable = true, location = "separateLine" }, showDocumentation = { enable = true } }
# vscode-eslint reads it for every source.fixAll request (Actions on Save, Code Cleanup).
codeActionOnSave = { mode = "all", rules = [] }
[debugger]
adapter = "js-debug"
-2
View File
@@ -68,5 +68,3 @@ nodePath = ""
problems = { shortenToSingleLine = false }
workingDirectory = { mode = "auto" }
codeAction = { disableRuleComment = { enable = true, location = "separateLine" }, showDocumentation = { enable = true } }
# vscode-eslint reads it for every source.fixAll request (Actions on Save, Code Cleanup).
codeActionOnSave = { mode = "all", rules = [] }
@@ -89,7 +89,6 @@
"--fg-editor-selection": "inherit",
"--bg-text-search-result": "#114957",
"--bg-text-search-result-current": "#165e70",
"--bg-search-result": "#2d543f",
"--bg-matched-brace": "#38424f",
"--matched-brace-weight": "700",
"--bg-matched-tag": "#353e4a",
+2 -3
View File
@@ -231,7 +231,7 @@ The full table is `UI_KEYS` in [`src/features/plugins/intellijTheme.ts`](../src/
| `--syn-rainbow-1` … `--syn-rainbow-5` (Semantic highlighting, rainbow indent guides) | `RAINBOW_COLOR1` … `RAINBOW_COLOR5` |
| `--wave-error`, `--wave-warning` | `ERRORS_ATTRIBUTES`, `WARNING_ATTRIBUTES` (effect colour) |
| `--bg-matched-brace`, `--bg-identifier-under-caret` | `MATCHED_BRACE_ATTRIBUTES`, `IDENTIFIER_UNDER_CARET_ATTRIBUTES` |
| `--bg-text-search-result`, `--bg-search-result` | `TEXT_SEARCH_RESULT_ATTRIBUTES`, `SEARCH_RESULT_ATTRIBUTES` |
| `--bg-text-search-result`, `--bg-search-match` | `TEXT_SEARCH_RESULT_ATTRIBUTES`, `SEARCH_RESULT_ATTRIBUTES` |
| `--syn-self`, `--syn-keyword-arg`, `--syn-docstring`, `--syn-predefined` | `PY.SELF_PARAMETER`, `PY.KEYWORD_ARGUMENT`, `PY.DOC_COMMENT`, `PY.BUILTIN_NAME` |
| `--ansi-*` | `CONSOLE_*_OUTPUT` (`CONSOLE_DARKGRAY_OUTPUT` is bright black, `CONSOLE_GRAY_OUTPUT` white) |
@@ -329,7 +329,7 @@ a token.
<!-- BEGIN GENERATED TOKEN REFERENCE: pnpm gen:theme-docs -->
264 themeable tokens: 125 in the UI theme file, 139 in the editor colour scheme file.
263 themeable tokens: 125 in the UI theme file, 138 in the editor colour scheme file.
### Surfaces (UI theme)
@@ -509,7 +509,6 @@ a token.
| `--fg-editor-selection` | `inherit` | `inherit` |
| `--bg-text-search-result` | `#114957` | `#fcd47e` |
| `--bg-text-search-result-current` | `#165e70` | `#f2bf57` |
| `--bg-search-result` | `#2d543f` | `#ccccff` |
| `--bg-matched-brace` | `#43454a` | `#93d9d9` |
| `--matched-brace-weight` | `var(--fw-code-bold)` | `inherit` |
| `--bg-matched-tag` | `#3f4045` | `#c9ecec` |
-10
View File
@@ -219,16 +219,6 @@ describe("IntelliJ theme import", () => {
});
});
it("keeps the search result colour apart from the text search result and the lists' search match", () => {
const bg = (c: string) => `<value><option name="BACKGROUND" value="${c}"/></value>`;
const s = convertIcls(`<scheme name="Search" parent_scheme="Darcula"><attributes>
<option name="SEARCH_RESULT_ATTRIBUTES">${bg("2d543f")}</option>
<option name="TEXT_SEARCH_RESULT_ATTRIBUTES">${bg("114957")}</option>
</attributes></scheme>`);
expect(s.tokens).toMatchObject({ "--bg-search-result": "#2d543f", "--bg-text-search-result": "#114957" });
expect(s.tokens).not.toHaveProperty("--bg-search-match");
});
it("produces an installable package", () => {
const light = stripJsonComments(themeJson.replace('"dark": true,', ""));
const c = convertIntellijTheme(light, icls.replace("Darcula", "Default"), "sea");
-2
View File
@@ -332,8 +332,6 @@ export const openideaTheme = EditorView.theme({
// Find in file (cm/find.ts)
...findStyles,
// Find in Files preview matches (search/find/parts.tsx)
".cm-openidea-searchResult": { backgroundColor: "var(--bg-search-result)" },
// Overlays
".cm-tooltip": {
-38
View File
@@ -318,44 +318,6 @@ describe("applying completions", () => {
});
for (const mode of ["insert", "replace"] as const) {
for (const prefix of ["", "v"]) {
it(`${mode} leaves the caret after a field snippet accepted at root.${prefix}`, () => {
const { view, req, pos } = setup(`int a = root.${prefix}|Suffix`);
const start = "int a = root.".length;
const c = makeCompletion(
item({
label: "val",
kind: "field",
insertText: "val",
isSnippet: true,
replaceRange: { start: { line: 0, col: start }, end: { line: 0, col: pos } },
}),
req,
);
applyLspCompletion(view, c, start, pos, mode);
expect(view.state.doc.toString()).toBe(`int a = root.val${mode === "insert" ? "Suffix" : ""}`);
expect(view.state.selection.main.head).toBe(start + 3);
});
}
it(`${mode} places a fieldless snippet after imports and a cached UTF-16 prefix`, () => {
const { view, req, pos } = setup("\n/* 😀 */ root.|Suffix", "v");
const c = makeCompletion(
item({
label: "val",
kind: "field",
insertText: "val",
isSnippet: true,
replaceRange: { start: { line: 1, col: pos - 1 }, end: { line: 1, col: pos - 1 } },
additionalEdits: [{ range: { start: { line: 0, col: 0 }, end: { line: 0, col: 0 } }, new_text: "import X;" }],
}),
req,
);
applyLspCompletion(view, c, pos, pos + 1, mode);
expect(view.state.doc.toString()).toBe(`import X;\n/* 😀 */ root.val${mode === "insert" ? "Suffix" : ""}`);
expect(view.state.selection.main.head).toBe(pos + "import X;val".length);
});
for (const shape of ["textEdit", "insertReplace", "noEdit"] as const) {
for (const isSnippet of [false, true]) {
it(`${mode} replaces a mixed-case prefix typed after a ${shape} request (snippet: ${isSnippet})`, () => {
-57
View File
@@ -146,63 +146,6 @@ describe("silent code actions", () => {
});
});
describe("fix-all from every server", () => {
const fixAll = (server: number, title: string): CodeAction => ({
...resolved,
title,
kind: "source.fixAll",
server,
edit: null,
data: "{}",
});
const editor = () => {
const view = new EditorView({
state: EditorState.create({
doc: "const a = 'x';",
extensions: [lspDoc.of({ doc: 1, path: "/p/a.ts", language: "typescript", uiMode: null, readOnly: false })],
}),
});
views.push(view);
return view;
};
it("applies each server's source.fixAll in turn, asking the next one again after an edit", async () => {
const view = editor();
const ts = fixAll(1, "Fix all fixable JS/TS issues");
const eslint = fixAll(2, "Fix all fixable ESLint issues");
const eslintAfter = { ...eslint, data: '{"version":2}' };
lspCodeActions
.mockResolvedValueOnce({ status: "ok", data: [ts, eslint] })
.mockResolvedValueOnce({ status: "ok", data: [{ ...ts, data: '{"again":true}' }, eslintAfter] });
lspApplyCodeAction.mockResolvedValue({ status: "ok", data: null });
expect(await applyKindSilently(view, "source.fixAll")).toBe(true);
expect(lspCodeActions).toHaveBeenCalledTimes(2);
expect(lspApplyCodeAction.mock.calls.map((c) => (c as unknown[])[1])).toEqual([ts, eslintAfter]);
});
it("goes on with the next server when one server's action fails", async () => {
const view = editor();
const ts = fixAll(1, "Fix all fixable JS/TS issues");
const eslint = fixAll(2, "Fix all fixable ESLint issues");
lspCodeActions.mockResolvedValue({ status: "ok", data: [ts, eslint] });
lspApplyCodeAction
.mockResolvedValueOnce({ status: "error", error: { kind: "internal", message: "boom" } })
.mockResolvedValueOnce({ status: "ok", data: null });
expect(await applyKindSilently(view, "source.fixAll")).toBe(true);
expect(lspApplyCodeAction.mock.calls.map((c) => ((c as unknown[])[1] as CodeAction).server)).toEqual([1, 2]);
});
it("applies a single organize-imports action", async () => {
const view = editor();
const a = { ...fixAll(1, "Organize Imports"), kind: "source.organizeImports" };
const b = { ...fixAll(2, "Organize Imports"), kind: "source.organizeImports" };
lspCodeActions.mockResolvedValueOnce({ status: "ok", data: [a, b] });
lspApplyCodeAction.mockResolvedValue({ status: "ok", data: null });
expect(await applyKindSilently(view, "source.organizeImports")).toBe(true);
expect(lspApplyCodeAction).toHaveBeenCalledTimes(1);
});
});
describe("intention preview", () => {
it("does not resolve lazy plugin actions while previewing", async () => {
const view = new EditorView({
+13 -47
View File
@@ -299,61 +299,27 @@ export function changedLines(before: Text, after: Text): Array<{ from: number; t
* Applies the first enabled whole-file action of `kind` without UI (Actions on
* Save). Waits until the backend edit reached this editor; false when none.
*/
/**
* Applies the server's `kind` action to the whole document without UI (Actions on Save,
* Code Cleanup). Fix-all runs once per server that offers it, one after another, each
* asked again for the document as the previous one left it (VS Code's codeActionsOnSave):
* vtsls and ESLint both answer `source.fixAll` for a TypeScript file.
*/
export async function applyKindSilently(view: EditorView, kind: string): Promise<boolean> {
const doc = docOf(view);
if (!doc || doc.readOnly) return false;
const matching = (actions: CodeAction[], server?: CodeAction["server"]) =>
actions.filter(
(a) =>
a.kind?.startsWith(kind) &&
!a.disabledReason &&
!isLazyPluginAction(a) &&
(server === undefined || a.server === server),
);
const pick = (actions: CodeAction[]) => actions.find((a) => a.isPreferred) ?? actions[0];
let actions: CodeAction[];
try {
actions = matching(await fetchActions(view, [kind], true));
} catch (e) {
console.warn(`[lsp] ${kind} on ${doc.path} failed`, e);
actions = (await fetchActions(view, [kind], true)).filter(
(a) => a.kind?.startsWith(kind) && !a.disabledReason && !isLazyPluginAction(a),
);
} catch {
return false;
}
const first = pick(actions);
if (!first) return false;
const servers = kind.startsWith("source.fixAll")
? [first.server, ...new Set(actions.map((a) => a.server).filter((s) => s !== first.server))]
: [first.server];
let applied = false;
for (const [i, server] of servers.entries()) {
let action: CodeAction | undefined = first;
if (i > 0) {
// The previous server's edit changed the document: this one's answer must be fresh.
try {
action = pick(matching(await fetchActions(view, [kind], true), server));
} catch (e) {
console.warn(`[lsp] ${kind} on ${doc.path} failed`, e);
continue;
}
if (!action) continue;
}
const before = view.state.doc;
const chosen = action;
try {
await lspRequest(() => commands.lspApplyCodeAction(doc.doc, chosen));
} catch (e) {
console.warn(`[lsp] applying ${chosen.kind} on ${doc.path} failed`, e);
continue;
}
applied = true;
for (let i = 0; i < 25 && view.state.doc === before; i++) await new Promise((r) => setTimeout(r, 20));
const action = actions.find((a) => a.isPreferred) ?? actions[0];
if (!action) return false;
const before = view.state.doc;
try {
await lspRequest(() => commands.lspApplyCodeAction(doc.doc, action));
} catch {
return false;
}
return applied;
for (let i = 0; i < 25 && view.state.doc === before; i++) await new Promise((r) => setTimeout(r, 20));
return true;
}
/**
+4 -29
View File
@@ -23,7 +23,7 @@ describe("lspSnippetToCm", () => {
});
it("handles LSP escapes", () => {
expect(lspSnippetToCm("cost \\$5 \\} \\\\")).toBe("cost $5 \\} \\" + "${0}");
expect(lspSnippetToCm("cost \\$5 \\} \\\\")).toBe("cost $5 \\} \\");
expect(expand(lspSnippetToCm("a \\${1}")).text).toBe("a ${1}");
});
@@ -37,8 +37,8 @@ describe("lspSnippetToCm", () => {
it("resolves variables, defaults and unknown names", () => {
const vars = snippetVariables("/p/src/Main.kt");
expect(lspSnippetToCm("class $TM_FILENAME_BASE", vars)).toBe("class Main${0}");
expect(lspSnippetToCm("${NOPE:dflt}", vars)).toBe("dflt${0}");
expect(lspSnippetToCm("class $TM_FILENAME_BASE", vars)).toBe("class Main");
expect(lspSnippetToCm("${NOPE:dflt}", vars)).toBe("dflt");
expect(lspSnippetToCm("$NOPE", vars)).toBe("${NOPE}");
});
@@ -51,7 +51,7 @@ describe("lspSnippetToCm", () => {
});
it("treats a lone dollar as text", () => {
expect(lspSnippetToCm("$ x $")).toBe("$ x $${0}");
expect(lspSnippetToCm("$ x $")).toBe("$ x $");
});
it("selects the first field when instantiated", () => {
@@ -60,31 +60,6 @@ describe("lspSnippetToCm", () => {
expect([sel.from, sel.to]).toEqual([5, 8]);
});
it.each([
["val", {}, "val"],
["$TM_FILENAME_BASE", { TM_FILENAME_BASE: "Main" }, "Main"],
["${MISSING:val}", {}, "val"],
["\\${1}", {}, "${1}"],
["cost \\$5", {}, "cost $5"],
["first\n\tlast", {}, "first\n last"],
["/* 😀 */ val", {}, "/* 😀 */ val"],
])("places the caret at the end of a fieldless snippet (%s)", (source, vars, expected) => {
const { text, sel } = expand(lspSnippetToCm(source, vars));
expect(text).toBe(expected);
expect([sel.from, sel.to]).toEqual([expected.length, expected.length]);
});
it.each([
["before$0after", "beforeafter", 6, 6],
["${1:arg}after", "argafter", 0, 3],
["${1|val,other|}after", "valafter", 0, 3],
["$UNKNOWN after", "UNKNOWN after", 0, 7],
])("preserves the selected field in %s", (source, expected, from, to) => {
const { text, sel } = expand(lspSnippetToCm(source));
expect(text).toBe(expected);
expect([sel.from, sel.to]).toEqual([from, to]);
});
it("produces plain text previews", () => {
expect(lspSnippetToPlain("foo(${1:a}, ${2|x,y|})$0")).toBe("foo(a, x)");
});
+2 -7
View File
@@ -189,12 +189,8 @@ function field(n: number | string, text: string): string {
/** Converts an LSP snippet into a CodeMirror snippet template. */
export function lspSnippetToCm(snippet: string, vars: SnippetVariables = {}): string {
const nodes = parse(snippet);
const hasField = nodes.some(
(n) => n.t === "tab" || n.t === "choice" || (n.t === "var" && vars[n.name] === undefined && !n.children),
);
let out = "";
for (const n of nodes) {
for (const n of parse(snippet)) {
if (n.t === "text") out += escapeText(n.v);
else if (n.t === "tab") out += field(n.n, n.children ? flatten(n.children, vars) : "");
else if (n.t === "choice") out += field(n.n, n.options[0] ?? "");
@@ -206,8 +202,7 @@ export function lspSnippetToCm(snippet: string, vars: SnippetVariables = {}): st
else out += field(n.name, "");
}
}
// CodeMirror leaves the caret before an insertion without fields.
return hasField ? out : out + "${0}";
return out;
}
/** The snippet's text with every field replaced by its default (for previews / plain insertion). */
+1 -1
View File
@@ -343,7 +343,7 @@ export function sqlCandidates({ model, analysis: a, driver, sourceName }: Comple
kind: "function",
tail: `(${f.params})`,
type: f.returns?.toUpperCase(),
snippet: f.params ? `${name}(\${})` : `${name}()\${0}`,
snippet: f.params ? `${name}(\${})` : `${name}()`,
boost: f.aggregate && clause !== "select" && clause !== "having" ? fnBoost - 4 : fnBoost,
});
}
-16
View File
@@ -1,6 +1,4 @@
import { describe, expect, it } from "vitest";
import { EditorState } from "@codemirror/state";
import { snippet } from "@codemirror/autocomplete";
import type { DbCompletion, DbDriver } from "@/platform/ipc";
import { analyze, templateContexts, valuesSlot } from "./analyze";
import { aliasFor, quoteIdent, sqlCandidates, type SqlCandidate } from "./complete";
@@ -329,20 +327,6 @@ describe("completion candidates", () => {
expect(c.find((x) => x.label === "count")).toMatchObject({ snippet: "count(${})" });
});
it.each(["sqlite", "postgres", "mysql"] as const)(
"leaves the caret after a %s function without parameters",
(driver) => {
const candidates = complete("select |", driver).filter((c) => c.kind === "function" && c.tail === "()");
expect(candidates.length).toBeGreaterThan(0);
for (const candidate of candidates) {
let state = EditorState.create({ doc: "select ", selection: { anchor: 7 } });
snippet(candidate.snippet!)({ state, dispatch: (tr) => (state = tr.state) }, null, 7, 7);
expect(state.doc.toString()).toBe(`select ${candidate.label}()`);
expect(state.selection.main.head).toBe(state.doc.length);
}
},
);
it("quotes identifiers that need it", () => {
expect(quoteIdent("Users", "postgres")).toBe('"Users"');
expect(quoteIdent("Users", "mysql")).toBe("Users");
+1 -1
View File
@@ -172,7 +172,7 @@ export const ICLS_ATTRIBUTES: Record<string, Array<[Part, string]>> = {
WARNING_ATTRIBUTES: [["EFFECT_COLOR", "--wave-warning"]],
NOT_USED_ELEMENT_ATTRIBUTES: [["FOREGROUND", "--fg-unused"]],
MATCHED_BRACE_ATTRIBUTES: [["BACKGROUND", "--bg-matched-brace"]],
SEARCH_RESULT_ATTRIBUTES: [["BACKGROUND", "--bg-search-result"]],
SEARCH_RESULT_ATTRIBUTES: [["BACKGROUND", "--bg-search-match"]],
TEXT_SEARCH_RESULT_ATTRIBUTES: [["BACKGROUND", "--bg-text-search-result"]],
IDENTIFIER_UNDER_CARET_ATTRIBUTES: [["BACKGROUND", "--bg-identifier-under-caret"]],
WRITE_IDENTIFIER_UNDER_CARET_ATTRIBUTES: [["BACKGROUND", "--bg-identifier-under-caret-write"]],
@@ -57,7 +57,7 @@ async function preview(container: HTMLElement): Promise<EditorView> {
const { CodePreview, previewDecorations } = await import("../find/parts");
describe("CodePreview", () => {
it("marks matches and the current line", () => {
it("marks matches (the current line's as current) and the current line", () => {
const doc = Text.of(["ab", "order order", "x"]);
const set = previewDecorations(doc, 1, [
{
@@ -76,9 +76,9 @@ describe("CodePreview", () => {
});
expect(out).toEqual([
"3-3:cm-activeLine",
"3-8:cm-openidea-searchResult",
"9-14:cm-openidea-searchResult",
"15-16:cm-openidea-searchResult",
"3-8:cm-openidea-findMatch cm-openidea-findMatch-current",
"9-14:cm-openidea-findMatch cm-openidea-findMatch-current",
"15-16:cm-openidea-findMatch",
]);
});
@@ -87,10 +87,10 @@ describe("CodePreview", () => {
await preview(container);
expect(container.querySelector(".cm-content")?.textContent).toContain("val order = 1");
expect(container.querySelector(".cm-content")?.getAttribute("contenteditable")).toBe("true");
expect(container.querySelector(".cm-openidea-searchResult")?.textContent).toBe("order");
expect(container.querySelector(".cm-openidea-findMatch")?.textContent).toBe("order");
});
it("nests matches inside syntax token spans, as find matches in an editor tab", async () => {
it("nests matches inside syntax token spans, so the search-match colour reaches the text", async () => {
// Like the syntax highlighter: a `Prec.high` mark with its own colour over "order".
const token = Prec.high(
EditorView.decorations.of(Decoration.set(Decoration.mark({ class: "token" }).range(15, 20))),
@@ -101,7 +101,7 @@ describe("CodePreview", () => {
]);
const { container } = render(<CodePreview path={path} line={1} highlights={[{ line: 1, ranges: [[4, 9]] }]} />);
await preview(container);
expect(container.querySelector(".token .cm-openidea-searchResult")?.textContent).toBe("order");
expect(container.querySelector(".token .cm-openidea-findMatch")?.textContent).toBe("order");
});
it.each([false, true])("shares edits, document undo and save with a tab (mounted: %s)", async (mounted) => {
@@ -115,7 +115,7 @@ describe("CodePreview", () => {
act(() => view.dispatch({ changes: { from: 0, insert: "// 😀\n" }, userEvent: "input.type" }));
expect(tab.currentState().doc.toString()).toBe("// 😀\n" + original);
expect(model.getMeta().dirty).toBe(true);
expect(view.state.doc.lineAt(view.posAtDOM(container.querySelector(".cm-openidea-searchResult")!)).number).toBe(3);
expect(view.state.doc.lineAt(view.posAtDOM(container.querySelector(".cm-openidea-findMatch")!)).number).toBe(3);
// The tab's Undo removes preview typing; the preview follows immediately.
act(() => {
undo({ state: tab.currentState(), dispatch: (tr) => tab.dispatchLocal(tr) });
+5 -5
View File
@@ -79,12 +79,12 @@ const previewMarks = StateField.define<DecorationSet>({
for (const e of tr.effects) if (e.is(setPreviewMarks)) return e.value;
return value.map(tr.changes);
},
// Above the syntax highlighter (`Prec.high`), so a match nests inside its token span as find
// matches do in an editor tab.
// Above the syntax highlighter (`Prec.high`), so a match nests inside its token span and
// `--fg-search-match` colours the text, as find matches do in an editor tab (#1733).
provide: (f) => Prec.highest(EditorView.decorations.from(f)),
});
/** IDEA's SEARCH_RESULT_ATTRIBUTES: a background only, so the syntax colours stay (#2858). */
const matchMark = Decoration.mark({ class: "cm-openidea-searchResult" });
const matchMark = Decoration.mark({ class: "cm-openidea-findMatch" });
const currentMatchMark = Decoration.mark({ class: "cm-openidea-findMatch cm-openidea-findMatch-current" });
const currentLineMark = Decoration.line({ class: "cm-activeLine" });
const readOnly = [EditorState.readOnly.of(true), EditorView.editable.of(false)];
@@ -98,7 +98,7 @@ export function previewDecorations(doc: Text, line: number, highlights: readonly
for (const [a, b] of h.ranges) {
const from = l.from + Math.min(a, l.length);
const to = l.from + Math.min(b, l.length);
if (to > from) out.push(matchMark.range(from, to));
if (to > from) out.push((h.line === line ? currentMatchMark : matchMark).range(from, to));
}
}
return Decoration.set(out, true);
+1 -1
View File
@@ -86,7 +86,7 @@ function CommitDialog({ onClose }: { onClose: () => void }) {
<SplitHandle />
<SplitPanel defaultSize="42%" minSize="120px">
<div className={viewStyles.commit}>
<CommitMessageField included={included} runner={runner} autoFocus />
<CommitMessageField included={included} runner={runner} />
</div>
</SplitPanel>
</SplitGroup>
+1 -12
View File
@@ -727,7 +727,6 @@ export function ChangesTree({ mode }: { mode: "commit" | "local" | "dialog" }) {
{mode !== "dialog" && <OperationBanner />}
<div
className={styles.treeWrap}
data-changes-tree={mode}
onKeyDownCapture={(e) => {
if (checkboxes && e.key === " " && !e.metaKey && !e.ctrlKey && selected.size) {
e.preventDefault();
@@ -980,16 +979,7 @@ export function useCommitRunner(included: number, onCommitted?: () => void): Com
}
/** Amend, message history and the message field with its inspections and failed checks. */
export function CommitMessageField({
included,
runner,
autoFocus = false,
}: {
included: number;
runner: CommitRunner;
/** The message takes the initial focus of the dialog it is in (the Commit dialog). */
autoFocus?: boolean;
}) {
export function CommitMessageField({ included, runner }: { included: number; runner: CommitRunner }) {
const message = useCommit((s) => s.message);
const amend = useCommit((s) => s.amend);
const focusTick = useCommit((s) => s.focusTick);
@@ -1062,7 +1052,6 @@ export function CommitMessageField({
</div>
<textarea
ref={ref}
data-autofocus={autoFocus ? "" : undefined}
className={clsx(styles.message, !showMargin && styles.noMargin, emptyError && styles.messageError)}
aria-invalid={emptyError || undefined}
style={{ ["--guide" as string]: `${margin}ch` }}
+1 -41
View File
@@ -24,7 +24,7 @@ import { useProject } from "@/platform/project";
import { commitChangelist, commitFiles } from "../changelistsUi";
import { initialChangelists, useChangelists } from "../changelists";
import { gitTabs, shownTab, stashTabOf, stashTabs } from "../gitTabs";
import { commitFromContext, pushDialogNeeded } from "../commitUi";
import { pushDialogNeeded } from "../commitUi";
import {
installPreparedMessage,
keepMessageAfterCommit,
@@ -188,46 +188,6 @@ describe("Commit File… and the modal commit interface", () => {
});
});
describe("Commit… from Local Changes with the modal interface", () => {
const status: FileStatus[] = [
{ path: "/p/src/a.ts", relPath: "src/a.ts", staged: null, unstaged: "modified", renamedFrom: null },
{ path: "/p/src/b.ts", relPath: "src/b.ts", staged: null, unstaged: "modified", renamedFrom: null },
{ path: "/p/new.txt", relPath: "new.txt", staged: null, unstaged: "untracked", renamedFrom: null },
];
const tree = document.createElement("div");
tree.dataset.changesTree = "local";
const row = document.createElement("button");
tree.append(row);
beforeEach(() => {
showCommitDialog.mockClear();
document.body.append(tree);
useProject.setState({ settings: { vcs: { commit: { nonModal: false } } } as Settings });
useVcs.setState({ available: true, root: "/p", repo: "/p", roots: ["/p"], status });
useCommit.setState({ checks: new Map(), selection: ["/p/src/b.ts", "/p/new.txt"] });
});
it("includes just the files selected there", async () => {
row.focus();
commitFromContext();
await vi.waitFor(() => expect(showCommitDialog).toHaveBeenCalled());
const checks = useCommit.getState().checks;
expect([checks.get("/p/src/a.ts"), checks.get("/p/src/b.ts"), checks.get("/p/new.txt")]).toEqual([
"none",
"all",
"all",
]);
});
it("falls back to the default inclusion when invoked elsewhere", async () => {
useCommit.setState({ checks: new Map([["/p/src/a.ts", "none"]]) });
row.blur();
commitFromContext();
await vi.waitFor(() => expect(showCommitDialog).toHaveBeenCalled());
expect(useCommit.getState().checks.size).toBe(0);
});
});
describe("log tabs of their own", () => {
beforeEach(() => {
vcsLog.mockReset().mockResolvedValue({ status: "ok", data: [] });
+3 -3
View File
@@ -44,7 +44,7 @@ import { gotoChange, rollbackSelectedLines } from "./gutter";
import { openLocalHistory } from "./localHistoryKind";
import { showSelectionHistory } from "./selectionHistoryKind";
import { openLogTab, revealCommit, selectGitTab, useGitLog } from "./logStore";
import { commitFromContext, isModalCommit, pushAfterCommit } from "./commitUi";
import { isModalCommit, openCommit, pushAfterCommit } from "./commitUi";
import { allRepos, runRemote } from "./ops";
import { showPushDialog, showUpdateDialog, type PushTarget, type UpdateTarget } from "./remoteUi";
import { comparePathsWithRevision, showDirectoryWorkingDiff } from "./comparePaths";
@@ -212,7 +212,7 @@ export function registerVcsActions(): () => void {
category: "Git",
icon: "commit",
when: VCS,
run: commitFromContext,
run: openCommit,
},
{
id: "vcs.commitAndPush",
@@ -223,7 +223,7 @@ export function registerVcsActions(): () => void {
run: async () => {
const { message, amend } = useCommit.getState();
// Nothing to commit with yet (or the modal interface): IDEA shows where the message is typed.
if (isModalCommit() || (!message.trim() && !amend)) return commitFromContext();
if (isModalCommit() || (!message.trim() && !amend)) return openCommit();
const id = await commitChanges();
if (id !== undefined) await pushAfterCommit();
},
-27
View File
@@ -6,8 +6,6 @@
import { workflowSettings, type GitSettingsValid } from "@/platform/workflowSettings";
import { toolWindowService } from "@/workbench/contributions";
import { useCommit, type CommitTab } from "./commitStore";
import type { FileStatus } from "@/bindings";
import { groupOf, isPartiallyStaged, type CheckState } from "./changes";
import { stashTabOf } from "./gitTabs";
import { selectGitTab } from "./logStore";
import { isProtectedBranch, pushWithAutoUpdate, showPushDialog } from "./remoteUi";
@@ -33,31 +31,6 @@ export function openCommit() {
void import("./CommitDialog").then((m) => m.showCommitDialog());
}
/**
* Commit… (⌘K) as an action: with the modal interface the dialog includes just the files
* selected in Local Changes when invoked from there, otherwise the default inclusion.
*/
export function commitFromContext() {
if (isModalCommit()) {
const fromLocalChanges = !!(document.activeElement as HTMLElement | null)?.closest?.('[data-changes-tree="local"]');
const selection = fromLocalChanges ? useCommit.getState().selection : [];
useCommit.setState({ checks: selection.length ? includeOnly(useVcs.getState().status, selection) : new Map() });
}
openCommit();
}
/** Inclusion that commits only `paths` of `status` (files elsewhere left out, conflicts as they are). */
export function includeOnly(status: readonly FileStatus[], paths: readonly string[]): Map<string, CheckState> {
const chosen = new Set(paths);
const checks = new Map<string, CheckState>();
for (const fs of status) {
const group = groupOf(fs);
if (!group || group === "conflicts") continue;
checks.set(fs.path, !chosen.has(fs.path) ? "none" : isPartiallyStaged(fs) ? "staged" : "all");
}
return checks;
}
/**
* Commit and Push…: whether the Push dialog comes up after the commit (Settings › Git ›
* "Show Push dialog for Commit and Push", optionally only for protected branches) or
-4
View File
@@ -195,7 +195,6 @@ html {
--fg-editor-selection: inherit;
--bg-text-search-result: #114957;
--bg-text-search-result-current: #165e70;
--bg-search-result: #2d543f;
--bg-matched-brace: #43454a;
--matched-brace-weight: var(--fw-code-bold);
--bg-matched-tag: #3f4045;
@@ -622,7 +621,6 @@ html {
--fg-editor-selection: inherit;
--bg-text-search-result: #fcd47e;
--bg-text-search-result-current: #f2bf57;
--bg-search-result: #ccccff;
--bg-matched-brace: #93d9d9;
--matched-brace-weight: inherit;
--bg-matched-tag: #c9ecec;
@@ -915,7 +913,6 @@ html {
--bg-editor-selection-inactive: #1f3f7a;
--bg-text-search-result: #146a7d;
--bg-text-search-result-current: #1d8aa3;
--bg-search-result: #006112;
--bg-matched-brace: #55585f;
--bg-matched-tag: #45474d;
--bg-fold-placeholder: #33353b;
@@ -1181,7 +1178,6 @@ html {
--bg-text-search-result: color-mix(in srgb, var(--palette-yellow) 22%, var(--palette-bg));
--bg-search-match-current: color-mix(in srgb, var(--palette-yellow) 36%, var(--palette-bg));
--bg-text-search-result-current: color-mix(in srgb, var(--palette-yellow) 36%, var(--palette-bg));
--bg-search-result: color-mix(in srgb, var(--palette-green) 22%, var(--palette-bg));
--fg-search-match: var(--palette-fg);
--border-tab-selected: var(--palette-accent);
--border-indent-guide-active: var(--palette-accent);
-2
View File
@@ -178,7 +178,6 @@ const ICONS = {
"file-gdshader": "openidea/fileTypes/gdshader",
"file-asciidoc": "openidea/fileTypes/asciidoc",
"file-d": "openidea/fileTypes/d",
"file-prisma": "openidea/fileTypes/prisma",
"file-dart": "openidea/fileTypes/dart",
"file-elixir": "openidea/fileTypes/elixir",
"file-erlang": "openidea/fileTypes/erlang",
@@ -1099,7 +1098,6 @@ const EXT_ICONS = table({
"file-gdshader": "gdshader gdshaderinc",
"file-asciidoc": "adoc asciidoc",
"file-d": "d di",
"file-prisma": "prisma",
"file-dart": "dart",
"file-elixir": "ex exs",
"file-erlang": "erl hrl escript app.src",
-3
View File
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke-linejoin="round"><path d="M14 4C14 2.89543 13.1046 2 12 2H4C2.89543 2 2 2.89543 2 4V12C2 13.1046 2.89543 14 4 14H12C13.1046 14 14 13.1046 14 12V4Z" fill="#3A8A80"/><path transform="translate(5 3.5)" d="M0.5 8.5V0.5H3.2C4.9 0.5 5.9 1.5 5.9 2.9C5.9 4.3 4.9 5.3 3.2 5.3H0.5" stroke="#2B2D30"/></g>
</svg>

Before

Width:  |  Height:  |  Size: 408 B

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke-linejoin="round"><path d="M14 4C14 2.89543 13.1046 2 12 2H4C2.89543 2 2 2.89543 2 4V12C2 13.1046 2.89543 14 4 14H12C13.1046 14 14 13.1046 14 12V4Z" fill="#4FA397"/><path transform="translate(5 3.5)" d="M0.5 8.5V0.5H3.2C4.9 0.5 5.9 1.5 5.9 2.9C5.9 4.3 4.9 5.3 3.2 5.3H0.5" stroke="#2B2D30"/></g>
</svg>

Before

Width:  |  Height:  |  Size: 408 B