Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance Matrix Shortcut with Multi-selection #394

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
dbd1298
feat: refactor and add multi-selection support
Testudinidae Feb 16, 2025
867e078
feat: trim whitespace surrounding cursor on Tab and Enter
Testudinidae Feb 13, 2025
845c056
feat: make trimming whitespace optional
Testudinidae Feb 13, 2025
b93b132
feat: trim extra '&' at end of line on Enter
Testudinidae Feb 16, 2025
bc92438
feat: disable auto '\\' insertion after '\hline' on Enter
Testudinidae Feb 19, 2025
836846f
feat: make disabling '\\' insertion after '\hline' optional
Testudinidae Feb 19, 2025
6f4f89b
refactor: split isWithinEnvironment into isWithinEnvironment and getE…
Testudinidae Feb 18, 2025
d4c0422
Merge branch 'feature/matrix-shortcuts-multi-selection' into feature/…
Testudinidae Feb 20, 2025
40ef922
feat: trim empty line when exiting an environment with Shift+Enter
Testudinidae Feb 17, 2025
0b97c4c
feat: make trimming empty line after environment optional
Testudinidae Feb 17, 2025
acf6688
feat: add line break when exiting an environment with Shift+Enter
Testudinidae Feb 19, 2025
fac502f
feat: make adding line break after environment optional
Testudinidae Feb 19, 2025
7f21a4b
Merge branch 'feature/matrix-shortcuts-add-line-break-after-environme…
Testudinidae Feb 20, 2025
f53e552
feat: make trimming alignment optional
Testudinidae Feb 20, 2025
dc78f1a
Merge branch 'feature/matrix-shortcut-hline-skip' into feature/matrix…
Testudinidae Feb 23, 2025
46f495b
Merge branch 'feature/matrix-shortcuts-after-environment' into featur…
Testudinidae Feb 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 141 additions & 26 deletions src/features/matrix_shortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,174 @@
import { EditorView } from "@codemirror/view";
import { setCursor } from "src/utils/editor_utils";
import { getLatexSuiteConfig } from "src/snippets/codemirror/config";
import { EditorSelection, SelectionRange } from "@codemirror/state";
import { Context } from "src/utils/context";
import { setCursor, replaceRange } from "src/utils/editor_utils";
import { getLatexSuiteConfig } from "src/snippets/codemirror/config";
import { tabout } from "src/features/tabout";


const ALIGNMENT = " & ";
const LINE_BREAK = " \\\\\n"
const LINE_BREAK_INLINE = " \\\\ "
const END_LINE_BREAK = LINE_BREAK.trimEnd();
let trimWhitespace = false;
let trimAlignment = false;
let hlineLineBreakEnabled = false;
let trimEmptyLineAfterEnv = false;
let addLineBreakAfterEnv = false;


const isLineBreak = (separator: string) => {
return separator.contains("\\\\");
}


const isMultiLineBreak = (separator: string): boolean => {
return separator.contains("\\\\") && separator.contains("\n");
}


const isHline = (line: string): boolean => {
return line.trimEnd().endsWith("\\hline");
}


const generateSeparatorChange = (separator: string, view: EditorView, range: SelectionRange): { from: number, to: number, insert: string } => {
const d = view.state.doc;

const fromLine = d.lineAt(range.from);
const textBeforeFrom = d.sliceString(fromLine.from, range.from).trimStart(); // Preserve indents

const toLine = d.lineAt(range.from);
const textAfterTo = d.sliceString(range.to, toLine.to);

let from = range.from;
let to = range.to;

if (!hlineLineBreakEnabled && isMultiLineBreak(separator) && isHline(textBeforeFrom)) {
separator = "\n";
}

if (trimWhitespace) {
// If at the beginning of the line
if (textBeforeFrom === "") {
separator = separator.match(/^[ \t]*([\s\S]*)$/)[1];
}

// Extend selection to include trailing whitespace before `from`
if (trimAlignment && isLineBreak(separator)) {
from -= textBeforeFrom.match(/\s*\&?\s*$/)[0].length;
}
else {
from -= textBeforeFrom.match(/\s*$/)[0].length;
}
to += textAfterTo.match(/^\s*/)[0].length; // Extend selection to include leading whitespace after `to`
}

// Insert indents
const leadingIndents = fromLine.text.match(/^\s*/)[0];
separator = separator.replaceAll("\n", `\n${leadingIndents}`);

return { from: from, to: to, insert: separator };
}


const applySeparator = (separator: string, view: EditorView) => {
const sel = view.state.selection;
const changes = sel.ranges.map(range => generateSeparatorChange(separator, view, range));

const tempTransaction = view.state.update({ changes });

const newSelection = EditorSelection.create(
changes.map(({ from, to, insert }) =>
EditorSelection.cursor(tempTransaction.changes.mapPos(from) + insert.length)
),
sel.mainIndex
);

view.dispatch(view.state.update({ changes, selection: newSelection }));
}


export const runMatrixShortcuts = (view: EditorView, ctx: Context, key: string, shiftKey: boolean): boolean => {
const settings = getLatexSuiteConfig(view);

// Check whether we are inside a matrix / align / case environment
let isInsideAnEnv = false;

let env;
for (const envName of settings.matrixShortcutsEnvNames) {
const env = { openSymbol: "\\begin{" + envName + "}", closeSymbol: "\\end{" + envName + "}" };
env = { openSymbol: "\\begin{" + envName + "}", closeSymbol: "\\end{" + envName + "}" };

isInsideAnEnv = ctx.isWithinEnvironment(ctx.pos, env);
if (isInsideAnEnv) break;
}

if (!isInsideAnEnv) return false;

// Take main cursor since ctx.mode takes the main cursor, weird behaviour is expected with multicursor because of this.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry to be nitpicky but why was this removed. Weird behavior still arises when the main cursor is in different environment then the other cursors.
For example if nothing is selected in the main cursor but something is selected in another cursor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to apologize—I believe all features should be discussed to find the most convenient solution for everyone. I'm not sure what you mean by "weird behavior." I think in multi-selection cases, all selections should behave consistently—either all selected lines increase indentation or all selections are replaced with " & ", always following the main selection. Other potentially strange behaviors, like some selections being outside the environment or even outside the math block, shouldn’t be an issue because users using multi-selection should clearly know what they are doing. I hope you can provide more specific examples of what you mean by "weird behavior."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My original thought was that a cursor according to its "environment" (like math, code or text).
So any cursor that doesn't follow the behaviour of its environment is messy behaviour, but since I couldn't give a use case I just called it weird behaviour.
Cause if you have the following code where | is the cursor

\begin{pmatrix}
1 & 2 |
\end{pmatrix}
| some selected text|

then either the lines are indented or the selected text is replaced with a &, which can be weird but also no use case.
The main cursor was indeed taken to be consistent with the rest of the plugin and put the comment there if someone wanted ctx cover multicursor.

But I still can't think of a use case, why someone would use cursors in different environments besides accidents, so it can be left out.

hope this explained it better.

trimWhitespace = settings.matrixShortcutsTrimWhitespace;
trimAlignment = settings.matrixShortcutsTrimAlignment;
hlineLineBreakEnabled = settings.matrixShortcutsHlineLineBreakEnabled;

if (key === "Tab" && view.state.selection.main.empty) {
view.dispatch(view.state.replaceSelection(" & "));
applySeparator(ALIGNMENT, view);

return true;
}

else if (key === "Enter") {
if (shiftKey && ctx.mode.blockMath) {
// Move cursor to end of next line
const d = view.state.doc;
if (shiftKey) {
if (ctx.mode.inlineMath) {
tabout(view, ctx);
}
else {
// Move cursor to end of next line
let d = view.state.doc;
const envBound = ctx.getEnvironmentBound(ctx.pos, env);
const envText = d.sliceString(envBound.start, envBound.end);

const nextLineNo = d.lineAt(ctx.pos).number + 1;
const nextLine = d.line(nextLineNo);
trimEmptyLineAfterEnv = settings.matrixShortcutsTrimEmptyLineAfterEnv;
addLineBreakAfterEnv = settings.matrixShortcutsAddLineBreakAfterEnv;

setCursor(view, nextLine.to);
}
else if (shiftKey && ctx.mode.inlineMath) {
tabout(view, ctx);
}
else if (ctx.mode.blockMath) {
const d = view.state.doc;
const lineText = d.lineAt(ctx.pos).text;
const matchIndents = lineText.match(/^\s*/);
const leadingIndents = matchIndents ? matchIndents[0] : "";
let line = d.lineAt(ctx.pos);
let lineNo = line.number;
let nextLine = d.line(lineNo + 1);
let newPos = nextLine.to;

view.dispatch(view.state.replaceSelection(` \\\\\n${leadingIndents}`));
if (newPos > envBound.end) {
if (trimEmptyLineAfterEnv && line.text.trim() === "") {
replaceRange(view, line.from, nextLine.from, "");

d = view.state.doc;
lineNo--;
line = d.line(lineNo);
nextLine = d.line(lineNo + 1);
newPos = nextLine.to;
}

if (addLineBreakAfterEnv && !envText.trimEnd().endsWith("\\\\")) {
setCursor(view, line.to);

applySeparator(END_LINE_BREAK, view);

d = view.state.doc;
nextLine = d.line(lineNo + 1);
newPos = nextLine.to;
}
}

setCursor(view, newPos);
}
}
else {
view.dispatch(view.state.replaceSelection(" \\\\ "));
if (ctx.mode.inlineMath) {
applySeparator(LINE_BREAK_INLINE, view);
}
else {
applySeparator(LINE_BREAK, view);
}
}

return true;
}
else {
return false;
}

return false;
}
10 changes: 10 additions & 0 deletions src/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ interface LatexSuiteBasicSettings {
autofractionSymbol: string;
autofractionBreakingChars: string;
matrixShortcutsEnabled: boolean;
matrixShortcutsTrimWhitespace: boolean;
matrixShortcutsTrimAlignment: boolean;
matrixShortcutsHlineLineBreakEnabled: boolean;
matrixShortcutsTrimEmptyLineAfterEnv: boolean;
matrixShortcutsAddLineBreakAfterEnv: boolean;
taboutEnabled: boolean;
autoEnlargeBrackets: boolean;
wordDelimiters: string;
Expand Down Expand Up @@ -72,6 +77,11 @@ export const DEFAULT_SETTINGS: LatexSuitePluginSettings = {
autofractionSymbol: "\\frac",
autofractionBreakingChars: "+-=\t",
matrixShortcutsEnabled: true,
matrixShortcutsTrimWhitespace: true,
matrixShortcutsTrimAlignment: true,
matrixShortcutsHlineLineBreakEnabled: false,
matrixShortcutsTrimEmptyLineAfterEnv: true,
matrixShortcutsAddLineBreakAfterEnv: false,
taboutEnabled: true,
autoEnlargeBrackets: true,
wordDelimiters: "., +-\\n\t:;!?\\/{}[]()=~$",
Expand Down
61 changes: 61 additions & 0 deletions src/settings/settings_tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ export class LatexSuiteSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.matrixShortcutsEnabled)
.onChange(async (value) => {
this.plugin.settings.matrixShortcutsEnabled = value;

await this.plugin.saveSettings();
}));

Expand All @@ -327,6 +328,66 @@ export class LatexSuiteSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));

new Setting(containerEl)
.setName("Trim Excess Whitespace")
.setDesc("When enabled, Tab and Enter will trim surrounding whitespace to prevent excessive spaces.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.matrixShortcutsTrimWhitespace)
.onChange(async (value) => {
this.plugin.settings.matrixShortcutsTrimWhitespace = value;

trimAlignmentSetting.settingEl.toggleClass("hidden", !value);

await this.plugin.saveSettings();
}));

const trimAlignmentSetting = new Setting(containerEl)
.setName("Trim Excess Alignment")
.setDesc("When enabled, Enter will trim the extra '&' to prevent excessive spacing.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.matrixShortcutsTrimAlignment)
.onChange(async (value) => {
this.plugin.settings.matrixShortcutsTrimAlignment = value;

await this.plugin.saveSettings();
}));

// Hide settings that are not relevant when "matrixShortcutsTrimWhitespace" is set to true/false
const matrixShortcutsTrimWhitespace = this.plugin.settings.matrixShortcutsTrimWhitespace;
trimAlignmentSetting.settingEl.toggleClass("hidden", !matrixShortcutsTrimWhitespace);

new Setting(containerEl)
.setName("Enable '\\\\' After '\\hline'")
.setDesc("When enabled, '\\\\' will be inserted after '\\hline'.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.matrixShortcutsHlineLineBreakEnabled)
.onChange(async (value) => {
this.plugin.settings.matrixShortcutsHlineLineBreakEnabled = value;

await this.plugin.saveSettings();
}));

new Setting(containerEl)
.setName("Trim Empty Line After Environment")
.setDesc("When enabled, Shift + Enter will remove an empty line when exiting an environment to prevent excessive spaces.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.matrixShortcutsTrimEmptyLineAfterEnv)
.onChange(async (value) => {
this.plugin.settings.matrixShortcutsTrimEmptyLineAfterEnv = value;

await this.plugin.saveSettings();
}));

new Setting(containerEl)
.setName("Add Line Break After Environment")
.setDesc("When enabled, Shift + Enter will add a line break when exiting an environment to ensure proper formatting.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.matrixShortcutsAddLineBreakAfterEnv)
.onChange(async (value) => {
this.plugin.settings.matrixShortcutsAddLineBreakAfterEnv = value;

await this.plugin.saveSettings();
}));
}

private displayTaboutSettings() {
Expand Down
18 changes: 11 additions & 7 deletions src/utils/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ export class Context {
return Context.fromState(view.state);
}

isWithinEnvironment(pos: number, env: Environment): boolean {
if (!this.mode.inMath()) return false;
getEnvironmentBound(pos: number, env: Environment): Bounds {
if (!this.mode.inMath()) return null;

const bounds = this.getInnerBounds();
if (!bounds) return;

const {start, end} = bounds;
const { start, end } = bounds;
const text = this.state.sliceDoc(start, end);
// pos referred to the absolute position in the whole document, but we just sliced the text
// so now pos must be relative to the start in order to be any useful
Expand All @@ -93,20 +93,24 @@ export class Context {
while (left != -1) {
const right = findMatchingBracket(text, left + offset, openSearchSymbol, env.closeSymbol, false);

if (right === -1) return false;
if (right === -1) return null;

// Check whether the cursor lies inside the environment symbols
if ((right >= pos) && (pos >= left + env.openSymbol.length)) {
return true;
return { start: start + left + env.openSymbol.length, end: start + right };
}

if (left <= 0) return false;
if (left <= 0) return null;

// Find the next open symbol
left = text.lastIndexOf(env.openSymbol, left - 1);
}

return false;
return null;
}

isWithinEnvironment(pos: number, env: Environment): boolean {
return this.getEnvironmentBound(pos, env) !== null;
}

inTextEnvironment(): boolean {
Expand Down