mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-11 23:44:23 +08:00
Implements the go back and forward keyboard shortcuts (#2789)
This commit is contained in:
parent
a7caa429b0
commit
aed88325c2
6 changed files with 353 additions and 14 deletions
|
@ -156,7 +156,9 @@ const Cell = {
|
||||||
if (event.type === "dispatch_queue_evaluation") {
|
if (event.type === "dispatch_queue_evaluation") {
|
||||||
this.handleDispatchQueueEvaluation(event.dispatch);
|
this.handleDispatchQueueEvaluation(event.dispatch);
|
||||||
} else if (event.type === "jump_to_line") {
|
} else if (event.type === "jump_to_line") {
|
||||||
this.handleJumpToLine(event.line);
|
if (this.isFocused) {
|
||||||
|
this.currentEditor().moveCursorToLine(event.line, event.offset || 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -173,12 +175,6 @@ const Cell = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleJumpToLine(line) {
|
|
||||||
if (this.isFocused) {
|
|
||||||
this.currentEditor().moveCursorToLine(line);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleCellEditorCreated(tag, liveEditor) {
|
handleCellEditorCreated(tag, liveEditor) {
|
||||||
this.liveEditors[tag] = liveEditor;
|
this.liveEditors[tag] = liveEditor;
|
||||||
|
|
||||||
|
@ -211,10 +207,18 @@ const Cell = {
|
||||||
// gives it focus
|
// gives it focus
|
||||||
if (!this.isFocused || !this.insertMode) {
|
if (!this.isFocused || !this.insertMode) {
|
||||||
this.currentEditor().blur();
|
this.currentEditor().blur();
|
||||||
|
} else {
|
||||||
|
this.sendCursorHistory();
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
liveEditor.onSelectionChange(() => {
|
||||||
|
if (this.isFocused) {
|
||||||
|
this.sendCursorHistory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (tag === "primary") {
|
if (tag === "primary") {
|
||||||
const source = liveEditor.getSource();
|
const source = liveEditor.getSource();
|
||||||
|
|
||||||
|
@ -370,6 +374,17 @@ const Cell = {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sendCursorHistory() {
|
||||||
|
const cursor = this.currentEditor().getCurrentCursorPosition();
|
||||||
|
if (cursor === null) return;
|
||||||
|
|
||||||
|
globalPubsub.broadcast("history", {
|
||||||
|
...cursor,
|
||||||
|
type: "navigation",
|
||||||
|
cellId: this.props.cellId,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Cell;
|
export default Cell;
|
||||||
|
|
|
@ -99,6 +99,14 @@ export default class LiveEditor {
|
||||||
*/
|
*/
|
||||||
onFocus = this._onFocus.event;
|
onFocus = this._onFocus.event;
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
_onSelectionChange = new Emitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback called whenever the editor changes selection.
|
||||||
|
*/
|
||||||
|
onSelectionChange = this._onSelectionChange.event;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
container,
|
container,
|
||||||
connection,
|
connection,
|
||||||
|
@ -166,6 +174,21 @@ export default class LiveEditor {
|
||||||
return node.parentElement;
|
return node.parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current main cursor position.
|
||||||
|
*/
|
||||||
|
getCurrentCursorPosition() {
|
||||||
|
if (!this.isMounted()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = this.view.state.selection.main.head;
|
||||||
|
const line = this.view.state.doc.lineAt(pos);
|
||||||
|
const offset = pos - line.from;
|
||||||
|
|
||||||
|
return { line: line.number, offset };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Focuses the editor.
|
* Focuses the editor.
|
||||||
*
|
*
|
||||||
|
@ -183,11 +206,12 @@ export default class LiveEditor {
|
||||||
/**
|
/**
|
||||||
* Updates editor selection such that cursor points to the given line.
|
* Updates editor selection such that cursor points to the given line.
|
||||||
*/
|
*/
|
||||||
moveCursorToLine(lineNumber) {
|
moveCursorToLine(lineNumber, offset) {
|
||||||
const line = this.view.state.doc.line(lineNumber);
|
const line = this.view.state.doc.line(lineNumber);
|
||||||
|
const position = line.from + offset;
|
||||||
|
|
||||||
this.view.dispatch({
|
this.view.dispatch({
|
||||||
selection: EditorSelection.single(line.from),
|
selection: EditorSelection.single(position),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,6 +332,10 @@ export default class LiveEditor {
|
||||||
{ key: "Alt-Enter", run: insertBlankLineAndCloseHints },
|
{ key: "Alt-Enter", run: insertBlankLineAndCloseHints },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const selectionChangeListener = EditorView.updateListener.of((update) =>
|
||||||
|
this.handleViewUpdate(update),
|
||||||
|
);
|
||||||
|
|
||||||
this.view = new EditorView({
|
this.view = new EditorView({
|
||||||
parent: this.container,
|
parent: this.container,
|
||||||
doc: this.source,
|
doc: this.source,
|
||||||
|
@ -369,6 +397,7 @@ export default class LiveEditor {
|
||||||
focus: this.handleEditorFocus.bind(this),
|
focus: this.handleEditorFocus.bind(this),
|
||||||
}),
|
}),
|
||||||
EditorView.clickAddsSelectionRange.of((event) => event.altKey),
|
EditorView.clickAddsSelectionRange.of((event) => event.altKey),
|
||||||
|
selectionChangeListener,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -389,7 +418,6 @@ export default class LiveEditor {
|
||||||
// We dispatch escape event, but only if it is not consumed by any
|
// We dispatch escape event, but only if it is not consumed by any
|
||||||
// registered handler in the editor, such as closing autocompletion
|
// registered handler in the editor, such as closing autocompletion
|
||||||
// or escaping Vim insert mode
|
// or escaping Vim insert mode
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
this.container.dispatchEvent(
|
this.container.dispatchEvent(
|
||||||
new CustomEvent("lb:editor_escape", { bubbles: true }),
|
new CustomEvent("lb:editor_escape", { bubbles: true }),
|
||||||
|
@ -415,6 +443,13 @@ export default class LiveEditor {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
handleViewUpdate(update) {
|
||||||
|
if (!update.state.selection.eq(update.startState.selection)) {
|
||||||
|
this._onSelectionChange.dispatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
completionSource(context) {
|
completionSource(context) {
|
||||||
const settings = settingsStore.get();
|
const settings = settingsStore.get();
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { leaveChannel } from "./js_view/channel";
|
||||||
import { isDirectlyEditable, isEvaluable } from "../lib/notebook";
|
import { isDirectlyEditable, isEvaluable } from "../lib/notebook";
|
||||||
import { settingsStore } from "../lib/settings";
|
import { settingsStore } from "../lib/settings";
|
||||||
import { LiveStore } from "../lib/live_store";
|
import { LiveStore } from "../lib/live_store";
|
||||||
|
import CursorHistory from "./session/cursor_history";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hook managing the whole session.
|
* A hook managing the whole session.
|
||||||
|
@ -81,6 +82,7 @@ const Session = {
|
||||||
this.viewOptions = null;
|
this.viewOptions = null;
|
||||||
this.keyBuffer = new KeyBuffer();
|
this.keyBuffer = new KeyBuffer();
|
||||||
this.lastLocationReportByClientId = {};
|
this.lastLocationReportByClientId = {};
|
||||||
|
this.cursorHistory = new CursorHistory();
|
||||||
this.followedClientId = null;
|
this.followedClientId = null;
|
||||||
this.store = LiveStore.create("session");
|
this.store = LiveStore.create("session");
|
||||||
|
|
||||||
|
@ -161,6 +163,7 @@ const Session = {
|
||||||
globalPubsub.subscribe("jump_to_editor", ({ line, file }) =>
|
globalPubsub.subscribe("jump_to_editor", ({ line, file }) =>
|
||||||
this.jumpToLine(file, line),
|
this.jumpToLine(file, line),
|
||||||
),
|
),
|
||||||
|
globalPubsub.subscribe("history", this.handleHistoryEvent.bind(this)),
|
||||||
];
|
];
|
||||||
|
|
||||||
this.initializeDragAndDrop();
|
this.initializeDragAndDrop();
|
||||||
|
@ -304,6 +307,7 @@ const Session = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmd = isMacOS() ? event.metaKey : event.ctrlKey;
|
const cmd = isMacOS() ? event.metaKey : event.ctrlKey;
|
||||||
|
const ctrl = event.ctrlKey;
|
||||||
const alt = event.altKey;
|
const alt = event.altKey;
|
||||||
const shift = event.shiftKey;
|
const shift = event.shiftKey;
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
|
@ -316,7 +320,16 @@ const Session = {
|
||||||
event.target.closest(`[data-el-outputs-container]`)
|
event.target.closest(`[data-el-outputs-container]`)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (cmd && shift && !alt && key === "Enter") {
|
// On macOS, ctrl+alt+- becomes an em-dash, so we check for the code
|
||||||
|
if (event.code === "Minus" && ctrl && alt) {
|
||||||
|
cancelEvent(event);
|
||||||
|
this.cursorHistoryGoBack();
|
||||||
|
return;
|
||||||
|
} else if (key === "=" && ctrl && alt) {
|
||||||
|
cancelEvent(event);
|
||||||
|
this.cursorHistoryGoForward();
|
||||||
|
return;
|
||||||
|
} else if (cmd && shift && !alt && key === "Enter") {
|
||||||
cancelEvent(event);
|
cancelEvent(event);
|
||||||
this.queueFullCellsEvaluation(true);
|
this.queueFullCellsEvaluation(true);
|
||||||
return;
|
return;
|
||||||
|
@ -1227,6 +1240,8 @@ const Session = {
|
||||||
},
|
},
|
||||||
|
|
||||||
handleCellDeleted(cellId, siblingCellId) {
|
handleCellDeleted(cellId, siblingCellId) {
|
||||||
|
this.cursorHistory.removeAllFromCell(cellId);
|
||||||
|
|
||||||
if (this.focusedId === cellId) {
|
if (this.focusedId === cellId) {
|
||||||
if (this.view) {
|
if (this.view) {
|
||||||
const visibleSiblingId = this.ensureVisibleFocusableEl(siblingCellId);
|
const visibleSiblingId = this.ensureVisibleFocusableEl(siblingCellId);
|
||||||
|
@ -1324,6 +1339,12 @@ const Session = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleHistoryEvent(event) {
|
||||||
|
if (event.type === "navigation") {
|
||||||
|
this.cursorHistory.push(event.cellId, event.line, event.offset);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
repositionJSViews() {
|
repositionJSViews() {
|
||||||
globalPubsub.broadcast("js_views", { type: "reposition" });
|
globalPubsub.broadcast("js_views", { type: "reposition" });
|
||||||
},
|
},
|
||||||
|
@ -1447,12 +1468,39 @@ const Session = {
|
||||||
|
|
||||||
jumpToLine(file, line) {
|
jumpToLine(file, line) {
|
||||||
const [_filename, cellId] = file.split("#cell:");
|
const [_filename, cellId] = file.split("#cell:");
|
||||||
|
|
||||||
this.setFocusedEl(cellId, { scroll: false });
|
this.setFocusedEl(cellId, { scroll: false });
|
||||||
this.setInsertMode(true);
|
this.setInsertMode(true);
|
||||||
|
|
||||||
globalPubsub.broadcast(`cells:${cellId}`, { type: "jump_to_line", line });
|
globalPubsub.broadcast(`cells:${cellId}`, { type: "jump_to_line", line });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cursorHistoryGoBack() {
|
||||||
|
if (this.cursorHistory.canGoBack()) {
|
||||||
|
const { cellId, line, offset } = this.cursorHistory.goBack();
|
||||||
|
this.setFocusedEl(cellId, { scroll: false });
|
||||||
|
this.setInsertMode(true);
|
||||||
|
|
||||||
|
globalPubsub.broadcast(`cells:${cellId}`, {
|
||||||
|
type: "jump_to_line",
|
||||||
|
line,
|
||||||
|
offset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cursorHistoryGoForward() {
|
||||||
|
if (this.cursorHistory.canGoForward()) {
|
||||||
|
const { cellId, line, offset } = this.cursorHistory.goForward();
|
||||||
|
this.setFocusedEl(cellId, { scroll: false });
|
||||||
|
this.setInsertMode(true);
|
||||||
|
|
||||||
|
globalPubsub.broadcast(`cells:${cellId}`, {
|
||||||
|
type: "jump_to_line",
|
||||||
|
line,
|
||||||
|
offset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Session;
|
export default Session;
|
||||||
|
|
133
assets/js/hooks/session/cursor_history.js
Normal file
133
assets/js/hooks/session/cursor_history.js
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
/**
|
||||||
|
* Allows for recording a sequence of focused cells with the focused line
|
||||||
|
* and navigate inside this stack.
|
||||||
|
*/
|
||||||
|
export default class CursorHistory {
|
||||||
|
/** @private */
|
||||||
|
entries = [];
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
index = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new cell to the stack.
|
||||||
|
*
|
||||||
|
* If the stack length is greater than the stack limit,
|
||||||
|
* it will remove the oldest entries.
|
||||||
|
*/
|
||||||
|
push(cellId, line, offset) {
|
||||||
|
const entry = { cellId, line, offset };
|
||||||
|
|
||||||
|
if (this.isSameCell(cellId)) {
|
||||||
|
this.entries[this.index] = entry;
|
||||||
|
} else {
|
||||||
|
if (this.entries[this.index + 1] !== undefined) {
|
||||||
|
this.entries = this.entries.slice(0, this.index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entries.push(entry);
|
||||||
|
this.index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.entries.length > 20) {
|
||||||
|
this.entries.shift();
|
||||||
|
this.index--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all matching cells with given id from the stack.
|
||||||
|
*/
|
||||||
|
removeAllFromCell(cellId) {
|
||||||
|
// We need to make sure the last entry from history
|
||||||
|
// doesn't belong to the given cell id that we need
|
||||||
|
// to remove from the entries list.
|
||||||
|
let cellIdCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i <= this.index; i++) {
|
||||||
|
const entry = this.get(i);
|
||||||
|
if (entry.cellId === cellId) cellIdCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entries = this.entries.filter((entry) => entry.cellId !== cellId);
|
||||||
|
this.index = this.index - cellIdCount;
|
||||||
|
if (this.index === -1 && this.entries.length > 0) this.index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current stack is available to navigate back.
|
||||||
|
*/
|
||||||
|
canGoBack() {
|
||||||
|
return this.canGetFromHistory(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates back in the current stack.
|
||||||
|
*
|
||||||
|
* If the navigation succeeds, it will return the entry from current index.
|
||||||
|
* Otherwise, returns null;
|
||||||
|
*/
|
||||||
|
goBack() {
|
||||||
|
return this.getFromHistory(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current stack is available to navigate forward.
|
||||||
|
*/
|
||||||
|
canGoForward() {
|
||||||
|
return this.canGetFromHistory(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates forward in the current stack.
|
||||||
|
*
|
||||||
|
* If the navigation succeeds, it will return the entry from current index.
|
||||||
|
* Otherwise, returns null;
|
||||||
|
*/
|
||||||
|
goForward() {
|
||||||
|
return this.getFromHistory(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the entry in the current stack.
|
||||||
|
*
|
||||||
|
* If the stack have at least one entry, it will return the entry from current index.
|
||||||
|
* Otherwise, returns null;
|
||||||
|
*/
|
||||||
|
getCurrent() {
|
||||||
|
return this.get(this.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private **/
|
||||||
|
getEntries() {
|
||||||
|
return this.entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private **/
|
||||||
|
get(index) {
|
||||||
|
if (this.entries.length <= 0) return null;
|
||||||
|
return this.entries[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private **/
|
||||||
|
getFromHistory(direction) {
|
||||||
|
if (!this.canGetFromHistory(direction)) return null;
|
||||||
|
|
||||||
|
this.index = Math.max(0, this.index + direction);
|
||||||
|
return this.entries[this.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private **/
|
||||||
|
canGetFromHistory(direction) {
|
||||||
|
if (this.entries.length === 0) return false;
|
||||||
|
|
||||||
|
const index = this.index + direction;
|
||||||
|
return 0 <= index && index < this.entries.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private **/
|
||||||
|
isSameCell(cellId) {
|
||||||
|
const lastEntry = this.get(this.index);
|
||||||
|
return lastEntry !== null && cellId === lastEntry.cellId;
|
||||||
|
}
|
||||||
|
}
|
96
assets/test/hooks/session/cursor_history.test.js
Normal file
96
assets/test/hooks/session/cursor_history.test.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import CursorHistory from "../../../js/hooks/session/cursor_history";
|
||||||
|
|
||||||
|
test("goBack returns when there's at least one cell", () => {
|
||||||
|
const cursorHistory = new CursorHistory();
|
||||||
|
const entry = { cellId: "c1", line: 1, offset: 1 };
|
||||||
|
|
||||||
|
expect(cursorHistory.canGoBack()).toBe(false);
|
||||||
|
expect(cursorHistory.getCurrent()).toStrictEqual(null);
|
||||||
|
|
||||||
|
cursorHistory.push(entry.cellId, entry.line, entry.offset);
|
||||||
|
|
||||||
|
expect(cursorHistory.canGoBack()).toBe(false);
|
||||||
|
expect(cursorHistory.getCurrent()).toStrictEqual(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("push", () => {
|
||||||
|
test("does not add duplicated cells", () => {
|
||||||
|
const cursorHistory = new CursorHistory();
|
||||||
|
const entry = { cellId: "c1", line: 1, offset: 1 };
|
||||||
|
|
||||||
|
expect(cursorHistory.canGoBack()).toBe(false);
|
||||||
|
|
||||||
|
cursorHistory.push(entry.cellId, entry.line, entry.offset);
|
||||||
|
cursorHistory.push(entry.cellId, entry.line, 2);
|
||||||
|
expect(cursorHistory.canGoBack()).toBe(false);
|
||||||
|
|
||||||
|
cursorHistory.push(entry.cellId, 2, entry.offset);
|
||||||
|
expect(cursorHistory.canGoBack()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removes oldest entries and keep it with a maximum of 20 entries", () => {
|
||||||
|
const cursorHistory = new CursorHistory();
|
||||||
|
|
||||||
|
for (let i = 0; i <= 19; i++) {
|
||||||
|
const value = i + 1;
|
||||||
|
cursorHistory.push(`123${value}`, value, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstEntry = cursorHistory.get(0);
|
||||||
|
cursorHistory.push("231", 1, 1);
|
||||||
|
|
||||||
|
// Navigates to the bottom of the stack
|
||||||
|
for (let i = 0; i <= 18; i++) {
|
||||||
|
cursorHistory.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
cursorHistory
|
||||||
|
.getEntries()
|
||||||
|
.forEach((entry) => expect(entry).not.toStrictEqual(firstEntry));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rewrites the subsequent cells if go back and saves a new cell", () => {
|
||||||
|
const cursorHistory = new CursorHistory();
|
||||||
|
expect(cursorHistory.canGoBack()).toBe(false);
|
||||||
|
|
||||||
|
cursorHistory.push("c1", 1, 1);
|
||||||
|
cursorHistory.push("c2", 1, 1);
|
||||||
|
cursorHistory.push("c3", 2, 1);
|
||||||
|
|
||||||
|
expect(cursorHistory.canGoBack()).toBe(true);
|
||||||
|
|
||||||
|
// Go back to cell id c2
|
||||||
|
cursorHistory.goBack();
|
||||||
|
expect(cursorHistory.canGoBack()).toBe(true);
|
||||||
|
|
||||||
|
// Go back to cell id c1
|
||||||
|
cursorHistory.goBack();
|
||||||
|
expect(cursorHistory.canGoBack()).toBe(false);
|
||||||
|
|
||||||
|
// Removes the subsequent cells from stack
|
||||||
|
// and adds this cell to the stack
|
||||||
|
cursorHistory.push("c4", 1, 1);
|
||||||
|
expect(cursorHistory.canGoForward()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removeAllFromCell removes the cells with given id", () => {
|
||||||
|
const cursorHistory = new CursorHistory();
|
||||||
|
const cellId = "123456789";
|
||||||
|
|
||||||
|
cursorHistory.push("123", 1, 1);
|
||||||
|
cursorHistory.push("456", 1, 1);
|
||||||
|
cursorHistory.push(cellId, 1, 1);
|
||||||
|
cursorHistory.push("1234", 1, 1);
|
||||||
|
cursorHistory.push(cellId, 1, 1);
|
||||||
|
cursorHistory.push("8901", 1, 1);
|
||||||
|
cursorHistory.push(cellId, 1, 1);
|
||||||
|
|
||||||
|
cursorHistory.removeAllFromCell(cellId);
|
||||||
|
expect(cursorHistory.canGoForward()).toBe(false);
|
||||||
|
expect(cursorHistory.getCurrent()).toStrictEqual({
|
||||||
|
cellId: "8901",
|
||||||
|
line: 1,
|
||||||
|
offset: 1,
|
||||||
|
});
|
||||||
|
});
|
|
@ -82,8 +82,8 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
|
||||||
],
|
],
|
||||||
navigation_mode: [
|
navigation_mode: [
|
||||||
%{seq: ["?"], desc: "Open this help modal", basic: true},
|
%{seq: ["?"], desc: "Open this help modal", basic: true},
|
||||||
%{seq: ["j"], desc: "Focus next cell", basic: true},
|
%{seq: ["j"], desc: "Focus cell below", basic: true},
|
||||||
%{seq: ["k"], desc: "Focus previous cell", basic: true},
|
%{seq: ["k"], desc: "Focus cell above", basic: true},
|
||||||
%{seq: ["J"], desc: "Move cell down"},
|
%{seq: ["J"], desc: "Move cell down"},
|
||||||
%{seq: ["K"], desc: "Move cell up"},
|
%{seq: ["K"], desc: "Move cell up"},
|
||||||
%{seq: ["i"], desc: "Switch to insert mode", basic: true},
|
%{seq: ["i"], desc: "Switch to insert mode", basic: true},
|
||||||
|
@ -143,6 +143,18 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
|
||||||
press_all: true,
|
press_all: true,
|
||||||
desc: "Save notebook",
|
desc: "Save notebook",
|
||||||
basic: true
|
basic: true
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
seq: ["ctrl", "alt", "-"],
|
||||||
|
seq_mac: ["⌃", "⌥", "-"],
|
||||||
|
press_all: true,
|
||||||
|
desc: "Go back to previous editor"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
seq: ["ctrl", "alt", "="],
|
||||||
|
seq_mac: ["⌃", "⌥", "="],
|
||||||
|
press_all: true,
|
||||||
|
desc: "Go forward to next editor"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue