Well, since there's not much functionality to User-Defined Languages in Notepad++
I decided to create a custom Lexer
to support Bioware's NWScript language
.
A Lexer
is just a program that can read, interpret and highlight programming code syntax.
But in this plugin, things rapidly expanded, so I also decided to provide the tools to compile and publish NWScript
files directly into the target games: Neverwinter Nights
Enhanced
and 2
(this one still a Work In Progress).
The plugin adds support for function Auto-Completion and can import newly defined functions from nwscripts.nss
(the main Neverwinter engine header).
If installing manually, create a folder called NWScript-Npp
inside the Notepad++
plugins installation folder and extract all of the contents of the .zip
archive there. Or, use the Notepad++ Plugins manager that will do it all automatically for you (TBA). After that, when the plugin starts, it will ask you to copy or patch some of the Notepad++ XML configurations file. Follow the instructions on screen or open the About Me
dialog box and read the first section of the Help there (also avaliable as online text here).
Here is a sample of the plugin's funcionality:
Dark Mode version:
The unity test file I used to take this screenshot is provided here.
Up to Notepad++ 8.3.2 and prior versions
:
- To use the plugin's built-in auto-indentation, you must first disable
Notepad++'s
Auto-Indentation function:
Settings -> Auto-Completion -> Auto-Indent (checkbox)
- Also while playing back a macro, especially the type-in ones, you'll want to disable the Plugin's auto-indent feature, as the plugin will not be able to detect a macro playback and will end messing-up any text typed within that macro.
From Notepad++ version 8.3.3 to later
:
- Those issues were fixed (thanks
@DonHo
for accepting my pull request) and no longer a concern, the option to select the plugin built-in auto-indentation won't even show up as an option to it's users anymore. So I suggest you to always keep yourNotepad++
version up-to-date. (if not possible because you use legacy plugins, well, just use the built-in auto-indent function then, it won't bite, I promise).
Click here to expand
This plugin is based on Notepad++ plugin template and the official Scintilla
C++ Lexer
. I managed to rewrite much of the code, clear and organize
classes, so anyone desiring to write future lexers will find it much easier to integrate a new lexer inside the Plugin. Just put your LexXXX.cpp
file on the project and add it to the Lexer Catalogue
and export it as a DLL
.
Also, for the NWScript
compilation, I "borrowed" the NWScript Compiler
code, since trying to write a compiler from scratch would be a monstrous task.
All files under this project are provided under the GPL v3.0 License
.
For reutilization of the project, the NWScript-Npp.vcxproj
is organized in the following way:
-
lib
: All linked library submodules found here. I got two things there: my personal port forNWScript Compiler
calledNscLib
(because it's only the library without the executable) and theLunaSVG
library - for managing vectorial images for high DPI support. The plugin project also depends on PCRE2, but it's installed/managed byvcpkg
. Dependencies are listed on thevcpkg.json manifest
of the project. Hence, to build my code from source you need it. Follow these steps:- Install vcpkg. Just follow
this guide
. - Don't forget the
vcpkg integrate install
part. - Done. The first time you build the project, all dependencies will be automatically installed. Click on
Rescan Solution
after building to update Intellisense. After that you can go to the vcpkg install dir and delete the temporary /downloads and /buildtrees.
- Install vcpkg. Just follow
-
Custom Lexers
: Here you'll write your new custom Lexers (example:LexNWScript.cpp
) and edit/place them insideLexerCatalogue.cpp
InstalledLexers[]
static object for the code to auto-initialize it upon plugin load. Something like this:constexpr static LexerDefinition InstalledLexers[] = { {"YourLexerName", TEXT("Your Lexer Status Text"), ANY_NUMBER, LexerScript::LexerFactoryFunction, ExternalLexerAutoIndentMode::XXX},} };
-
Where:
YourLexerName
is a 16 bytes-length string;Your Lexer Status Text
is a 32 bytes-length string (that will be displayed in Notepad++ status bar on the bottom of the screen);ANY_NUMBER
is just a number to uniquely-identify the Lexer inside your code (this is not used byNotepad++
in any way, this is just an internal number and you can set to0
if wanted ). In my case I#defined
aMACRO
for this;- A pointer to a
“Factory”
function to get your lexer's instantiated object. In my case it just returns a newLexerNWScript
class pointer - which implements theILexer5
interface. Like this:
static ILexer5* LexerFactoryNWScript() { return new LexerNWScript(ConstructorVariables, ...); }
- As a remark, this method is now deprecated, since
Notepad++
implemented theScintilla 5.2.2
engine version. Now an additional method is required:CreateLexer(name)
. This is howNotepad++
will from now on (version 8.3.4 and forward) will instantiate Lexers, so you must also modify this method to include any other custom lexer avaliable. - The
ExternalLexerAutoIndentMode
enum class
. This is a new feature I developed forNotepad++
to help plugins dealing with auto-indentation. Prior toNotepad++ version 8.3.3
, if you tried to perform a custom-made auto-indentation with your plugin, andNotepad++
had it's Auto-Indentation preference set toON
, it would override your plugin behavior and you wouldn't be able to properly auto-indent user inputs. So from8.3.3
version and forward, since this is not a standardILexer5
functionality, you'll be able to sendNotepad++
the messageNPPM_SETEXTERNALLEXERAUTOINDENTMODE
to makeNotepad++
work in 3 different ways about auto-indentation with your custom language:Standard
, which will tellNotepad++
to perform the default behavior (to just maintain any amount of tab spacing of previous line),C_Like
to tellNotepad++
your code support a C-Like syntax indentation-> which will read any curly brackets{
before a new line and advance the indent amount by one on the next line and then read the other paired curly bracket}
and go back one step in indentation... or you can tellNotepad++
that your plugin doesCustom
indentation, soNotepad++
won't perform ANY kind of auto-indent for your plugin lexer, even if it's set toON
inside the user's preferences - because now your plugin will be the one responsible for handling it. You can queryNotepad++
about this user setting with theNPPM_ISAUTOINDENTON
message. For more info, just study the code, especially the methodsSetAutoIndentSupport()
andLoadNotepadLexer()
inside myPluginMain.cpp
class, also along withProcessMessagesSci()
, especially theSCN_CHARADDED
message processing, to see how my plugin handles auto-indentation with newer and older versions ofNotepad++
. That field is only present there (onInstalledLexers[]
variable) to help you if you want your plugin to have more than onelexer
installed, so you can checkup whichlexers
are installed and to keep track of which auto-indentmode
they use.Notepad++
will never need or read that value in any way. Again, check theSetAutoIndentSupport()
andLoadNotepadLexer()
methods to understand this "language auto-indentation" thing better. (I also strongly suggest studyingNotepad++'s
maintainIndentation()
method insideNotepadPlus.cpp
file so you can see howNotepad++
performs it's own auto-indentation functionality).
-
-
Notepad Controls
: Contains some class templates to display dialog boxes. Versions ofStatic
,Modal
andDockable
dialogs boxes are avaliable. -
Plugin Interface
: Contains all code necessary to initialize the DLL and communicate withNotepad++
main executable, including the Lexer part. You probably won't need to change (much of) this code, EXCEPT to make it point to YOUR plugin class(es) instead of mine's. -
Resource Files
: Contains the XML necessary for the Lexer to work withNotepad++
. Without it,Notepad++
will just mark your plugin asincompatible
.- Also contains a
.targets
file that is imported inside thevcxproj
MSBuild
project file to automate deployment of the pluginDLL
toNotepad++'s
install directory to help you with your plugin debugging. Make sureNotepad++
isn't running when you build your code. Also make sure to give yourself write permissions to the Notepad/plugin installation folder and subfolders, so the compiler can copy the outputDLL
to that path. You'll be notified if it cannot and also the build will fail and the debugger will not run if it can't deploy at least theDLL
there (theXML
deploying is optional and only emits a warning). - Also, I've setup a
ProjectVersion.rc
file along with a header calledProjectVersion.h
to perform auto-increments on theVS_VERSION_INFO
associated resource. This works as following: Every time you hit the build command in Visual Studio, a pre-build event occurs, which calls thisThe auto-increment is now disabled by default.PowerShell
script on the project root that will editProjectVersion.h
and increment theVERSION_BUILD
macro inside that file.- Then the pre-compiler will read that macro and since
VS_VERSION_INFO
is setup to use macros for replacing version information, it will compile with whichever version is printed on ProjectVersion.h at the time of compilation. - Hence I advise you to
NEVER
touch or editProjectVersion.rc
inside theResource Editor
, or it will overwrite and destroy the macros inside and cause you to lose thebuild auto-increment
funcionality. Edit it manually (inside any raw text editor) andonly
to change other info, likeDLL Name
,Company Name
,Copyright Info
, etc and leave all the macros there about versioning untouched. - To increment major, minor or patch numbers, edit the
ProjectVersion.h
file instead.Only(as stated above, the auto-increment is now disabled by default), so if you want yourbuild
numbers are setup to auto-increment on my scriptmajor
,minor
orpatch
versions to change, you'll have do it manually, editting their respectiveVERSION_MAJOR
,VERSION_MINOR
andVERSION_PATCH
macros (leaveVERSION_STRING
andVERSION_STRING_BUILD
alone as they are). I designed this intentionally, since every person or team have its own standards for managing project versions.
- Then the pre-compiler will read that macro and since
- Also contains a
-
Utils
: Contains utilitary headers and code to help dealing with settings,.ini
files,regular expressions
, etc. -
DarkMode
: This is a ripped-off and enhanced/modified version ofNotepad++
experimentalDark Mode
support. It can be reused in other plugins with few to none modifications, so you may use the same interfaceNotepad++
uses to implement it'sDark Mode
interface now. It is based onUxtheme.lib
library andVsstyles.h
. Also contains a class calledDPIManager
to deal with all kind of things related to highDPI
support. Please, notice thatDark Mode
forWin32 API
is experimental, and a lot of things on it are undocumented features Microsoft implemented prior to pushing forwardWindows WPF
andUWP
, so many things are unsupported and/or unimplemented there, making us to rely more onSubclassing
controls andCustom/Owner-Drawing
(https://docs.microsoft.com/en-us/windows/win32/controls/about-custom-draw) our own versions. Expect to find most control classes avaliable there, and a few ones (like theDataGrid
) that currently don't have this support. -
Root Directory
: This is where the Plugin code really begins. I designed a baseSingleton
class calledPluginMain
to setup the Menu Commands, to deal with message processing, and all of the main plugin funcions, because, yeah... it will be created only once during a session or DLL loading. You'll need to change this as suitable. Perhaps in the future I'll clean up the code from my specific usage and leave a framework for others to developed upon. No promises made, though (and hey, it's easy to delete aPluginMain.cpp
and add your own class... just don't forget to updatePluginInterface.cpp
to point to your own classes instead of mine for handling plugin initialization, message parsing, etc).- Also, since many plugins use
.ini
files to store their settings, I already provided aSettings.cpp
class that will do that (almost) automatically for you. Just replace my variables with yours, update theSave()
andLoad()
functions to save/load your variables instead and you're done. The Settings class uses a modified version ofMiniINI
API to handle ini files reading, writting, etc., so it's really simple to use instead of writting your own version. It supportsANSI
andUNICODE
files and filenames. - And the
Common.h
file is just a bunch of aggregated functions I wrote myself or captured over the web, to help me dealing with unicode strings, conversions, Windows Icon and Bitmap handling, etc... (the method I developed for theNotepad++
auto-restart functionality with a temporarybatch
file involved into aShellExecute
API call was kind of... crusty... 🤣 but since I did not know of any other method out there and was a bit lazy to research more on this when I was writting features, well... I'll just leave that there... for now. 😇).
- Also, since many plugins use
-
Last but not least:
Plugin Dialogs
are just the instanced versions ofNotepad Controls
classes, to manage MY specific dialog boxes, etc. You really don't need these, except if you want to use them as examples.
All other files on this project are just internal work for my plugin specific funcionalities, and hence I will not be providing too much information on them here. I consider the code at least reasonably documented and commented already anyway, so feel free to explore it by yourself.
-
NWScript-Npp.vcxproj
file sets the<PlatformToolset>
tov143
for using withVisual Studio 2022
. -
Also, we are targeting
ISO C++ 20
standard here, and since Visual Studio 2022 still don't supportstd::format
on itsICO C++20
implementation, we set the project to usePreview features from the latest C++ working draft
. -
Interface functions required for NPP to use the lexer are all declared with:
extern "C" __declspec(dllexport)
- I created a
MACRO
calledDLLAPI
to help with that, so if parts of your code are to be used in otherDLLs
, it will change to:
extern "C" __declspec(dllimport)
- And if linking statically to a code, it will
#define
DLLAPI
to nothing.
- I created a
-
src/Lexers/Scintilla
,src/Lexers/Lexilla
andsrc/Lexers/Lexlib
are unmodified files copied from the Scintilla and Lexilla projects appended to Notepad++. You can update them with newer versions when needed. -
src/Lexers/Config/NWScript-Npp.xml
defines the language keywords & styles. Required for the plugin and will be published on project build. When changing theDLL
name, you MUST also change this to the exact name yourDLL
target gets, or elseNotepad++
will not recognize it. You'll also need to modify theand
tags there and replacename="NWScript"
to yourInstalledLexers[]
language name, or else it still won't link properly toNotepad++
and no custom colors for your plugin. Also theattribute obviously points to which file extension your language is to be automatically associated with when opening under `Notepad++` and the
attribute is what is displayed as the language name for the user when he goes to theSettings -> Style Configurator
to customize the language colors. -
To debug, just point the debugger to autorun
Notepad++.exe
for all supported plataforms (x86
orx64
), since this option can't be saved inside theMSBUILD
files (it's more of an environment configuration for the project).
Click here to expand
To tell the truth, I began this project as a self-imposed test. After spending quite some time enjoying community content from Neverwinter Vault
, I decided I should also give something back to the community. Add this to a self-motivation to write a nice piece of software in C++
, something I've never done before. I am a somewhat old of a IT guy. Started programming at 13 in the earlies 1990's and already deeply knew many languages, being the C language
among them (because I decided to follow the path more of an infrastructure architect rather than a pure programmer I quit programming on 2000's hence never followed the trends properly). Then I braced myself and seeing that all the major Notepad++ plugins used pure C++ and Win32, I decided to roll with that. Then I opened my Visual Studio IDE and started coding... and learning again stuff long forgotten. For me, this so far, is being an interesting experience, but with a lot of pitfalls and caveats. For instance, the LINKER can be tricky to manage. If your dependencies are not very well set, you can end up with missing symbols in your code that are ratherly hard to track, especially for any novice developer. Even if you are already very experient with other environments and languages, things are not so intuitive. Then you'll have to worry if you are linking against the static library, the dynamic library or the static library referencing the dynamic CRT library... The Visual Studio IDE also has it's own issues, like making easy to forget when you are editting your project properties to set configurations to Debug, Release, 32-Bit, 64-Bit... The C++ libraries around I found to be also very dependent on external examples. Lots of auto-generated documentations and many packages there don't come with many usage examples avaliable.
Joined to this, hundreds and hundreds of language peculiarities - memory leaks, access violations, strange and confusing declarations - like pointerofpointer** variable, void (function*)(arguments), variable&&, variable*&, etc., and several other features that can quickly render your code mostly unreadable if you don't take a very special care with your code styling.
Aside from that, I still think it's one of the BEST languages around. Fast, portable, NATIVE (withuot depencencies on virtual machines), and the one that unleashes the FULL potential and control of your hardware and deliver that in your sole hands (this last one can be a very good or a very bad thing).
Between all the helping hands around, I give a special thanks to The Cherno C++ series which helped an old developer a lot, that although had many years of IT experience (I'm actually a professional database architect), would never have touched a C++
code since about the early 2000s (yeah, I tried to use C#
syntax here and as you can presume, sooner than later I was screwing things up really fast - like, using the new
keyword to "instantiate" classes - yeah, you may lol to that 😅 - and doing other things an experienced C++
programmer would never think of doing with their code).
Because of this series, I decided to scratch all I assumed I knew about C
language and started all over with his series. That changed things really fast - and the catchup wasn't even that big of an effort.
Also, a BIG thanks to the https://regex101.com/ creators. While dealing with regular expressions
- something I needed to use to parse NWScript files for Notepad++
auto-complete integration, I was severally struggling with backtracking
up until I learned about possessive operators ( *+
, ++
, ?+
), atomic groups ( ?>
) and many other juicy concepts. That was a life-changing experience... So I REALLY advise you before trying to write regexes
, to do a pause and study the subject deeper first, instead of just copy-pasting code from google searches like I was doing my entire life up to that day... (yeah, never bothered in really learning regex for a long, long time 😔). That website alone solved almost 90% of my problems, and offered a really good debugger, from where I could figure out what EXACTLY was going on when a regular expression was being processed.
Talking about regex, that learning step lead me up to...
During the regex
development phase, I first started with std::regex
library to parse my strings, since it is in fact THE international STANDARD
library for doing this; so it must be a good, reliable and fast code to build your project upon... right? Until I found out that this engine had severe restrictions and wasn't even compiling expressions with named capture groups
... maybe not a big deal for simple regular expressions
out there but for me, a nuisance to keep changing matching indexes
everytime an expression was updated to fix a bug or another. Also, I found the execution really slow - it took aprox. 80 seconds to fully parse a nwscript.nss definitions file in debug mode
. In release mode
that dropped to 8 seconds, so even getting rid of every compiler debbuging overhead wasn't helping that much. All of this running in a pretty recent and fast machine setup (won't be spec'ing my setup here, for the sake story simplification).
I was bugged with that, because in an end-user perspective, especially if one used an older CPU, that seemed like my plugin was crashing or not responding, and they could even end up Ctrl+Alt+Del to task manager kill
the poor Notepad++
app for that (and prolly also swearing at me for hanging their machine up)... so, instead of thinking in just accepting what I had and going ahead adding [threads
] (https://www.cplusplus.com/reference/thread/thread/) and a possible % file analysis complete
dialog screen to the file parsing execution, I first decided to test other "alternative" engines... after doing a web scan on some researches about regular expressions benchmarking, I decided to go with boost::regex
, since that's the one being used by Notepad++
up to now and the one that appeared to have the most compatibility with the code I was alreaady using - just a matter of variable re-declaration and no needed to rewrite any of my already tested routines (the correct name for that inside a class is a method
, I know... but anyway...).
Sounded good at first...
Amazing! Parsing times dropped from 80 to 8 seconds, just by merely Ctrl+H replacing my variable declarations from std::regex
to boost::regex
. Nothing else changed. And a whooping 10x increase for that! And now it even supports my long sought named capture groups
, so I didn't need to change indexes anymore! Wow!
But that all changed when I decided to write more robust versions of my regular expressions
, since they were still unstable, and any malformed file could easily cause many severe catastrophic backtrackings
, stack overflows
and many other crashes
inside my code. Not really a fan of too much #try-#catch
blocks of code into my projects here, and also, the user could think this was taking to long... back to the dreaded Ctrl+Alt+Del #issue here (with the probable user-swearing parts and all that stuff). Hence, I decided to go back to halt all my other feature developments, go to regex101, and stay there for an indeterminate amount of time, until my regular expressions were working like a charm to any file I dumped in my application - well, not ANY
kind of files like heavly mangled ones and anything severely unrelated to the nwscript language, but anyway... you got the spirit.
After successfully finishing the expressions, I went back to Visual Studio
... just to find out that boost::regex
did not support subroutines
, something now crucial for interpreting object-nestings
and other stuff my new "robust" code was requiring... a quote from www.regular-expressions.info broke my heart:
Boost does not support the Ruby syntax for subroutine calls. In Boost
\g<1>
is a backreference---not a subroutine call---to capturing group 1. So([ab])\g<1>
can match aa and bb but not ab or ba. In Ruby the same regex would match all four strings. No other flavor discussed in this tutorial uses this syntax for backreferences.
Then, in frustration, I realized I had to change the engine... again.
So I decided to go back and integrate PCRE2
into my code, since that was the marked engine I was using while developing at Regex101
anyway. I knew PCRE2
was not very C++
- friendly, since it's a pure C
implementation of code. So I decided to look for a C++ Wrapper
to help me there, so I would't end up having an indigestable and inelegant code-salad in my project. Fortunately I found one
so I did not have to write it myself. A relief! Now I just needed to link with PCRE2
libraries aaand... Whoops! those aren't avaliable as a package, just as source code... and this code wasn't even written specifically to build under Visual Studio
or even Windows
: the author had it designed in the most generic form possible, so to allow ports to POSIX
, zOS
or any other kind of operating system and anything else capable of chewing on a raw C-language
standard
file and spewing out machine code after...
And there I go again, spending a whole day more, studying the library documentation
trying to figure out how to configure the package to compile under VS2022
, which features the author implemented and why... having to write my own visual studio configuration file
, dealing in what Windows
features and functions I had or had not avaliable, the confusing different library flags, like PCRE2_CODE_UNIT_WIDTH
for different library compilations - must I use just ONE code with for my entire project or can I have them all? Why the author says it also supports a 0
there and says it's "generic", even thought its not compiling? How all of those functions-types-and-other-stuff
declarations macros are all about, and so forth. And then, even spending a whole night alone just to figure out how to link the library statically
with my project until I found out I had to #define
PCRE2_STATIC
also within my project scope, because if I just #defined
that inside the LIBRARY
project, and then #include <pcre2.h>
on my side to use the library, some of the complex macros
there would lead to many functions being redeclared as extern __declspec(dllimport)
on MY side, leading my linker
into several missing symbols
! Yeah, that kind of nasty stuff to deal with!. (And then you can imagine my face when I discovered that ALL of that stuff wasn't even really necessary, because vcpkg
- something I came to discover only later in my endeavours - already had a port
of PCRE2
included, with ALL the configurations requirements already performed by Microsoft's team... 🤦 Anyway... 😊).
And then I had to rewrite all my file parsing routines
(yeah, I know, methods
), since my new C++ Wrapper
worked differently from the standard ones defined both in std::regex
and boost::regex
. (okay, that last part was a breeze and took the least insignificant amount of time on this whole process).
But ALL of that (re)work DID pay off when I put my new robust regexes to run inside PCRE2
engine. It dropped from boost's
8 seconds (on debug mode) to an amazing 500ms parsing time! Yeah, another 16x gain... but now I know that this is a bit of an unfair comparison with boost
engine, because now I didn't have the chance to re-test my new regular expressions against boost
with the new remade syntax and code blocks - like atomic groups, possessive operators and subroutines to avoid as much backtracking as possible - just because boost
didn't compile my regexes anymore... so I wonder what performance gap this would really be. Anyway...
What I did know then is that now I was able to finally close this #issue
and go back to coding more features to my plugin peacefully again.
(and here ends the PCRE2 regex engine saga, if you care to read it, I hope you find at least some useful information there).
If you read up to here - also passing though the collapsed sections, congrats, you got patience, and that's a virtue! (Not one of the 8 *officially defined virtues*, but anyway)... 🤴🧘
If you're reading this, I just wish you the best luck in your Neverwinter project (since this plugin is just a helper to Neverwinter content creators, nothing more). As NWN player myself, I always found that the community provided such an amazing ammount of good content and spent years doing so... so that community creators deserved some of my weeks to do a work for them and provide more support and better tools to help them in developing their inventions for the community. Sorry for the timming though... I know the game is out there for a looong time, and this plugin just came out now, in 2022. But well, that was because I just "rediscovered" Enhanced Edition and learned all about the Vault Community
a couple of months ago. I never thought a 2002 game would have so much untapped potential before. So, I think it's better later than never anyway, hehe.
So, to all content developers, this piece of software is made for you, and especially for you. Use it as it best suits you!
As for any #issues found, please report them back here
on Github
. I intend to support this project for awhile yet - up to it becoming stable, and while Notepad++
still keeps it compatible without having to rewrite large portions of code (unlikely).
Best regards,
Leonardo Silva.
(aka: Leonard-The-Wise, my D&D chosen DM name)