diff --git a/.changeset/slick-owls-watch.md b/.changeset/slick-owls-watch.md new file mode 100644 index 0000000000..c1de09f387 --- /dev/null +++ b/.changeset/slick-owls-watch.md @@ -0,0 +1,5 @@ +--- +"emacs-mcx": patch +--- + +Paredit config effective per document rather than globally diff --git a/README.md b/README.md index 2ba1f419f1..f662b55a92 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ When true, line-move moves point by visual lines (same as an Emacs variable line ### `emacs-mcx.paredit.parentheses` -Key-value pairs of parentheses like the following example to be used in the ParEdit commands. +Key-value pairs of parentheses to be used in the ParEdit commands like the following example. ```json { @@ -199,6 +199,22 @@ Key-value pairs of parentheses like the following example to be used in the ParE } ``` +The parentheses pairs are inherited from a default configuration and merged in order: Default → User-defined global config → User-defined per-language. Each finer-grained config can override individual pair definitions from its parent. +You can also override the default pairs or disable them by setting `null` as the value. For example: + +```json +"emacs-mcx.paredit.parentheses": { + "<": ">", // New pair + "{": null, // Override to disable +} +// This will result in the following configuration for files of the language: +// { +// "[": "]", +// "(": ")", +// "<": ">" +// } +``` + ### `emacs-mcx.subwordMode` When true, word-oriented move and edit commands, including M-f, M-b, M-d will diff --git a/cspell.json b/cspell.json index d9290bc6ff..773fb409f1 100644 --- a/cspell.json +++ b/cspell.json @@ -31,6 +31,8 @@ "Bksp", "dabbrev-expand", "isearch", + // Programming + "iconfiguration", // npm "unproxify", "jsep", diff --git a/package.json b/package.json index a4b5967ed8..ca9b0e542a 100644 --- a/package.json +++ b/package.json @@ -175,7 +175,10 @@ "type": "object", "patternProperties": { "^\\S$": { - "type": "string", + "type": [ + "string", + "null" + ], "pattern": "^\\S$" } }, @@ -183,7 +186,9 @@ "[": "]", "(": ")", "{": "}" - } + }, + "description": "Defines matching parenthesis pairs used by Paredit commands. Keys are opening delimiters and values are their corresponding closing delimiters.", + "scope": "language-overridable" }, "emacs-mcx.subwordMode": { "type": "boolean", diff --git a/src/commands/paredit.ts b/src/commands/paredit.ts index d7cfc93435..bc4e953e46 100644 --- a/src/commands/paredit.ts +++ b/src/commands/paredit.ts @@ -1,13 +1,31 @@ import * as paredit from "paredit.js"; import { TextDocument, Selection, Range, TextEditor, Position } from "vscode"; +import * as vscode from "vscode"; import { EmacsCommand } from "."; import { KillYankCommand } from "./kill"; import { AppendDirection } from "../kill-yank"; import { revealPrimaryActive } from "./helpers/reveal"; import { MessageManager } from "../message"; +import { Logger } from "../logger"; + +const logger = Logger.get("paredit"); type PareditNavigatorFn = (ast: paredit.AST, idx: number) => number; +function getPareditParenthesesConfig(document: vscode.TextDocument): { [key: string]: string } { + const config = vscode.workspace.getConfiguration("emacs-mcx", document); + const parentheses = config.get<{ [key: string]: string | null }>("paredit.parentheses"); + // parentheses[open] can be null to explicitly disable a pair + const filteredParentheses: { [key: string]: string } = {}; + for (const open in parentheses) { + const close = parentheses[open]; + if (close != null) { + filteredParentheses[open] = close; + } + } + return filteredParentheses; +} + // Languages in which semicolon represents comment const languagesSemicolonComment = new Set(["clojure", "lisp", "scheme"]); @@ -19,8 +37,13 @@ const makeSexpTravelFunc = (doc: TextDocument, pareditNavigatorFn: PareditNaviga // However, in other languages, semicolon should be treated as one entity, but not comment for convenience. // To do so, ";" is replaced with another character which is not treated as comment by paredit.js // if the document is not lisp or lisp-like languages. - src = src.split(";").join("_"); // split + join = replaceAll + src = src.replaceAll(";", "_"); } + + const parentheses = getPareditParenthesesConfig(doc); + logger.debug(`Using paredit parentheses: ${JSON.stringify(parentheses)}`); + paredit.reader.setParentheses(parentheses); + const ast = paredit.parse(src); return (position: Position, repeat: number): Position => { diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 051e991fe8..8f07d53c39 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -4,8 +4,7 @@ import { Logger } from "../logger"; import * as vscode from "vscode"; -import { IConfiguration, IDebugConfiguration, IPareditConfiguration } from "./iconfiguration"; -import * as paredit from "paredit.js"; +import { IConfiguration, IDebugConfiguration } from "./iconfiguration"; export class Configuration implements IConfiguration, vscode.Disposable { /** @@ -81,10 +80,6 @@ export class Configuration implements IConfiguration, vscode.Disposable { return this.scrollDownCommandBehavior; } - public paredit: IPareditConfiguration = { - parentheses: { "[": "]", "(": ")", "{": "}" }, - }; - public debug: IDebugConfiguration = { silent: false, loggingLevelForAlert: "error", @@ -124,9 +119,6 @@ export class Configuration implements IConfiguration, vscode.Disposable { } Logger.configChanged(this); - - // Update configs in the third-party libraries. - paredit.reader.setParentheses(this.paredit.parentheses); } private static unproxify(obj: { [key: string]: unknown }) { diff --git a/src/configuration/iconfiguration.ts b/src/configuration/iconfiguration.ts index c1ab8af3b8..d876f5c39f 100644 --- a/src/configuration/iconfiguration.ts +++ b/src/configuration/iconfiguration.ts @@ -1,7 +1,3 @@ -export interface IPareditConfiguration { - parentheses: { [key: string]: string }; -} - export interface IDebugConfiguration { /** * Boolean indicating whether all logs should be suppressed @@ -59,11 +55,6 @@ export interface IConfiguration { scrollDownCommandBehavior: "vscode" | "emacs"; wordNavigationStyle: "vscode" | "emacs"; - /** - * Paredit configuration - */ - paredit: IPareditConfiguration; - /** * Extension debugging settings */ diff --git a/src/test/suite/commands/paredit.test.ts b/src/test/suite/commands/paredit.test.ts index ab15c19bb4..ae9ad41407 100644 --- a/src/test/suite/commands/paredit.test.ts +++ b/src/test/suite/commands/paredit.test.ts @@ -14,7 +14,6 @@ import { assertSelectionsEqual, createEmulator, } from "../utils"; -import { Configuration } from "../../../configuration/configuration"; suite("paredit commands", () => { let activeTextEditor: TextEditor; @@ -83,17 +82,20 @@ suite("Parentheses config", () => { }); teardown(async () => { getConfigurationStub.restore(); - Configuration.reload(); await cleanUpWorkspace(); }); function mockPareditConfig(parentheses: Record) { - getConfigurationStub.returns({ - paredit: { - parentheses, - }, - }); - Configuration.reload(); + getConfigurationStub.withArgs("emacs-mcx", activeTextEditor.document).returns( + new (class { + get(section: string) { + if (section === "paredit.parentheses") { + return parentheses; + } + return undefined; + } + })(), + ); } test("forwardSexp", async () => { diff --git a/src/test/suite/extension.native.test.ts b/src/test/suite/extension.native.test.ts index 964fbd4739..84766b1336 100644 --- a/src/test/suite/extension.native.test.ts +++ b/src/test/suite/extension.native.test.ts @@ -67,8 +67,8 @@ suite("package.json", () => { return false; } - if (keyFirstSegment == "subwordMode") { - // Special case subwordMode. It's handled by wordSeparators.ts. + if (keyFirstSegment && ["subwordMode", "paredit"].includes(keyFirstSegment)) { + // These configs are consumed in code but do not have direct handlers in Configuration class. return false; } return true;