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

Feature/reverse tabout #402

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,20 @@ To reveal the LaTeX syntax, move your cursor over it. You can also choose to del
![conceal demo](https://raw.githubusercontent.com/artisticat1/obsidian-latex-suite/main/gifs/conceal.png)


### Tabout
### Tabout & Reverse Tabout
#### Tabout
To make it easier to navigate and exit equations,

- Pressing <kbd>Tab</kbd> while the cursor is at the end of an equation will move the cursor outside the `$` symbols.
- Otherwise, pressing <kbd>Tab</kbd> will advance the cursor to the next closing bracket: `)`, `]`, `}`, `>`, or `|`.
- If the cursor is inside a `\left ... \right` pair, pressing <kbd>Tab</kbd> will jump to after the `\right` command and its corresponding delimiter.
- Otherwise, pressing <kbd>Tab</kbd> will advance the cursor to the next closing bracket: `)`, `]`, `}`, `\rangle`, or `\rvert`.

#### Reverse Tabout
To navigate equations in the opposite direction:

- Pressing <kbd>Shift + Tab</kbd> while the cursor is at the beginning of an equation will move the cursor before the opening `$` symbol.
- If the cursor is inside a `\left ... \right` pair, pressing <kbd>Shift + Tab</kbd> will jump to before the `\left` command.
- Otherwise, pressing <kbd>Shift + Tab</kbd> will move the cursor to the previous opening bracket: `(`, `[`, `{`, `\langle`, or `\lvert`.

### Preview inline math
When your cursor is inside inline math, a popup window showing the rendered math will be displayed.
Expand Down
259 changes: 246 additions & 13 deletions src/features/tabout.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,157 @@
import { EditorView } from "@codemirror/view";
import { replaceRange, setCursor, getCharacterAtPos } from "src/utils/editor_utils";
import { Context } from "src/utils/context";
import { getLatexSuiteConfig } from "src/snippets/codemirror/config";


export const tabout = (view: EditorView, ctx: Context):boolean => {
if (!ctx.mode.inMath()) return false;
let sortedLeftCommands: string[] = [];
let sortedRightCommands: string[] = [];
let sortedDelimiters: string[] = [];
let sortedOpeningSymbols: string[] = [];
let sortedClosingSymbols: string[] = [];


const isCommandEnd = (str: string): boolean => {
return /\\[a-zA-Z]+\\*?$/.test(str);
}


const isMatchingCommand = (text: string, command: string, startIndex: number): boolean => {
if (!text.startsWith(command, startIndex)) {
return false;
}

const nextChar = text.charAt(startIndex + command.length);
const isEndOfCommand = !/[a-zA-Z]/.test(nextChar);

return isEndOfCommand;
}


const isMatchingToken = (text: string, token: string, startIndex: number): boolean => {
if (isCommandEnd(token)) {
return isMatchingCommand(text, token, startIndex);
}
else {
return text.startsWith(token, startIndex);
}
}


const findTokenLength = (sortedTokens: string[], text: string, startIndex: number): number => {
const matchedToken = sortedTokens.find((token) => isMatchingToken(text, token, startIndex));

if (matchedToken) {
return matchedToken.length;
}

return 0;
}


const findCommandWithDelimiterLength = (sortedCommands: string[], text: string, startIndex: number): number => {
const matchedCommand = sortedCommands.find((command) => isMatchingCommand(text, command, startIndex));

if (!matchedCommand) {
return 0;
}

const afterCommandIndex = startIndex + matchedCommand.length;

let whitespaceCount = 0;
while (/\s/.test(text.charAt(afterCommandIndex + whitespaceCount))) {
whitespaceCount++;
}
const delimiterStartIndex = afterCommandIndex + whitespaceCount;

const matchedDelimiter = sortedDelimiters.find((delimiter) => isMatchingToken(text, delimiter, delimiterStartIndex));

if (!matchedDelimiter) {
return 0;
}

return matchedCommand.length + whitespaceCount + matchedDelimiter.length;
}


const findLeftDelimiterLength = (text: string, startIndex: number): number => {
const leftDelimiterLength = findCommandWithDelimiterLength(sortedLeftCommands, text, startIndex);
if (leftDelimiterLength) return leftDelimiterLength;

const openingSymbolLength = findTokenLength(sortedOpeningSymbols, text, startIndex);
if (openingSymbolLength) return openingSymbolLength;

return 0;
}


const findRightDelimiterLength = (text: string, startIndex: number): number => {
const rightDelimiterLength = findCommandWithDelimiterLength(sortedRightCommands, text, startIndex);
if (rightDelimiterLength) return rightDelimiterLength;

const closingSymbolLength = findTokenLength(sortedClosingSymbols, text, startIndex);
if (closingSymbolLength) return closingSymbolLength;

return 0;
}


export const tabout = (view: EditorView, ctx: Context): boolean => {
if (!ctx.mode.inMath()) return false;

const result = ctx.getBounds();
if (!result) return false;

const start = result.start;
const end = result.end;

const pos = view.state.selection.main.to;

const d = view.state.doc;
const text = d.toString();

// Move to the next closing bracket: }, ), ], >, |, or \\rangle
const rangle = "\\rangle";
sortedLeftCommands = getLatexSuiteConfig(view).sortedTaboutLeftCommands;
sortedRightCommands = getLatexSuiteConfig(view).sortedTaboutRightCommands;
sortedDelimiters = getLatexSuiteConfig(view).sortedTaboutDelimiters;
sortedClosingSymbols = getLatexSuiteConfig(view).sortedTaboutClosingSymbols;

// Move to the next closing bracket
let i = start;
while (i < end) {
const rightDelimiterLength = findRightDelimiterLength(text, i);
if (rightDelimiterLength > 0) {
i += rightDelimiterLength;

for (let i = pos; i < end; i++) {
if (["}", ")", "]", ">", "|", "$"].contains(text.charAt(i))) {
setCursor(view, i+1);
if (i > pos) {
setCursor(view, i);
return true;
}

return true;
continue;
}
else if (text.slice(i, i + rangle.length) === rangle) {
setCursor(view, i + rangle.length);

return true;
// Attempt to match only the right command if matching right command + delimiter fails
const rightCommandLength = findTokenLength(sortedRightCommands, text, i);
if (rightCommandLength > 0) {
i += rightCommandLength;

if (i > pos) {
setCursor(view, i);
return true;
}

continue;
}

// Skip left command + delimiter
const leftDelimiterLength = findCommandWithDelimiterLength(sortedLeftCommands, text, i);
if (leftDelimiterLength > 0) {
i += leftDelimiterLength;

continue;
}

i++;
}


Expand All @@ -47,7 +171,7 @@ export const tabout = (view: EditorView, ctx: Context):boolean => {
}
else {
// First, locate the $$ symbol
const dollarLine = d.lineAt(end+2);
const dollarLine = d.lineAt(end + 2);

// If there's no line after the equation, create one

Expand All @@ -69,6 +193,115 @@ export const tabout = (view: EditorView, ctx: Context):boolean => {
}


export const reverseTabout = (view: EditorView, ctx: Context): boolean => {
if (!ctx.mode.inMath()) return false;

const result = ctx.getBounds();
if (!result) return false;

const start = result.start;
const end = result.end;

const pos = view.state.selection.main.to;

const d = view.state.doc;
const text = d.toString();

sortedLeftCommands = getLatexSuiteConfig(view).sortedTaboutLeftCommands;
sortedRightCommands = getLatexSuiteConfig(view).sortedTaboutRightCommands;
sortedDelimiters = getLatexSuiteConfig(view).sortedTaboutDelimiters;
sortedOpeningSymbols = getLatexSuiteConfig(view).sortedTaboutOpeningSymbols;

const textBtwnStartAndCursor = d.sliceString(start, pos);
const isAtStart = textBtwnStartAndCursor.trim().length === 0;

// Move out of the equation.
if (isAtStart) {
if (ctx.mode.inlineMath || ctx.mode.codeMath) {
setCursor(view, start - 1);
}
else {
let whitespaceCount = 0;
while (/[ ]/.test(text.charAt(start - 2 - whitespaceCount - 1))) {
whitespaceCount++;
}
if (text.charAt(start - 2 - whitespaceCount - 1) == "\n") {
setCursor(view, start - 2 - whitespaceCount - 1);
}
else {
setCursor(view, start - 2);
}
}

return true;
}

// Move to the previous openinging bracket
let previous_i = start;
let i = start;
while (i < end) {
const leftDelimiterLength = findLeftDelimiterLength(text, i);
if (leftDelimiterLength > 0) {
if (i >= pos) {
setCursor(view, previous_i);

return true;
}

previous_i = i;
i += leftDelimiterLength;

if (i >= pos) {
setCursor(view, previous_i);

return true;
}

continue;
}

// Attempt to match only the left command if matching left command + delimiter fails
const leftCommandLength = findTokenLength(sortedLeftCommands, text, i);
if (leftCommandLength > 0) {
if (i >= pos) {
setCursor(view, previous_i);

return true;
}

previous_i = i;
i += leftCommandLength;

if (i >= pos) {
setCursor(view, previous_i);

return true;
}

// This helps users easily identify and correct missing delimiters.
// Set cursor to the next to the left coomand
previous_i = i;

continue;
}

// Skip right command + delimiter
const rightDelimiterLength = findCommandWithDelimiterLength(sortedRightCommands, text, i);
if (rightDelimiterLength > 0) {
i += rightDelimiterLength;

continue;
}

i++;
}

setCursor(view, previous_i);

return true;
}


export const shouldTaboutByCloseBracket = (view: EditorView, keyPressed: string) => {
const sel = view.state.selection.main;
if (!sel.empty) return;
Expand All @@ -83,4 +316,4 @@ export const shouldTaboutByCloseBracket = (view: EditorView, keyPressed: string)
else {
return false;
}
}
}
14 changes: 11 additions & 3 deletions src/latex_suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EditorView, ViewUpdate } from "@codemirror/view";

import { runSnippets } from "./features/run_snippets";
import { runAutoFraction } from "./features/autofraction";
import { tabout, shouldTaboutByCloseBracket } from "./features/tabout";
import { tabout, reverseTabout, shouldTaboutByCloseBracket } from "./features/tabout";
import { runMatrixShortcuts } from "./features/matrix_shortcuts";

import { Context } from "./utils/context";
Expand Down Expand Up @@ -98,9 +98,17 @@ export const handleKeydown = (key: string, shiftKey: boolean, ctrlKey: boolean,
}

if (settings.taboutEnabled) {
if (key === "Tab" || shouldTaboutByCloseBracket(view, key)) {
success = tabout(view, ctx);
if (key === "Tab") {
if (settings.reverseTaboutEnabled && shiftKey) {
success = reverseTabout(view, ctx);
}
else {
success = tabout(view, ctx);
}
if (success) return true;
}

if (shouldTaboutByCloseBracket(view, key)) {
if (success) return true;
}
}
Expand Down
Loading