Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CoreEditor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"globals": "^15.15.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"markedit-api": "https://github.com/MarkEdit-app/MarkEdit-api#v0.37.0",
"markedit-api": "https://github.com/MarkEdit-app/MarkEdit-api#v0.38.0",
"ts-gyb": "^0.12.0",
"ts-jest": "^29.4.5",
"tslib": "^2.6.2",
Expand Down
11 changes: 9 additions & 2 deletions CoreEditor/src/api/ui.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EditorView } from '@codemirror/view';
import { MenuItem, MenuItemState, Alert, TextBox, OpenPanelOptions, PrintPanelOptions, SavePanelOptions } from 'markedit-api';
import { MenuItem, MenuItemState, ContextMenuOptions, Alert, TextBox, OpenPanelOptions, PrintPanelOptions, SavePanelOptions } from 'markedit-api';

import { WebMenuItem } from '../@types/WebMenuItem';
import { WebPoint } from '../@types/WebPoint';
Expand All @@ -15,12 +15,19 @@ export function addMainMenuItem(spec: MenuItem | MenuItem[]): void {
});
}

export function showContextMenu(items: MenuItem[], location?: WebPoint) {
export function showContextMenu(items: MenuItem[], location?: WebPoint, options?: ContextMenuOptions) {
let { highlightedIndex } = options ?? {};
if (highlightedIndex !== undefined && !Number.isSafeInteger(highlightedIndex)) {
console.error('showContextMenu: highlightedIndex must be a safe integer.', highlightedIndex);
highlightedIndex = undefined;
}

const editor = focusedEditor() ?? window.editor;
const caretPos = editor.state.selection.main.head;
const invokeNative = () => {
window.nativeModules.api.showContextMenu({
items: items.map(item => createMenuItem(item, contextActions)),
highlightedIndex: highlightedIndex === undefined ? undefined : (highlightedIndex as CodeGen_Int),
location: location ?? (() => {
const coords = editor.coordsAtPos(caretPos);
if (coords === null) {
Expand Down
2 changes: 1 addition & 1 deletion CoreEditor/src/bridge/native/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface NativeModuleAPI extends NativeModule {
closeDocument(): Promise<boolean>;
recentDocumentPaths(): Promise<string[]>;
addMainMenuItems({ items }: { items: WebMenuItem[] }): void;
showContextMenu(args: { items: WebMenuItem[]; location: WebPoint }): void;
showContextMenu(args: { items: WebMenuItem[]; location: WebPoint; highlightedIndex?: CodeGen_Int }): void;
showAlert(args: { title?: string; message?: string; buttons?: string[] }): Promise<CodeGen_Int>;
showTextBox(args: { title?: string; placeholder?: string; defaultValue?: string }): Promise<string | undefined>;
showOpenPanel({ options }: { options: OpenPanelOptions }): Promise<string[] | undefined>;
Expand Down
93 changes: 93 additions & 0 deletions CoreEditor/test/nestedEditors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,99 @@ describe('context menu caret geometry', () => {
expect(dispatch).not.toHaveBeenCalled();
});

test.each([{}, { highlightedIndex: 0 }])('uses the cell caret with options %j', options => {
jest.useFakeTimers();
const show = prepareMenu();
showContextMenu([{ title: 'First' }], undefined, options);
expect(show).not.toHaveBeenCalled();

jest.advanceTimersByTime(50);
expect(show).toHaveBeenCalledWith({
items: [expect.objectContaining({ title: 'First' })],
location: { x: 80, y: 70 },
highlightedIndex: options.highlightedIndex,
});
});

test.each([undefined, 0, 1, 2])('uses an explicit location with highlighted index %s', highlightedIndex => {
const show = prepareMenu();
const dispatch = jest.spyOn(cell, 'dispatch');

showContextMenu([{ title: 'First' }, { separator: true }, { title: 'Last' }], { x: 1, y: 2 }, {
highlightedIndex,
});

expect(show).toHaveBeenCalledWith({
items: [expect.objectContaining({ title: 'First' }), expect.objectContaining({ separator: true }), expect.objectContaining({ title: 'Last' })],
location: { x: 1, y: 2 },
highlightedIndex,
});

expect(dispatch).not.toHaveBeenCalled();
});

test.each([-1, 3, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER])(
'forwards out-of-range integer %s for native fallback',
highlightedIndex => {
const show = prepareMenu();
showContextMenu([
{ title: 'First' },
{ separator: true },
{ title: 'Disabled', state: () => ({ isEnabled: false }) },
], { x: 1, y: 2 }, { highlightedIndex });

expect(show).toHaveBeenCalledWith(expect.objectContaining({
location: { x: 1, y: 2 },
highlightedIndex,
}));
},
);

test.each([0.5, NaN, Infinity, -Infinity, Number.MAX_VALUE, Number.MAX_SAFE_INTEGER + 1, Number.MIN_SAFE_INTEGER - 1])(
'opens menus without a highlight for non-safe integer %s',
highlightedIndex => {
jest.useFakeTimers();
const show = prepareMenu();
jest.spyOn(console, 'error').mockImplementation(() => {});
for (const location of [undefined, { x: 1, y: 2 }]) {
showContextMenu([{ title: 'First' }], location, { highlightedIndex });
}

expect(show).toHaveBeenCalledTimes(1);
expect(show).toHaveBeenNthCalledWith(1, expect.objectContaining({
items: [expect.objectContaining({ title: 'First' })],
location: { x: 1, y: 2 },
highlightedIndex: undefined,
}));

jest.advanceTimersByTime(50);
expect(show).toHaveBeenCalledTimes(2);
expect(show).toHaveBeenNthCalledWith(2, expect.objectContaining({
items: [expect.objectContaining({ title: 'First' })],
location: { x: 80, y: 70 },
highlightedIndex: undefined,
}));
},
);

test('forwards an integer index for native fallback in an empty menu', () => {
const show = prepareMenu();
showContextMenu([], { x: 1, y: 2 }, { highlightedIndex: 0 });
expect(show).toHaveBeenCalledWith({ items: [], location: { x: 1, y: 2 }, highlightedIndex: 0 });
});

test('leaves item state evaluation to native code after scrolling to the caret', () => {
jest.useFakeTimers();
const show = prepareMenu();
const state = jest.fn(() => ({ isEnabled: false }));
showContextMenu([{ title: 'First', state }], undefined, { highlightedIndex: 0 });
expect(state).not.toHaveBeenCalled();

jest.advanceTimersByTime(50);
expect(state).not.toHaveBeenCalled();
expect(show).toHaveBeenCalledWith(expect.objectContaining({ highlightedIndex: 0 }));
});

test('falls back to document geometry when no editor has focus', () => {
jest.useFakeTimers();
const show = prepareMenu();
Expand Down
10 changes: 5 additions & 5 deletions CoreEditor/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4178,9 +4178,9 @@ __metadata:
languageName: node
linkType: hard

"markedit-api@https://github.com/MarkEdit-app/MarkEdit-api#v0.37.0":
version: 0.37.0
resolution: "markedit-api@https://github.com/MarkEdit-app/MarkEdit-api.git#commit=7bce1077f0471a7b92d63e7e3ee0c084cea23c61"
"markedit-api@https://github.com/MarkEdit-app/MarkEdit-api#v0.38.0":
version: 0.38.0
resolution: "markedit-api@https://github.com/MarkEdit-app/MarkEdit-api.git#commit=3f8f22bcb1067c00e1c22ae425a06ed8da710d89"
peerDependencies:
"@codemirror/autocomplete": ^6.0.0
"@codemirror/commands": ^6.0.0
Expand All @@ -4193,7 +4193,7 @@ __metadata:
"@lezer/highlight": ^1.0.0
"@lezer/lr": ^1.0.0
"@lezer/markdown": ^1.0.0
checksum: 10c0/f3b25689cc34e12c8ece581e7bcfeed43b343f64892c21cf52e86a7ffa4d5e2d9abb608f957bcb660fcebb4300b85da25fc1ddcdc3927732b052bc76b3251906
checksum: 10c0/15973f74e46056ebe74b3f9bac8fc250b8acafc226e0f363576e82016d7492c9ba54f017f38d10c5ed21727dd286f2022e6b591de51c6a619b189aae5f8a5e26
languageName: node
linkType: hard

Expand Down Expand Up @@ -4225,7 +4225,7 @@ __metadata:
globals: "npm:^15.15.0"
jest: "npm:^30.2.0"
jest-environment-jsdom: "npm:^30.2.0"
markedit-api: "https://github.com/MarkEdit-app/MarkEdit-api#v0.37.0"
markedit-api: "https://github.com/MarkEdit-app/MarkEdit-api#v0.38.0"
ts-gyb: "npm:^0.12.0"
ts-jest: "npm:^29.4.5"
tslib: "npm:^2.6.2"
Expand Down
16 changes: 10 additions & 6 deletions MarkEdit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
objects = {

/* Begin PBXBuildFile section */
0A1100013010000000000001 /* QuickSearchUI in Frameworks */ = {isa = PBXBuildFile; productRef = 0A1100013010000000000002 /* QuickSearchUI */; };
0A1100013010000000000003 /* AppDelegate+QuickSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1100013010000000000004 /* AppDelegate+QuickSearch.swift */; };
0A1100023010000000000001 /* QuickSearchCore in Frameworks */ = {isa = PBXBuildFile; productRef = 0A1100023010000000000002 /* QuickSearchCore */; };
84FB00F02C6C4AED00850E94 /* AppWritingTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB00EF2C6C4AED00850E94 /* AppWritingTools.swift */; };
8701D0A42CC12321006AF8C2 /* FileVersion in Frameworks */ = {isa = PBXBuildFile; productRef = 8701D0A32CC12321006AF8C2 /* FileVersion */; };
8703C175303E7DD300A896D0 /* QuickLookWindowSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8703C171303E7DCF00A896D0 /* QuickLookWindowSize.swift */; };
Expand All @@ -24,9 +27,6 @@
870F17682985285B00003DA7 /* EditorViewController+Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870F17672985285B00003DA7 /* EditorViewController+Config.swift */; };
870F177D29855CA500003DA7 /* MarkEditKit in Frameworks */ = {isa = PBXBuildFile; productRef = 870F177C29855CA500003DA7 /* MarkEditKit */; };
870F178E2985775B00003DA7 /* SharedUI in Frameworks */ = {isa = PBXBuildFile; productRef = 870F178D2985775B00003DA7 /* SharedUI */; };
0A1100013010000000000001 /* QuickSearchUI in Frameworks */ = {isa = PBXBuildFile; productRef = 0A1100013010000000000002 /* QuickSearchUI */; };
0A1100023010000000000001 /* QuickSearchCore in Frameworks */ = {isa = PBXBuildFile; productRef = 0A1100023010000000000002 /* QuickSearchCore */; };
0A1100013010000000000003 /* AppDelegate+QuickSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1100013010000000000004 /* AppDelegate+QuickSearch.swift */; };
870F17902985775B00003DA7 /* AppKitExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 870F178F2985775B00003DA7 /* AppKitExtensions */; };
870F17962985775B00003DA7 /* SettingsUI in Frameworks */ = {isa = PBXBuildFile; productRef = 870F17952985775B00003DA7 /* SettingsUI */; };
87132AA32C188FE900FD262B /* AppShortcuts.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 87132AA22C188FE900FD262B /* AppShortcuts.xcstrings */; };
Expand Down Expand Up @@ -95,7 +95,6 @@
87AF5957302379C4006E8C6A /* AppRelauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AF5956302379C3006E8C6A /* AppRelauncher.swift */; };
87B000000000000000000102 /* BookmarkStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B000000000000000000101 /* BookmarkStore.swift */; };
87B000000000000000000104 /* BookmarkStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B000000000000000000103 /* BookmarkStoreTests.swift */; };
87D700000000000000000001 /* QuickSearchMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D700000000000000000002 /* QuickSearchMenuTests.swift */; };
87B0D1022F1A4C0100B0D101 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 87B0D1012F1A4C0100B0D101 /* Localizable.xcstrings */; };
87B3ED5A2CBCA97D001FA6CE /* AppDocumentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B3ED592CBCA97D001FA6CE /* AppDocumentController.swift */; };
87B3ED5D2CBCADD6001FA6CE /* NSDocumentController+Extras.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B3ED5C2CBCADD6001FA6CE /* NSDocumentController+Extras.swift */; };
Expand All @@ -105,6 +104,8 @@
87BEF30129A88F6800596E17 /* AppCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BEF30029A88F6800596E17 /* AppCustomization.swift */; };
87C600000000000000000001 /* EditorDocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C600000000000000000002 /* EditorDocumentTests.swift */; };
87CC48FD29CC01E100BE1441 /* TextBundle in Frameworks */ = {isa = PBXBuildFile; productRef = 87CC48FC29CC01E100BE1441 /* TextBundle */; };
87D700000000000000000001 /* QuickSearchMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D700000000000000000002 /* QuickSearchMenuTests.swift */; };
87D800000000000000000001 /* ContextMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D800000000000000000002 /* ContextMenuTests.swift */; };
87D9322E2A925C6700B20D84 /* EditorReplaceTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D9322D2A925C6700B20D84 /* EditorReplaceTextField.swift */; };
87DA0000000000000000E00B /* UpdateInstaller.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 87DA0000000000000000E000 /* UpdateInstaller.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
87DA0000000000000000E020 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DA0000000000000000E010 /* main.swift */; };
Expand Down Expand Up @@ -189,6 +190,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
0A1100013010000000000004 /* AppDelegate+QuickSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+QuickSearch.swift"; sourceTree = "<group>"; };
84FB00EF2C6C4AED00850E94 /* AppWritingTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppWritingTools.swift; sourceTree = "<group>"; };
8703C171303E7DCF00A896D0 /* QuickLookWindowSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookWindowSize.swift; sourceTree = "<group>"; };
87067E2629755D5C0052E795 /* EditorStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorStatusView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -276,7 +278,6 @@
87AF5956302379C3006E8C6A /* AppRelauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRelauncher.swift; sourceTree = "<group>"; };
87B000000000000000000101 /* BookmarkStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkStore.swift; sourceTree = "<group>"; };
87B000000000000000000103 /* BookmarkStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkStoreTests.swift; sourceTree = "<group>"; };
87D700000000000000000002 /* QuickSearchMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSearchMenuTests.swift; sourceTree = "<group>"; };
87B0D1012F1A4C0100B0D101 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
87B3ED592CBCA97D001FA6CE /* AppDocumentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDocumentController.swift; sourceTree = "<group>"; };
87B3ED5C2CBCADD6001FA6CE /* NSDocumentController+Extras.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSDocumentController+Extras.swift"; sourceTree = "<group>"; };
Expand All @@ -287,6 +288,8 @@
87C3CBFA29545944002A3436 /* MarkEditKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MarkEditKit; sourceTree = "<group>"; };
87C600000000000000000002 /* EditorDocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorDocumentTests.swift; sourceTree = "<group>"; };
87C600000000000000000004 /* MarkEditMacTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MarkEditMacTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
87D700000000000000000002 /* QuickSearchMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSearchMenuTests.swift; sourceTree = "<group>"; };
87D800000000000000000002 /* ContextMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuTests.swift; sourceTree = "<group>"; };
87D9322D2A925C6700B20D84 /* EditorReplaceTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorReplaceTextField.swift; sourceTree = "<group>"; };
87DA0000000000000000E000 /* UpdateInstaller.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = UpdateInstaller.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
87DA0000000000000000E010 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
Expand All @@ -307,7 +310,6 @@
B0E02F672D8F77A50060DEA5 /* MarkEdit.sdef */ = {isa = PBXFileReference; lastKnownFileType = text; path = MarkEdit.sdef; sourceTree = "<group>"; };
CC000002300000000000001B /* EditorDocument+ClosedTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorDocument+ClosedTab.swift"; sourceTree = "<group>"; };
CC000003300000000000001B /* AppDelegate+ClosedTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+ClosedTab.swift"; sourceTree = "<group>"; };
0A1100013010000000000004 /* AppDelegate+QuickSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+QuickSearch.swift"; sourceTree = "<group>"; };
CC000004300000000000001B /* EditorSelectionHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorSelectionHistory.swift; sourceTree = "<group>"; };
CC000006300000000000001B /* EditorClosedTabHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorClosedTabHistory.swift; sourceTree = "<group>"; };
EE100007EEEE000000000007 /* ExtensionInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionInstaller.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -727,6 +729,7 @@
87C600000000000000000002 /* EditorDocumentTests.swift */,
87B000000000000000000103 /* BookmarkStoreTests.swift */,
87D700000000000000000002 /* QuickSearchMenuTests.swift */,
87D800000000000000000002 /* ContextMenuTests.swift */,
);
path = Tests;
sourceTree = "<group>";
Expand Down Expand Up @@ -1082,6 +1085,7 @@
87C600000000000000000001 /* EditorDocumentTests.swift in Sources */,
87B000000000000000000104 /* BookmarkStoreTests.swift in Sources */,
87D700000000000000000001 /* QuickSearchMenuTests.swift in Sources */,
87D800000000000000000001 /* ContextMenuTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public protocol NativeModuleAPI: NativeModule {
func closeDocument() async -> Bool
func recentDocumentPaths() async -> [String]
func addMainMenuItems(items: [WebMenuItem])
func showContextMenu(items: [WebMenuItem], location: WebPoint)
func showContextMenu(items: [WebMenuItem], location: WebPoint, highlightedIndex: Int?)
func showAlert(title: String?, message: String?, buttons: [String]?) async -> Int
func showTextBox(title: String?, placeholder: String?, defaultValue: String?) async -> String?
func showOpenPanel(options: OpenPanelOptions) async -> [String]?
Expand Down Expand Up @@ -191,11 +191,13 @@ final class NativeBridgeAPI: NativeBridge {
struct Message: Decodable {
var items: [WebMenuItem]
var location: WebPoint
var highlightedIndex: Int?

init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: BridgeFieldKey.self)
items = try container.value("items")
location = try container.value("location")
highlightedIndex = try container.value("highlightedIndex")
}
}

Expand All @@ -207,7 +209,7 @@ final class NativeBridgeAPI: NativeBridge {
return .failure(error)
}

module.showContextMenu(items: message.items, location: message.location)
module.showContextMenu(items: message.items, location: message.location, highlightedIndex: message.highlightedIndex)
return .success(nil)
}

Expand Down
Loading
Loading