Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 11 additions & 5 deletions src/features/run_snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,17 @@ const runSnippetCursor = (view: EditorView, ctx: Context, key: string, range: Se
continue;
}

if (snippet.options.automatic || snippet.type === "visual") {
if (
snippet.options.automatic ||
(snippet.type === "visual" && !snippet.triggerKey)
) {
// If the key pressed wasn't a text character, continue
if (!(key.length === 1)) continue;
effectiveLine = updatedLine;
}
else if (!(key === settings.snippetsTrigger)) {
} else if (
!(key === settings.snippetsTrigger && !snippet.triggerKey) &&
snippet.triggerKey !== key
) {
// The snippet must be triggered by a key
continue;
}
Expand Down Expand Up @@ -82,7 +87,8 @@ const runSnippetCursor = (view: EditorView, ctx: Context, key: string, range: Se

// Expand the snippet
const start = triggerPos;
queueSnippet(view, start, to, replacement, key);
const triggerKey = (snippet.options.automatic && snippet.type !== "visual") ? key : undefined;
queueSnippet(view, start, to, replacement, triggerKey);

const containsTrigger = settings.autoEnlargeBracketsTriggers.some(word => replacement.contains("\\" + word));
return {success: true, shouldAutoEnlargeBrackets: containsTrigger};
Expand Down Expand Up @@ -148,4 +154,4 @@ const trimWhitespace = (replacement: string, ctx: Context) => {
}

return replacement;
}
}
250 changes: 182 additions & 68 deletions src/latex_suite.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EditorView, ViewUpdate } from "@codemirror/view";
import { EditorView, KeyBinding, runScopeHandlers, ViewUpdate } from "@codemirror/view";

import { runSnippets } from "./features/run_snippets";
import { runAutoFraction } from "./features/autofraction";
Expand All @@ -15,6 +15,7 @@ import { handleUndoRedo } from "./snippets/codemirror/history";

import { handleMathTooltip } from "./editor_extensions/math_tooltip";
import { isComposing } from "./utils/editor_utils";
import { LatexSuiteCMSettings } from "./settings/settings";

export const handleUpdate = (update: ViewUpdate) => {
const settings = getLatexSuiteConfig(update.state);
Expand All @@ -28,85 +29,198 @@ export const handleUpdate = (update: ViewUpdate) => {
handleUndoRedo(update);
}

export const onKeydown = (event: KeyboardEvent, view: EditorView) => {
const success = handleKeydown(event.key, event.shiftKey, event.ctrlKey || event.metaKey, isComposing(view, event), view);

if (success) event.preventDefault();
}

export const handleKeydown = (key: string, shiftKey: boolean, ctrlKey: boolean, isIME: boolean, view: EditorView) => {
/**
* A global variable to store the latest context.
* This not the most elegant way of caching the context, but it works.
*/
let latestContext: Context | null = null;

const settings = getLatexSuiteConfig(view);
const ctx = Context.fromView(view);

let success = false;

/*
* When backspace is pressed, if the cursor is inside an empty inline math,
* delete both $ symbols, not just the first one.
*/
if (settings.autoDelete$ && key === "Backspace" && ctx.mode.inMath()) {
const charAtPos = getCharacterAtPos(view, ctx.pos);
const charAtPrevPos = getCharacterAtPos(view, ctx.pos - 1);

if (charAtPos === "$" && charAtPrevPos === "$") {
replaceRange(view, ctx.pos - 1, ctx.pos + 1, "");
// Note: not sure if removeAllTabstops is necessary
removeAllTabstops(view);
return true;
export const onKeydown = (event: KeyboardEvent, view: EditorView) => {
try {
latestContext = Context.fromView(view);
if (
handleKeydown(
event.key,
event.ctrlKey || event.metaKey,
isComposing(view, event),
view
) ||
runScopeHandlers(view, event, "latex-suite")
) {
event.preventDefault();
return;
}
} finally {
latestContext = null;
}
};

if (settings.snippetsEnabled) {
export const handleKeydown = (key: string, ctrlKey: boolean, isIME: boolean, view: EditorView) => {

// Prevent IME from triggering keydown events.
if (settings.suppressSnippetTriggerOnIME && isIME) return;
const settings = getLatexSuiteConfig(view);
const ctx = latestContext ?? Context.fromView(view);

if (
!settings.snippetsEnabled ||
// Prevent IME from triggering keydown events.
(settings.suppressSnippetTriggerOnIME && isIME) ||
// Allows Ctrl + z for undo, instead of triggering a snippet ending with z
if (!ctrlKey) {
try {
success = runSnippets(view, ctx, key);
if (success) return true;
}
catch (e) {
clearSnippetQueue(view);
console.error(e);
}
}
ctrlKey
) {
return false;
}

if (key === "Tab") {
success = setSelectionToNextTabstop(view);

if (success) return true;
try {
if (runSnippets(view, ctx, key)) return true;
} catch (e) {
clearSnippetQueue(view);
console.error(e);
}
return false;
};

if (settings.autofractionEnabled && ctx.mode.strictlyInMath()) {
if (key === "/") {
success = runAutoFraction(view, ctx);

if (success) return true;
}
}

if (settings.matrixShortcutsEnabled && ctx.mode.strictlyInMath()) {
if (["Tab", "Enter"].contains(key)) {
success = runMatrixShortcuts(view, ctx, key, shiftKey);

if (success) return true;
}
}
type LatexSuiteKeyBinding = KeyBinding & {scope: "latex-suite"};

if (settings.taboutEnabled) {
// check if the main cursor has something selected since ctx.mode only checks the main cursor.
// This does give weird behaviour with multicursor.
if ((key === "Tab" && view.state.selection.main.empty)
|| shouldTaboutByCloseBracket(view, key)) {
success = tabout(view, ctx);
/**
* Get the keymaps specific for Latex Suite. These keymaps only run in scope `latex-suite`.
* @param settings The settings with the keybindings to use
* @returns The keymaps for the LaTeX suite based on the provided settings
*/
export function getKeymaps(settings: LatexSuiteCMSettings): LatexSuiteKeyBinding[] {
// Order matters for keybindings,
// as they are checked in order from the beginning of the array to the end
const keybindings: KeyBinding[] = [];

if (success) return true;
/*
* When backspace is pressed, if the cursor is inside an empty inline math,
* delete both $ symbols, not just the first one.
*/
keybindings.push({
key: "Backspace",
run: (view: EditorView) => {
if (!getLatexSuiteConfig(view).autoDelete$) return false;
const ctx = latestContext ?? Context.fromView(view);
if (!ctx.mode.strictlyInMath()) return false;
const charAtPos = getCharacterAtPos(view, ctx.pos);
const charAtPrevPos = getCharacterAtPos(view, ctx.pos - 1);
if (charAtPos === "$" && charAtPrevPos === "$") {
replaceRange(view, ctx.pos - 1, ctx.pos + 1, "");
// Note: not sure if removeAllTabstops is necessary
removeAllTabstops(view);
return true;
}
},
});

const snippet_triggers = new Set(
settings.snippets.map((s) => s.triggerKey).filter((s) => s)
);
snippet_triggers.add(settings.snippetsTrigger);
const runMaker = (key: string) => (view: EditorView) => {
const settings = getLatexSuiteConfig(view);
if (!settings.snippetsEnabled) return;
// Prevent IME from triggering keydown events.
if (settings.suppressSnippetTriggerOnIME && view.composing) return;
try {
const ctx = latestContext ?? Context.fromView(view);
return runSnippets(view, ctx, key);
} catch (e) {
clearSnippetQueue(view);
console.error(e);
return false;
}
}

return false;
};

keybindings.push(
...Array.from(snippet_triggers, (key) => {
return {
key,
run: runMaker(key),
};
})
);

keybindings.push({
key: "Tab",
run: (view: EditorView) => {
return setSelectionToNextTabstop(view);
},
});

keybindings.push({
key: "/",
run: (view: EditorView) => {
if (!getLatexSuiteConfig(view).autofractionEnabled) return;
const ctx = latestContext ?? Context.fromView(view);
if (!ctx.mode.strictlyInMath()) return false;
return runAutoFraction(view, ctx);
},
});

// Matrix shortcuts are intentionally put before tabout shortcuts,
const matrixShortcuts = [
{
key: "Enter",
run: (view: EditorView) => {
const ctx = latestContext ?? Context.fromView(view);
if (!ctx.mode.strictlyInMath()) return;
return runMatrixShortcuts(view, ctx, "Enter", false);
},
},
{
key: "Tab",
run: (view: EditorView) => {
const ctx = latestContext ?? Context.fromView(view);
if (!ctx.mode.strictlyInMath()) return;
return runMatrixShortcuts(view, ctx, "Tab", false);
},
},
{
key: "Shift-Enter",
run: (view: EditorView) => {
const ctx = latestContext ?? Context.fromView(view);
if (!ctx.mode.strictlyInMath()) return;
return runMatrixShortcuts(view, ctx, "Enter", true);
},
},
];
matrixShortcuts.map((keybinding) => {
keybinding.run = (view: EditorView) => {
if (!getLatexSuiteConfig(view).matrixShortcutsEnabled) return;
return keybinding.run(view);
};
});
keybindings.push(...matrixShortcuts);

const taboutShortcuts = [
{
key: settings.taboutTrigger,
run: (view: EditorView) => {
if (!view.state.selection.main.empty) return;
const ctx = latestContext ?? Context.fromView(view);
return tabout(view, ctx);
},
},
...[")", "}", "]"].map((key) => ({
key,
run: (view: EditorView) => {
if (!shouldTaboutByCloseBracket(view, key)) return false;
const ctx = latestContext ?? Context.fromView(view);
return tabout(view, ctx);
},
})),
];
taboutShortcuts.map((keybinding) => {
keybinding.run = (view: EditorView) => {
if (!getLatexSuiteConfig(view).taboutEnabled) return false;
return keybinding.run(view);
};
});
keybindings.push(...taboutShortcuts);

return keybindings.map((keybinding) => ({
...keybinding,
scope: "latex-suite",
}));
}

7 changes: 5 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { ICONS } from "./settings/ui/icons";
import { getEditorCommands } from "./features/editor_commands";
import { getLatexSuiteConfigExtension } from "./snippets/codemirror/config";
import { SnippetVariables, parseSnippetVariables, parseSnippets } from "./snippets/parse";
import { handleUpdate, onKeydown } from "./latex_suite";
import { EditorView, tooltips } from "@codemirror/view";
import { getKeymaps, handleUpdate, onKeydown } from "./latex_suite";
import { EditorView, keymap, tooltips } from "@codemirror/view";
import { snippetExtensions } from "./snippets/codemirror/extensions";
import { mkConcealPlugin } from "./editor_extensions/conceal";
import { colorPairedBracketsPluginLowestPrec, highlightCursorBracketsPlugin } from "./editor_extensions/highlight_brackets";
Expand Down Expand Up @@ -166,6 +166,9 @@ export default class LatexSuitePlugin extends Plugin {
EditorView.updateListener.of(handleUpdate),
snippetExtensions,
]);

const latexSuiteKeymaps = getKeymaps(this.CMSettings)
this.editorExtensions.push(keymap.of(latexSuiteKeymaps))

// Optional extensions
if (this.CMSettings.concealEnabled) {
Expand Down
6 changes: 5 additions & 1 deletion src/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { DEFAULT_SNIPPET_VARIABLES } from "src/utils/default_snippet_variables";

interface LatexSuiteBasicSettings {
snippetsEnabled: boolean;
snippetsTrigger: "Tab" | " "
/** trigger following the same format as https://codemirror.net/docs/ref/#view.KeyBinding */
snippetsTrigger: string;
suppressSnippetTriggerOnIME: boolean;
removeSnippetWhitespace: boolean;
autoDelete$: boolean;
Expand All @@ -24,6 +25,8 @@ interface LatexSuiteBasicSettings {
autofractionBreakingChars: string;
matrixShortcutsEnabled: boolean;
taboutEnabled: boolean;
/** trigger following the same format as https://codemirror.net/docs/ref/#view.KeyBinding */
taboutTrigger: string;
autoEnlargeBrackets: boolean;
wordDelimiters: string;
}
Expand Down Expand Up @@ -73,6 +76,7 @@ export const DEFAULT_SETTINGS: LatexSuitePluginSettings = {
autofractionBreakingChars: "+-=\t",
matrixShortcutsEnabled: true,
taboutEnabled: true,
taboutTrigger: "Tab",
autoEnlargeBrackets: true,
wordDelimiters: "., +-\\n\t:;!?\\/{}[]()=~$",

Expand Down
Loading