diff --git a/macos/CMakeLists.txt b/macos/CMakeLists.txt index 525f80cb12de..23e97390b5cb 100644 --- a/macos/CMakeLists.txt +++ b/macos/CMakeLists.txt @@ -384,6 +384,8 @@ add_executable(MacNotePlusPlus "${CMAKE_CURRENT_SOURCE_DIR}/platform/print_support.mm" "${CMAKE_CURRENT_SOURCE_DIR}/platform/hash_tools.mm" "${CMAKE_CURRENT_SOURCE_DIR}/platform/macro_manager.mm" + "${CMAKE_CURRENT_SOURCE_DIR}/platform/plugin_manager.mm" + "${CMAKE_CURRENT_SOURCE_DIR}/platform/nppm_handler.mm" ) target_include_directories(MacNotePlusPlus PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/platform" @@ -392,6 +394,7 @@ target_include_directories(MacNotePlusPlus PRIVATE "${SCINTILLA_INCLUDE_DIR}" "${LEXILLA_INCLUDE_DIR}" "${UCHARDET_DIR}" + "${NPP_SRC_DIR}/MISC/PluginsManager" ) target_link_libraries(MacNotePlusPlus win32shim @@ -410,6 +413,8 @@ target_compile_options(MacNotePlusPlus PRIVATE -fobjc-arc -Wno-deprecated-declarations ) +# Export symbols so plugins loaded via dlopen can resolve host functions (SendMessageW, etc.) +target_link_options(MacNotePlusPlus PRIVATE -Wl,-export_dynamic) add_dependencies(MacNotePlusPlus AppIcon) add_custom_command(TARGET MacNotePlusPlus POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different @@ -421,6 +426,36 @@ add_custom_command(TARGET MacNotePlusPlus POST_BUILD COMMENT "Copying logo.png and AppIcon.icns" ) +# ============================================================ +# Sample plugin: HelloMacNote +# ============================================================ +add_library(HelloMacNote MODULE + "${CMAKE_CURRENT_SOURCE_DIR}/plugin-sdk/example/HelloMacNote/HelloMacNote.cpp" +) +target_include_directories(HelloMacNote PRIVATE + "${SHIM_INCLUDE_DIR}" + "${NPP_SRC_DIR}/MISC/PluginsManager" + "${SCINTILLA_INCLUDE_DIR}" +) +set_target_properties(HelloMacNote PROPERTIES + PREFIX "" + SUFFIX ".dylib" + OUTPUT_NAME "HelloMacNote" +) +target_compile_options(HelloMacNote PRIVATE -Wno-deprecated-declarations) +target_link_options(HelloMacNote PRIVATE -undefined dynamic_lookup) + +# Opt-in install target: cmake --build . --target install_sample_plugin +add_custom_target(install_sample_plugin + DEPENDS HelloMacNote + COMMAND ${CMAKE_COMMAND} -E make_directory + "$ENV{HOME}/Library/Application Support/MacNote++/plugins/HelloMacNote" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "$ENV{HOME}/Library/Application Support/MacNote++/plugins/HelloMacNote/HelloMacNote.dylib" + COMMENT "Installing HelloMacNote plugin to ~/Library/Application Support/MacNote++/plugins/" +) + # ============================================================ # Packaging: App bundle, code signing, and DMG # ============================================================ diff --git a/macos/platform/app_delegate.mm b/macos/platform/app_delegate.mm index a994f683f011..14ecc779ff49 100644 --- a/macos/platform/app_delegate.mm +++ b/macos/platform/app_delegate.mm @@ -20,6 +20,8 @@ #include "scintilla_bridge.h" #include "handle_registry.h" #include "settings_manager.h" +#include "plugin_manager.h" +#include "Notepad_plus_msgs.h" #include "file_monitor_mac.h" #include "brace_match.h" #include "smart_highlight.h" @@ -251,12 +253,41 @@ - (void)applicationDidFinishLaunching:(NSNotification*)notification return; } + // Register main Scintilla view with HandleRegistry so SendMessage(sciHandle, SCI_*, ...) works + { + HandleRegistry::WindowInfo sciInfo{}; + sciInfo.nativeView = ctx().scintillaView; + sciInfo.className = L"Scintilla"; + sciInfo.isScintilla = true; + sciInfo.parent = ctx().mainHwnd; + ctx().scintillaMainHwnd = HandleRegistry::createWindow(std::move(sciInfo)); + } + configureScintilla(ctx().scintillaView); applyAppearance(); if (ctx().documentMapEnabled) initializeDocumentMap(); setSyncScrollingEnabled(ctx().syncScrolling); + // Pre-create the second Scintilla view (hidden) so plugins always have a valid handle. + // doSplit() will reuse this view instead of creating a new one. + { + NSView* hiddenContainer = [[NSView alloc] initWithFrame:NSZeroRect]; + hiddenContainer.hidden = YES; + [ctx().editorContainer addSubview:hiddenContainer]; + ctx().scintillaView2 = ScintillaBridge_createView((__bridge void*)hiddenContainer, 0, 0, 0, 0); + if (ctx().scintillaView2) + { + HandleRegistry::WindowInfo sci2Info{}; + sci2Info.nativeView = ctx().scintillaView2; + sci2Info.className = L"Scintilla"; + sci2Info.isScintilla = true; + sci2Info.parent = ctx().mainHwnd; + ctx().scintillaSecondHwnd = HandleRegistry::createWindow(std::move(sci2Info)); + configureScintilla(ctx().scintillaView2); + } + } + // Scintilla notification callback for main view ScintillaBridge_setNotifyCallback(ctx().scintillaView, (intptr_t)ctx().mainHwnd, [](intptr_t windowid, unsigned int iMessage, uintptr_t wParam, uintptr_t lParam) { @@ -368,6 +399,9 @@ - (void)applicationDidFinishLaunching:(NSNotification*)notification if (MacroManager::instance().isRecording()) MacroManager::instance().recordStep(scn->message, scn->wParam, scn->lParam); } + + // Forward Scintilla notifications to plugins + pluginManager().notify(reinterpret_cast(scn)); } }); @@ -515,6 +549,25 @@ - (void)applicationDidFinishLaunching:(NSNotification*)notification bindDocumentMapToActiveView(); updateDocumentMapViewport(); + // Initialize plugin system + { + NppData nppData; + nppData._nppHandle = ctx().mainHwnd; + nppData._scintillaMainHandle = ctx().scintillaMainHwnd; + nppData._scintillaSecondHandle = ctx().scintillaSecondHwnd; + pluginManager().init(nppData); + pluginManager().loadPlugins(); + pluginManager().initMenu(getPluginsMenuHandle()); + } + + // Notify plugins that initialization is complete + { + SCNotification readyNotif{}; + readyNotif.nmhdr.hwndFrom = ctx().mainHwnd; + readyNotif.nmhdr.code = NPPN_READY; + pluginManager().notify(&readyNotif); + } + NSLog(@"=== Notepad++ macOS Port — Phase 7 ==="); NSLog(@"Settings, split view, edit commands, encoding, session, drag-and-drop!"); } @@ -590,6 +643,26 @@ - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender - (void)applicationWillTerminate:(NSNotification*)notification { + // Notify plugins that we are shutting down + { + SCNotification shutdownNotif{}; + shutdownNotif.nmhdr.hwndFrom = ctx().mainHwnd; + shutdownNotif.nmhdr.code = NPPN_SHUTDOWN; + pluginManager().notify(&shutdownNotif); + } + + // Destroy Scintilla HWNDs to release HandleRegistry-retained native views + if (ctx().scintillaSecondHwnd) + { + HandleRegistry::destroyWindow(ctx().scintillaSecondHwnd); + ctx().scintillaSecondHwnd = nullptr; + } + if (ctx().scintillaMainHwnd) + { + HandleRegistry::destroyWindow(ctx().scintillaMainHwnd); + ctx().scintillaMainHwnd = nullptr; + } + saveSession(); auto& s = SettingsManager::instance().settings; diff --git a/macos/platform/app_state.h b/macos/platform/app_state.h index 403a6041c973..cb4b9695078f 100644 --- a/macos/platform/app_state.h +++ b/macos/platform/app_state.h @@ -24,6 +24,7 @@ struct AppContext { // Main editor void* scintillaView = nullptr; + HWND scintillaMainHwnd = nullptr; NSWindow* mainWindow = nil; HWND mainHwnd = nullptr; HWND tabHwnd = nullptr; @@ -62,6 +63,7 @@ struct AppContext // Split view state void* scintillaView2 = nullptr; + HWND scintillaSecondHwnd = nullptr; NSSplitView* splitView = nil; bool isSplit = false; int activeView = 0; // 0=main, 1=sub diff --git a/macos/platform/document_manager.mm b/macos/platform/document_manager.mm index 754ef40eea99..c0f598d5dcc8 100644 --- a/macos/platform/document_manager.mm +++ b/macos/platform/document_manager.mm @@ -14,6 +14,8 @@ #include "function_list_panel.h" #include "file_switcher_panel.h" #include "sync_scroll.h" +#include "plugin_manager.h" +#include "Notepad_plus_msgs.h" #include "windows.h" #include "commctrl.h" #include "handle_registry.h" @@ -224,6 +226,15 @@ void closeTabFromView(int viewIndex, int tabIndex) return; if (!sci) return; + // Notify plugins that a file is about to be closed + { + SCNotification notif{}; + notif.nmhdr.hwndFrom = ctx().mainHwnd; + notif.nmhdr.code = NPPN_FILEBEFORECLOSE; + notif.nmhdr.idFrom = 0; // V1: no stable buffer ID yet + pluginManager().notify(¬if); + } + if (docs.size() <= 1) { if (docs[0].functionListDocumentId != 0) @@ -248,6 +259,15 @@ void closeTabFromView(int viewIndex, int tabIndex) [ctx().mainWindow setTitle:@"Notepad++ — Untitled"]; updateTabModifiedIndicator(viewIndex, 0); updateWindowDocumentEdited(); + + // Notify plugins that the file has been closed + { + SCNotification notif{}; + notif.nmhdr.hwndFrom = ctx().mainHwnd; + notif.nmhdr.code = NPPN_FILECLOSED; + notif.nmhdr.idFrom = 0; + pluginManager().notify(¬if); + } return; } @@ -281,6 +301,15 @@ void closeTabFromView(int viewIndex, int tabIndex) NSString* title = WideToNSString(doc.title.c_str()); [ctx().mainWindow setTitle:[NSString stringWithFormat:@"Notepad++ — %@", title]]; updateWindowDocumentEdited(); + + // Notify plugins that the file has been closed + { + SCNotification notif{}; + notif.nmhdr.hwndFrom = ctx().mainHwnd; + notif.nmhdr.code = NPPN_FILECLOSED; + notif.nmhdr.idFrom = 0; + pluginManager().notify(¬if); + } } void closeTab(int tabIndex) diff --git a/macos/platform/file_operations.mm b/macos/platform/file_operations.mm index b3cb830205e3..62c5bc983d1d 100644 --- a/macos/platform/file_operations.mm +++ b/macos/platform/file_operations.mm @@ -16,6 +16,8 @@ #include "scintilla_bridge.h" #include "file_monitor_mac.h" #include "uchardet.h" +#include "plugin_manager.h" +#include "Notepad_plus_msgs.h" #include "windows.h" #include "commdlg.h" #include "commctrl.h" @@ -206,6 +208,16 @@ bool openFileAtPath(NSString* path) addRecentFile(wpath); rebuildRecentMenu(); updateStatusBar(); + + // Notify plugins that a file was opened + { + SCNotification notif{}; + notif.nmhdr.hwndFrom = ctx().mainHwnd; + notif.nmhdr.code = NPPN_FILEOPENED; + notif.nmhdr.idFrom = 0; // V1: no stable buffer ID yet + pluginManager().notify(¬if); + } + return true; } @@ -322,6 +334,15 @@ void saveCurrentFile() addRecentFile(doc.filePath); rebuildRecentMenu(); + + // Notify plugins that the file was saved + { + SCNotification notif{}; + notif.nmhdr.hwndFrom = ctx().mainHwnd; + notif.nmhdr.code = NPPN_FILESAVED; + notif.nmhdr.idFrom = 0; // V1: no stable buffer ID yet + pluginManager().notify(¬if); + } } delete[] buf; diff --git a/macos/platform/menu_builder.h b/macos/platform/menu_builder.h index 13cf9f688fea..53e88921cf5c 100644 --- a/macos/platform/menu_builder.h +++ b/macos/platform/menu_builder.h @@ -7,3 +7,6 @@ #include "windows.h" HMENU buildMenuBar(); + +// Returns the Plugins submenu handle (populated by MacPluginManager after loading) +HMENU getPluginsMenuHandle(); diff --git a/macos/platform/menu_builder.mm b/macos/platform/menu_builder.mm index 15a54de28110..71eefdcbf7df 100644 --- a/macos/platform/menu_builder.mm +++ b/macos/platform/menu_builder.mm @@ -8,6 +8,8 @@ #include "language_defs.h" #include "windows.h" +static HMENU s_pluginsMenuHandle = nullptr; + HMENU buildMenuBar() { HMENU hMenuBar = CreateMenu(); @@ -196,6 +198,11 @@ HMENU buildMenuBar() AppendMenuW(hToolsMenu, MF_POPUP, reinterpret_cast(hHashMenu), L"&Hash"); AppendMenuW(hMenuBar, MF_POPUP, reinterpret_cast(hToolsMenu), L"&Tools"); + // Plugins menu (populated by MacPluginManager after loading) + HMENU hPluginsMenu = CreatePopupMenu(); + s_pluginsMenuHandle = hPluginsMenu; + AppendMenuW(hMenuBar, MF_POPUP, reinterpret_cast(hPluginsMenu), L"&Plugins"); + // Language menu HMENU hLangMenu = CreatePopupMenu(); for (int i = 0; i < g_numLanguages; ++i) @@ -213,3 +220,8 @@ HMENU buildMenuBar() return hMenuBar; } + +HMENU getPluginsMenuHandle() +{ + return s_pluginsMenuHandle; +} diff --git a/macos/platform/nppm_handler.h b/macos/platform/nppm_handler.h new file mode 100644 index 000000000000..ec8143c83b94 --- /dev/null +++ b/macos/platform/nppm_handler.h @@ -0,0 +1,9 @@ +// nppm_handler.h — NPPM_* and RUNCOMMAND_USER message handlers +// Handles plugin messages sent via SendMessage(nppHandle, NPPM_*, ...) + +#pragma once + +#include "windows.h" + +LRESULT handleNppmMessage(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); +LRESULT handleRunCommandMessage(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); diff --git a/macos/platform/nppm_handler.mm b/macos/platform/nppm_handler.mm new file mode 100644 index 000000000000..b379b5404e7d --- /dev/null +++ b/macos/platform/nppm_handler.mm @@ -0,0 +1,307 @@ +// nppm_handler.mm — NPPM_* and RUNCOMMAND_USER message handlers + +#import +#include "nppm_handler.h" +#include "app_state.h" +#include "plugin_manager.h" +#include "scintilla_bridge.h" +#include "language_defs.h" +#include "lexer_styles.h" +#include "Notepad_plus_msgs.h" +#include "Scintilla.h" + +#include +#include + +// ============================================================ +// Helper: copy wide string to plugin buffer with two-call contract +// ============================================================ +static LRESULT copyWideToBuffer(const std::wstring& src, WPARAM maxChars, LPARAM lParam) +{ + if (!lParam) + { + // First call: return number of wchar_t needed (not including null) + return static_cast(src.size()); + } + + // Fail without writing when the buffer is too small (matches upstream behavior). + // Also reject maxChars==0 with a non-null buffer to prevent overflow. + if (maxChars == 0 || src.size() >= static_cast(maxChars)) + return 0; + + wchar_t* buf = reinterpret_cast(lParam); + const size_t copyLen = src.size(); + wcsncpy(buf, src.c_str(), copyLen); + buf[copyLen] = L'\0'; + return TRUE; +} + +// ============================================================ +// NPPMSG range (WM_USER + 1000) +// ============================================================ +LRESULT handleNppmMessage(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + auto& docs = ctx().activeDocuments(); + int tabIdx = ctx().activeTabIndex(); + + switch (msg) + { + case NPPM_GETCURRENTSCINTILLA: + { + if (lParam) + *reinterpret_cast(lParam) = ctx().activeView; + return TRUE; + } + + case NPPM_GETCURRENTLANGTYPE: + { + if (lParam && tabIdx >= 0 && tabIdx < static_cast(docs.size())) + *reinterpret_cast(lParam) = docs[tabIdx].languageIndex; + return TRUE; + } + + case NPPM_SETCURRENTLANGTYPE: + { + int langType = static_cast(lParam); + if (tabIdx >= 0 && tabIdx < static_cast(docs.size())) + { + docs[tabIdx].languageIndex = langType; + applyLanguage(langType); + + // Notify plugins that the language changed + SCNotification notif{}; + notif.nmhdr.hwndFrom = hWnd; + notif.nmhdr.code = NPPN_LANGCHANGED; + notif.nmhdr.idFrom = 0; + pluginManager().notify(¬if); + } + return TRUE; + } + + case NPPM_GETNBOPENFILES: + { + int viewType = static_cast(lParam); + if (viewType == 1) // PRIMARY_VIEW + return static_cast(ctx().documents.size()); + else if (viewType == 2) // SECOND_VIEW + return static_cast(ctx().documents2.size()); + else // ALL_OPEN_FILES — deduplicate split-view mirrors + { + size_t count = ctx().documents.size(); + for (const auto& doc2 : ctx().documents2) + { + bool duplicate = false; + if (!doc2.filePath.empty()) + { + for (const auto& doc1 : ctx().documents) + { + if (doc1.filePath == doc2.filePath) + { + duplicate = true; + break; + } + } + } + if (!duplicate) + ++count; + } + return static_cast(count); + } + } + + case NPPM_SETMENUITEMCHECK: + { + HMENU hMenu = nullptr; // Search across all menus + (void)hMenu; + ::CheckMenuItem(GetMenu(hWnd), static_cast(wParam), + lParam ? MF_CHECKED : MF_UNCHECKED); + return TRUE; + } + + case NPPM_GETPLUGINSCONFIGDIR: + { + @autoreleasepool { + NSString* configDir = [@"~/Library/Application Support/MacNote++/plugins/Config" + stringByExpandingTildeInPath]; + [[NSFileManager defaultManager] createDirectoryAtPath:configDir + withIntermediateDirectories:YES attributes:nil error:nil]; + std::wstring wConfigDir; + NSData* data = [configDir dataUsingEncoding:NSUTF32LittleEndianStringEncoding]; + if (data) + wConfigDir = std::wstring(reinterpret_cast(data.bytes), + data.length / sizeof(wchar_t)); + return copyWideToBuffer(wConfigDir, wParam, lParam); + } + } + + case NPPM_MENUCOMMAND: + { + ::SendMessageW(hWnd, WM_COMMAND, static_cast(lParam), 0); + return TRUE; + } + + case NPPM_GETNPPVERSION: + { + // Return version as MAKELONG(minor, major) + // Major = 1, Minor = 0 for MacNote++ 1.0 + int major = 1; + int minor = 0; + if (wParam) // ADD_ZERO_PADDING + minor = 0; // Already zero-padded + return MAKELONG(minor, major); + } + + case NPPM_ALLOCATECMDID: + { + int* startNumber = reinterpret_cast(lParam); + if (!startNumber) + return FALSE; + return pluginManager().allocateCmdID(static_cast(wParam), startNumber) ? TRUE : FALSE; + } + + case NPPM_ALLOCATEMARKER: + { + int* startNumber = reinterpret_cast(lParam); + if (!startNumber) + return FALSE; + return pluginManager().allocateMarker(static_cast(wParam), startNumber) ? TRUE : FALSE; + } + + case NPPM_GETCURRENTVIEW: + return static_cast(ctx().activeView); + + case NPPM_ALLOCATEINDICATOR: + { + int* startNumber = reinterpret_cast(lParam); + if (!startNumber) + return FALSE; + return pluginManager().allocateIndicator(static_cast(wParam), startNumber) ? TRUE : FALSE; + } + + default: + NSLog(@"Unhandled NPPM message: 0x%X (offset +%d)", msg, msg - NPPMSG); + return 0; + } +} + +// ============================================================ +// RUNCOMMAND_USER range (WM_USER + 3000) +// ============================================================ +LRESULT handleRunCommandMessage(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + auto& docs = ctx().activeDocuments(); + int tabIdx = ctx().activeTabIndex(); + std::wstring filePath; + + if (tabIdx >= 0 && tabIdx < static_cast(docs.size())) + filePath = docs[tabIdx].filePath; + + switch (msg) + { + case NPPM_GETFULLCURRENTPATH: + return copyWideToBuffer(filePath, wParam, lParam); + + case NPPM_GETCURRENTDIRECTORY: + { + if (filePath.empty()) + return copyWideToBuffer(L"", wParam, lParam); + namespace fs = std::filesystem; + fs::path p(filePath); + return copyWideToBuffer(p.parent_path().wstring(), wParam, lParam); + } + + case NPPM_GETFILENAME: + { + if (filePath.empty()) + return copyWideToBuffer(L"", wParam, lParam); + namespace fs = std::filesystem; + fs::path p(filePath); + return copyWideToBuffer(p.filename().wstring(), wParam, lParam); + } + + case NPPM_GETNAMEPART: + { + if (filePath.empty()) + return copyWideToBuffer(L"", wParam, lParam); + namespace fs = std::filesystem; + fs::path p(filePath); + return copyWideToBuffer(p.stem().wstring(), wParam, lParam); + } + + case NPPM_GETEXTPART: + { + if (filePath.empty()) + return copyWideToBuffer(L"", wParam, lParam); + namespace fs = std::filesystem; + fs::path p(filePath); + return copyWideToBuffer(p.extension().wstring(), wParam, lParam); + } + + case NPPM_GETCURRENTWORD: + { + void* sci = ctx().activeScintillaView(); + if (!sci) + return copyWideToBuffer(L"", wParam, lParam); + + // If there is a selection, return the selected text (matches upstream behavior). + // Only fall back to word-boundary logic when there is no selection. + intptr_t selStart = ScintillaBridge_sendMessage(sci, SCI_GETSELECTIONSTART, 0, 0); + intptr_t selEnd = ScintillaBridge_sendMessage(sci, SCI_GETSELECTIONEND, 0, 0); + + intptr_t rangeStart, rangeEnd; + if (selEnd > selStart) + { + rangeStart = selStart; + rangeEnd = selEnd; + } + else + { + intptr_t pos = ScintillaBridge_sendMessage(sci, SCI_GETCURRENTPOS, 0, 0); + rangeStart = ScintillaBridge_sendMessage(sci, SCI_WORDSTARTPOSITION, pos, 1); + rangeEnd = ScintillaBridge_sendMessage(sci, SCI_WORDENDPOSITION, pos, 1); + } + + if (rangeEnd <= rangeStart) + return copyWideToBuffer(L"", wParam, lParam); + + intptr_t len = rangeEnd - rangeStart; + std::string utf8(static_cast(len) + 1, '\0'); + Sci_TextRangeFull tr{}; + tr.chrg.cpMin = rangeStart; + tr.chrg.cpMax = rangeEnd; + tr.lpstrText = utf8.data(); + ScintillaBridge_sendMessage(sci, SCI_GETTEXTRANGEFULL, 0, reinterpret_cast(&tr)); + + // Convert UTF-8 to wide + @autoreleasepool { + NSString* str = [NSString stringWithUTF8String:utf8.c_str()]; + if (!str) return copyWideToBuffer(L"", wParam, lParam); + NSData* data = [str dataUsingEncoding:NSUTF32LittleEndianStringEncoding]; + if (!data) return copyWideToBuffer(L"", wParam, lParam); + std::wstring wWord(reinterpret_cast(data.bytes), + data.length / sizeof(wchar_t)); + return copyWideToBuffer(wWord, wParam, lParam); + } + } + + case NPPM_GETCURRENTLINE: + { + void* sci = ctx().activeScintillaView(); + if (!sci) return 0; + intptr_t pos = ScintillaBridge_sendMessage(sci, SCI_GETCURRENTPOS, 0, 0); + return static_cast(ScintillaBridge_sendMessage(sci, SCI_LINEFROMPOSITION, pos, 0)); + } + + case NPPM_GETCURRENTCOLUMN: + { + void* sci = ctx().activeScintillaView(); + if (!sci) return 0; + intptr_t pos = ScintillaBridge_sendMessage(sci, SCI_GETCURRENTPOS, 0, 0); + return static_cast(ScintillaBridge_sendMessage(sci, SCI_GETCOLUMN, pos, 0)); + } + + default: + NSLog(@"Unhandled RUNCOMMAND_USER message: 0x%X (offset +%d)", msg, msg - RUNCOMMAND_USER); + return 0; + } +} diff --git a/macos/platform/plugin_manager.h b/macos/platform/plugin_manager.h new file mode 100644 index 000000000000..789510981ff7 --- /dev/null +++ b/macos/platform/plugin_manager.h @@ -0,0 +1,86 @@ +// plugin_manager.h — macOS plugin loading and management +// Source-code-compatible plugin system for MacNote++. +// Loads .dylib plugins using the same PluginInterface.h contract as Notepad++. + +#pragma once + +#include +#include +#include + +#include "windows.h" +#include "PluginInterface.h" +#include "IDAllocator.h" + +struct MacPluginInfo +{ + MacPluginInfo() = default; + ~MacPluginInfo(); + + HINSTANCE _hLib = nullptr; + HMENU _pluginMenu = nullptr; + + PFUNCSETINFO _pFuncSetInfo = nullptr; + PFUNCGETNAME _pFuncGetName = nullptr; + PBENOTIFIED _pBeNotified = nullptr; + PFUNCGETFUNCSARRAY _pFuncGetFuncsArray = nullptr; + PMESSAGEPROC _pMessageProc = nullptr; + + FuncItem* _funcItems = nullptr; + int _nbFuncItem = 0; + std::wstring _moduleName; + std::wstring _displayName; + + // Index of first command in the global _pluginsCommands vector + int _cmdIdBase = 0; +}; + +struct PluginCommand +{ + std::wstring _pluginName; + PFUNCPLUGINCMD _pFunc = nullptr; +}; + +class MacPluginManager +{ +public: + MacPluginManager(); + ~MacPluginManager() = default; + + void init(const NppData& nppData) { _nppData = nppData; } + + bool loadPlugins(); + int loadPluginFromPath(const std::wstring& pluginFilePath); + + HMENU initMenu(HMENU hPluginsMenu); + + void runPluginCommand(int index); + + void notify(const SCNotification* notification); + void relayNppMessages(UINT Message, WPARAM wParam, LPARAM lParam); + + bool inDynamicRange(int id) const { return _dynamicCmdAlloc.isInRange(id); } + + bool allocateCmdID(int numberRequired, int* start); + bool allocateMarker(int numberRequired, int* start); + bool allocateIndicator(int numberRequired, int* start); + + bool hasPlugins() const { return !_pluginInfos.empty(); } + +private: + NppData _nppData{}; + HMENU _hPluginsMenu = nullptr; + + std::vector> _pluginInfos; + std::vector _pluginsCommands; + + IDAllocator _staticCmdAlloc; + IDAllocator _dynamicCmdAlloc; + IDAllocator _markerAlloc; + IDAllocator _indicatorAlloc; + + bool _noMoreNotification = false; +}; + +// Singleton accessor +MacPluginManager& pluginManager(); diff --git a/macos/platform/plugin_manager.mm b/macos/platform/plugin_manager.mm new file mode 100644 index 000000000000..3009519635a2 --- /dev/null +++ b/macos/platform/plugin_manager.mm @@ -0,0 +1,432 @@ +// plugin_manager.mm — macOS plugin loading and management +// Loads .dylib plugins from ~/Library/Application Support/MacNote++/plugins/ + +#import +#include "plugin_manager.h" +#include "handle_registry.h" + +#include +#include +#include + +// Command ID ranges from upstream resource.h +#define ID_PLUGINS_CMD 22000 +#define ID_PLUGINS_CMD_LIMIT 22999 +#define ID_PLUGINS_CMD_DYNAMIC 23000 +#define ID_PLUGINS_CMD_DYNAMIC_LIMIT 24999 +#define MARKER_PLUGINS 1 +#define MARKER_PLUGINS_LIMIT 15 +#define INDICATOR_PLUGINS 8 +#define INDICATOR_PLUGINS_LIMIT 20 + +using PFUNCISUNICODE = BOOL (__cdecl*)(); + +// ============================================================ +// MacPluginInfo +// ============================================================ + +MacPluginInfo::~MacPluginInfo() +{ + if (_pluginMenu) + ::DestroyMenu(_pluginMenu); + if (_hLib) + ::FreeLibrary(_hLib); +} + +// ============================================================ +// Helpers +// ============================================================ + +static std::string wideToUTF8(const std::wstring& wide) +{ + if (wide.empty()) return ""; + NSString* str = [[NSString alloc] initWithBytes:wide.data() + length:wide.size() * sizeof(wchar_t) + encoding:NSUTF32LittleEndianStringEncoding]; + return str ? std::string([str UTF8String]) : ""; +} + +static std::wstring utf8ToWide(const char* utf8) +{ + if (!utf8) return L""; + NSString* str = [NSString stringWithUTF8String:utf8]; + if (!str) return L""; + NSData* data = [str dataUsingEncoding:NSUTF32LittleEndianStringEncoding]; + if (!data || data.length < sizeof(wchar_t)) return L""; + return std::wstring(reinterpret_cast(data.bytes), + data.length / sizeof(wchar_t)); +} + +// Validate that a Mach-O binary matches the current architecture +static bool validateMachOArchitecture(const std::string& path) +{ + FILE* f = fopen(path.c_str(), "rb"); + if (!f) + return false; + + uint32_t magic = 0; + if (fread(&magic, sizeof(magic), 1, f) != 1) + { + fclose(f); + return false; + } + + bool valid = false; + +#ifdef __arm64__ + cpu_type_t expectedCpu = CPU_TYPE_ARM64; +#else + cpu_type_t expectedCpu = CPU_TYPE_X86_64; +#endif + + if (magic == MH_MAGIC_64) + { + // 64-bit Mach-O: read cputype from header + fseek(f, 0, SEEK_SET); + struct mach_header_64 header; + if (fread(&header, sizeof(header), 1, f) == 1) + valid = (header.cputype == expectedCpu); + } + else if (magic == FAT_MAGIC || magic == FAT_CIGAM) + { + // Universal binary: check if it contains our architecture + fseek(f, 0, SEEK_SET); + struct fat_header fh; + if (fread(&fh, sizeof(fh), 1, f) == 1) + { + uint32_t nArch = (magic == FAT_CIGAM) ? OSSwapInt32(fh.nfat_arch) : fh.nfat_arch; + for (uint32_t i = 0; i < nArch && !valid; ++i) + { + struct fat_arch arch; + if (fread(&arch, sizeof(arch), 1, f) == 1) + { + cpu_type_t cpu = (magic == FAT_CIGAM) ? OSSwapInt32(arch.cputype) : arch.cputype; + if (cpu == expectedCpu) + valid = true; + } + } + } + } + + fclose(f); + return valid; +} + +// ============================================================ +// MacPluginManager +// ============================================================ + +MacPluginManager::MacPluginManager() + : _staticCmdAlloc(ID_PLUGINS_CMD, ID_PLUGINS_CMD_LIMIT) + , _dynamicCmdAlloc(ID_PLUGINS_CMD_DYNAMIC, ID_PLUGINS_CMD_DYNAMIC_LIMIT) + , _markerAlloc(MARKER_PLUGINS, MARKER_PLUGINS_LIMIT) + , _indicatorAlloc(INDICATOR_PLUGINS, INDICATOR_PLUGINS_LIMIT + 1) +{ +} + +int MacPluginManager::loadPluginFromPath(const std::wstring& pluginFilePath) +{ + std::string utf8Path = wideToUTF8(pluginFilePath); + + // Extract filename from path + NSString* nsPath = [NSString stringWithUTF8String:utf8Path.c_str()]; + NSString* fileName = [nsPath lastPathComponent]; + std::wstring wFileName = utf8ToWide([fileName UTF8String]); + + auto pi = std::make_unique(); + pi->_moduleName = wFileName; + // Display name: strip .dylib extension + NSString* displayName = [fileName stringByDeletingPathExtension]; + pi->_displayName = utf8ToWide([displayName UTF8String]); + + // Validate architecture + if (!validateMachOArchitecture(utf8Path)) + { + NSLog(@"Plugin %@ has incompatible architecture, skipping.", fileName); + return -1; + } + + // Load the dylib + pi->_hLib = ::LoadLibraryExW(pluginFilePath.c_str(), nullptr, 0); + if (!pi->_hLib) + { + NSLog(@"Failed to load plugin %@: dlopen error", fileName); + return -1; + } + + // Resolve isUnicode (optional check) + auto pIsUnicode = reinterpret_cast(::GetProcAddress(pi->_hLib, "isUnicode")); + if (pIsUnicode && !pIsUnicode()) + { + NSLog(@"Plugin %@ is ANSI-only, skipping.", fileName); + return -1; + } + + // Resolve required exports + pi->_pFuncSetInfo = reinterpret_cast(::GetProcAddress(pi->_hLib, "setInfo")); + if (!pi->_pFuncSetInfo) + { + NSLog(@"Plugin %@ missing setInfo export.", fileName); + return -1; + } + + pi->_pFuncGetName = reinterpret_cast(::GetProcAddress(pi->_hLib, "getName")); + if (!pi->_pFuncGetName) + { + NSLog(@"Plugin %@ missing getName export.", fileName); + return -1; + } + + pi->_pBeNotified = reinterpret_cast(::GetProcAddress(pi->_hLib, "beNotified")); + if (!pi->_pBeNotified) + { + NSLog(@"Plugin %@ missing beNotified export.", fileName); + return -1; + } + + pi->_pMessageProc = reinterpret_cast(::GetProcAddress(pi->_hLib, "messageProc")); + if (!pi->_pMessageProc) + { + NSLog(@"Plugin %@ missing messageProc export.", fileName); + return -1; + } + + pi->_pFuncGetFuncsArray = reinterpret_cast(::GetProcAddress(pi->_hLib, "getFuncsArray")); + if (!pi->_pFuncGetFuncsArray) + { + NSLog(@"Plugin %@ missing getFuncsArray export.", fileName); + return -1; + } + + // Initialize plugin + pi->_pFuncSetInfo(_nppData); + + // Get the plugin's display name + const wchar_t* pluginName = pi->_pFuncGetName(); + if (pluginName && pluginName[0]) + pi->_displayName = pluginName; + + // Get function items + pi->_funcItems = pi->_pFuncGetFuncsArray(&pi->_nbFuncItem); + if (!pi->_funcItems || pi->_nbFuncItem <= 0) + { + NSLog(@"Plugin %@ returned no function items.", fileName); + return -1; + } + + // Allocate command IDs from the static range and register commands + pi->_cmdIdBase = static_cast(_pluginsCommands.size()); + for (int i = 0; i < pi->_nbFuncItem; ++i) + { + // Skip separators (null _pFunc) — upstream only assigns IDs to real commands + if (!pi->_funcItems[i]._pFunc) + continue; + + int cmdId = _staticCmdAlloc.allocate(1); + if (cmdId < 0) + { + NSLog(@"Exhausted static plugin command IDs."); + break; + } + pi->_funcItems[i]._cmdID = cmdId; + + PluginCommand cmd; + cmd._pluginName = pi->_displayName; + cmd._pFunc = pi->_funcItems[i]._pFunc; + _pluginsCommands.push_back(cmd); + } + + // Plugin submenu is created later during menu initialization (initMenu). + + NSLog(@"Loaded plugin: %@", [NSString stringWithUTF8String:wideToUTF8(pi->_displayName).c_str()]); + + _pluginInfos.push_back(std::move(pi)); + return static_cast(_pluginInfos.size() - 1); +} + +bool MacPluginManager::loadPlugins() +{ + @autoreleasepool { + NSString* pluginDir = [@"~/Library/Application Support/MacNote++/plugins" + stringByExpandingTildeInPath]; + + NSFileManager* fm = [NSFileManager defaultManager]; + + // Create directory if it doesn't exist + [fm createDirectoryAtPath:pluginDir withIntermediateDirectories:YES attributes:nil error:nil]; + + NSArray* contents = [fm contentsOfDirectoryAtPath:pluginDir error:nil]; + if (!contents) + return false; + + for (NSString* item in contents) + { + NSString* itemPath = [pluginDir stringByAppendingPathComponent:item]; + BOOL isDir = NO; + if ([fm fileExistsAtPath:itemPath isDirectory:&isDir] && isDir) + { + // Look for /.dylib + NSString* dylibName = [item stringByAppendingPathExtension:@"dylib"]; + NSString* dylibPath = [itemPath stringByAppendingPathComponent:dylibName]; + + if ([fm fileExistsAtPath:dylibPath]) + { + std::wstring wPath = utf8ToWide([dylibPath UTF8String]); + loadPluginFromPath(wPath); + } + } + } + } + + return !_pluginInfos.empty(); +} + +HMENU MacPluginManager::initMenu(HMENU hPluginsMenu) +{ + _hPluginsMenu = hPluginsMenu; + if (!_hPluginsMenu) + return nullptr; + + for (auto& pi : _pluginInfos) + { + // Create per-plugin submenu + HMENU pluginSub = ::CreatePopupMenu(); + pi->_pluginMenu = pluginSub; + + for (int i = 0; i < pi->_nbFuncItem; ++i) + { + FuncItem& fi = pi->_funcItems[i]; + if (!fi._pFunc) + { + // Null _pFunc indicates a separator (upstream convention) + ::AppendMenuW(pluginSub, MF_SEPARATOR, 0, nullptr); + } + else + { + UINT flags = MF_STRING; + if (fi._init2Check) + flags |= MF_CHECKED; + ::AppendMenuW(pluginSub, flags, static_cast(fi._cmdID), fi._itemName); + } + } + + // Add plugin submenu to main Plugins menu + ::AppendMenuW(_hPluginsMenu, MF_POPUP, reinterpret_cast(pluginSub), + pi->_displayName.c_str()); + } + + return _hPluginsMenu; +} + +void MacPluginManager::runPluginCommand(int index) +{ + if (index < 0 || index >= static_cast(_pluginsCommands.size())) + return; + + auto& cmd = _pluginsCommands[index]; + if (cmd._pFunc) + { + try + { + cmd._pFunc(); + } + catch (...) + { + NSLog(@"Plugin command crashed: %@", + [NSString stringWithUTF8String:wideToUTF8(cmd._pluginName).c_str()]); + } + } +} + +void MacPluginManager::notify(const SCNotification* notification) +{ + if (_noMoreNotification) + return; + + if (notification->nmhdr.code == NPPN_SHUTDOWN) + _noMoreNotification = true; + + for (auto& pi : _pluginInfos) + { + if (pi->_hLib && pi->_pBeNotified) + { + try + { + // Copy the notification per plugin so one plugin cannot mutate + // the object seen by later plugins (matches upstream behavior). + SCNotification scNotifCopy = *notification; + pi->_pBeNotified(&scNotifCopy); + } + catch (...) + { + NSLog(@"Plugin beNotified crashed: %@", + [NSString stringWithUTF8String:wideToUTF8(pi->_moduleName).c_str()]); + } + } + } +} + +void MacPluginManager::relayNppMessages(UINT Message, WPARAM wParam, LPARAM lParam) +{ + for (auto& pi : _pluginInfos) + { + if (pi->_hLib && pi->_pMessageProc) + { + try + { + pi->_pMessageProc(Message, wParam, lParam); + } + catch (...) + { + NSLog(@"Plugin messageProc crashed: %@", + [NSString stringWithUTF8String:wideToUTF8(pi->_moduleName).c_str()]); + } + } + } +} + +bool MacPluginManager::allocateCmdID(int numberRequired, int* start) +{ + int result = _dynamicCmdAlloc.allocate(numberRequired); + if (result < 0) + { + *start = 0; + return false; + } + *start = result; + return true; +} + +bool MacPluginManager::allocateMarker(int numberRequired, int* start) +{ + int result = _markerAlloc.allocate(numberRequired); + if (result < 0) + { + *start = 0; + return false; + } + *start = result; + return true; +} + +bool MacPluginManager::allocateIndicator(int numberRequired, int* start) +{ + int result = _indicatorAlloc.allocate(numberRequired); + if (result < 0) + { + *start = 0; + return false; + } + *start = result; + return true; +} + +// ============================================================ +// Singleton +// ============================================================ + +MacPluginManager& pluginManager() +{ + static MacPluginManager instance; + return instance; +} diff --git a/macos/platform/split_view.mm b/macos/platform/split_view.mm index b72e60d884fd..585ebd91f8fd 100644 --- a/macos/platform/split_view.mm +++ b/macos/platform/split_view.mm @@ -23,6 +23,7 @@ #include "panel_layout.h" #include "file_switcher_panel.h" #include "scintilla_notify.h" +#include "plugin_manager.h" #include "macro_manager.h" #include "status_bar.h" #include "windows.h" @@ -125,7 +126,33 @@ void doSplit() ctx().sciContainer2.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; [ctx().editorContainer2 addSubview:ctx().sciContainer2]; - ctx().scintillaView2 = ScintillaBridge_createView((__bridge void*)ctx().sciContainer2, 0, 0, 0, 0); + // Reuse the pre-created second Scintilla view (created hidden at startup for plugin compatibility) + if (ctx().scintillaView2) + { + NSView* sciView = (__bridge NSView*)ctx().scintillaView2; + NSView* oldParent = [sciView superview]; + [sciView removeFromSuperview]; + if (oldParent && oldParent.hidden) + [oldParent removeFromSuperview]; // Remove the hidden container + NSView* parent = ctx().sciContainer2; + sciView.frame = parent.bounds; + [parent addSubview:sciView]; + sciView.hidden = NO; + } + else + { + ctx().scintillaView2 = ScintillaBridge_createView((__bridge void*)ctx().sciContainer2, 0, 0, 0, 0); + if (ctx().scintillaView2) + { + HandleRegistry::WindowInfo sci2Info{}; + sci2Info.nativeView = ctx().scintillaView2; + sci2Info.className = L"Scintilla"; + sci2Info.isScintilla = true; + sci2Info.parent = ctx().mainHwnd; + ctx().scintillaSecondHwnd = HandleRegistry::createWindow(std::move(sci2Info)); + } + } + if (!ctx().scintillaView2) { // Rollback: restore editorContainer to contentView @@ -255,6 +282,9 @@ void doSplit() if (MacroManager::instance().isRecording()) MacroManager::instance().recordStep(scn->message, scn->wParam, scn->lParam); } + + // Forward Scintilla notifications to plugins + pluginManager().notify(reinterpret_cast(scn)); } }); } @@ -342,14 +372,18 @@ void doUnsplit() } } - // Destroy second Scintilla view + // Hide second Scintilla view (keep alive for plugin handle compatibility) if (ctx().scintillaView2) { cancelPendingSmartHighlight(); ScintillaBridge_clearNotifyCallback(ctx().scintillaView2); autoCloseOnViewDestroyed(ctx().scintillaView2); - ScintillaBridge_destroyView(ctx().scintillaView2); - ctx().scintillaView2 = nullptr; + NSView* sciView = (__bridge NSView*)ctx().scintillaView2; + [sciView removeFromSuperview]; + NSView* hiddenContainer = [[NSView alloc] initWithFrame:NSZeroRect]; + hiddenContainer.hidden = YES; + [ctx().editorContainer addSubview:hiddenContainer]; + [hiddenContainer addSubview:sciView]; } // Destroy second tab bar diff --git a/macos/platform/wndproc.mm b/macos/platform/wndproc.mm index 937d18211fb3..fb7d388fdede 100644 --- a/macos/platform/wndproc.mm +++ b/macos/platform/wndproc.mm @@ -39,6 +39,9 @@ #include "macro_manager.h" #include "windows.h" #include "commctrl.h" +#include "plugin_manager.h" +#include "nppm_handler.h" +#include "Notepad_plus_msgs.h" LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { @@ -48,6 +51,11 @@ LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { UINT cmdId = LOWORD(wParam); + // Relay WM_COMMAND to plugins' messageProc before dispatching. + // Individual command handlers return early, so the catch-all at the + // end of MainWndProc would never reach relayNppMessages for them. + pluginManager().relayNppMessages(msg, wParam, lParam); + if (cmdId >= IDM_FILE_RECENT_BASE && cmdId < IDM_FILE_RECENT_BASE + ctx().MAX_RECENT_FILES) { openRecentFile(cmdId - IDM_FILE_RECENT_BASE); @@ -66,12 +74,30 @@ LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) docs[tabIdx].languageIndex = langIdx; ++docs[tabIdx].functionListRevision; scheduleFunctionListRefresh(); + + // Notify plugins that the language changed + SCNotification notif{}; + notif.nmhdr.hwndFrom = ctx().mainHwnd; + notif.nmhdr.code = NPPN_LANGCHANGED; + notif.nmhdr.idFrom = 0; // V1: no stable buffer ID yet + pluginManager().notify(¬if); } } applyLanguage(langIdx); return 0; } + // Plugin commands: static FuncItem range + if (cmdId >= 22000 && cmdId < 23000) // ID_PLUGINS_CMD .. ID_PLUGINS_CMD_LIMIT + { + int i = cmdId - 22000; + pluginManager().runPluginCommand(i); + return 0; + } + // Plugin commands: dynamic range (allocated via NPPM_ALLOCATECMDID) + if (pluginManager().inDynamicRange(cmdId)) + return 0; + switch (cmdId) { case IDM_FILE_NEW: @@ -813,7 +839,28 @@ LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) KillTimer(hWnd, IDT_STATUSBAR); PostQuitMessage(0); return 0; + default: + { + // Handle NPPM range (WM_USER + 1000) + if (msg >= NPPMSG && msg < NPPMSG + 200) + { + LRESULT result = handleNppmMessage(hWnd, msg, wParam, lParam); + pluginManager().relayNppMessages(msg, wParam, lParam); + return result; + } + // Handle RUNCOMMAND_USER range (WM_USER + 3000) + if (msg >= RUNCOMMAND_USER && msg < RUNCOMMAND_USER + 20) + { + LRESULT result = handleRunCommandMessage(hWnd, msg, wParam, lParam); + pluginManager().relayNppMessages(msg, wParam, lParam); + return result; + } + break; + } } + // Catch-all: relay all messages to plugins' messageProc (matches upstream) + pluginManager().relayNppMessages(msg, wParam, lParam); + return DefWindowProcW(hWnd, msg, wParam, lParam); } diff --git a/macos/plugin-sdk/CMakeLists.txt b/macos/plugin-sdk/CMakeLists.txt new file mode 100644 index 000000000000..7480a30650d7 --- /dev/null +++ b/macos/plugin-sdk/CMakeLists.txt @@ -0,0 +1,24 @@ +# MacNote++ Plugin SDK +# For building plugins as .dylib files that load into MacNote++. +# +# Plugins include PluginInterface.h which pulls in Notepad_plus_msgs.h and Scintilla.h. +# On macOS, resolves to the Win32 shim from macos/shim/include/. + +cmake_minimum_required(VERSION 3.20) +project(MacNotePlusPluginSDK LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 20) + +# Paths to headers +set(SHIM_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../shim/include") +set(NPP_PLUGINS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../PowerEditor/src/MISC/PluginsManager") +set(SCINTILLA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../scintilla/include") + +# Example plugin +add_subdirectory(example/HelloMacNote) + +# Apply SDK include paths to all plugin targets +target_include_directories(HelloMacNote PRIVATE + "${SHIM_DIR}" + "${NPP_PLUGINS_DIR}" + "${SCINTILLA_DIR}" +) diff --git a/macos/plugin-sdk/example/HelloMacNote/CMakeLists.txt b/macos/plugin-sdk/example/HelloMacNote/CMakeLists.txt new file mode 100644 index 000000000000..4ae76ec1ac71 --- /dev/null +++ b/macos/plugin-sdk/example/HelloMacNote/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.20) +project(HelloMacNote LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 20) + +add_library(HelloMacNote MODULE HelloMacNote.cpp) + +set_target_properties(HelloMacNote PROPERTIES + PREFIX "" + SUFFIX ".dylib" + OUTPUT_NAME "HelloMacNote" +) + +# Allow unresolved symbols that the host app provides at load time +target_link_options(HelloMacNote PRIVATE + "LINKER:-undefined,dynamic_lookup" +) diff --git a/macos/plugin-sdk/example/HelloMacNote/HelloMacNote.cpp b/macos/plugin-sdk/example/HelloMacNote/HelloMacNote.cpp new file mode 100644 index 000000000000..427dc4ac9067 --- /dev/null +++ b/macos/plugin-sdk/example/HelloMacNote/HelloMacNote.cpp @@ -0,0 +1,96 @@ +// HelloMacNote — Sample Notepad++ plugin +// Compiles on both Windows (.dll) and macOS (.dylib) without #ifdef guards. + +#include "PluginInterface.h" +#include + +static NppData nppData; +static const int NB_FUNC = 3; +static FuncItem funcItems[NB_FUNC]; + +// ============================================================ +// Plugin commands +// ============================================================ + +static void helloWorld() +{ + ::MessageBox(nppData._nppHandle, L"Hello from MacNote++ plugin!", L"HelloMacNote", MB_OK); +} + +static void insertTimestamp() +{ + // Get the current Scintilla view + int which = 0; + ::SendMessage(nppData._nppHandle, NPPM_GETCURRENTSCINTILLA, 0, reinterpret_cast(&which)); + HWND sci = (which == 0) ? nppData._scintillaMainHandle : nppData._scintillaSecondHandle; + + // Format current date/time and insert at cursor + time_t now = time(nullptr); + struct tm* lt = localtime(&now); + char buf[64]; + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", lt); + ::SendMessage(sci, SCI_REPLACESEL, 0, reinterpret_cast(buf)); +} + +static void showVersion() +{ + LRESULT ver = ::SendMessage(nppData._nppHandle, NPPM_GETNPPVERSION, TRUE, 0); + int major = HIWORD(ver); + int minor = LOWORD(ver); + + wchar_t msg[128]; + swprintf(msg, 128, L"MacNote++ version: %d.%d", major, minor); + ::MessageBox(nppData._nppHandle, msg, L"Version", MB_OK); +} + +// ============================================================ +// Required plugin exports +// ============================================================ + +extern "C" __declspec(dllexport) void setInfo(NppData nd) +{ + nppData = nd; + + // Initialize function items + wcscpy(funcItems[0]._itemName, L"Hello World"); + funcItems[0]._pFunc = helloWorld; + + wcscpy(funcItems[1]._itemName, L"Insert Timestamp"); + funcItems[1]._pFunc = insertTimestamp; + + wcscpy(funcItems[2]._itemName, L"Show Version"); + funcItems[2]._pFunc = showVersion; +} + +extern "C" __declspec(dllexport) const wchar_t* getName() +{ + return L"HelloMacNote"; +} + +extern "C" __declspec(dllexport) FuncItem* getFuncsArray(int* nbItems) +{ + *nbItems = NB_FUNC; + return funcItems; +} + +extern "C" __declspec(dllexport) void beNotified(SCNotification* scn) +{ + if (scn->nmhdr.code == NPPN_READY) + { + // Plugin is ready + } + else if (scn->nmhdr.code == NPPN_SHUTDOWN) + { + // Cleanup before shutdown + } +} + +extern "C" __declspec(dllexport) LRESULT messageProc(UINT Message, WPARAM wParam, LPARAM lParam) +{ + return TRUE; +} + +extern "C" __declspec(dllexport) BOOL isUnicode() +{ + return TRUE; +}