4.6.15. Triggering certain actions while saving a file

Probably you already thought of that: how useful it would be if NppExec allowed to trigger certain actions each time a file is saved. For example, you might want an external spell-checker to be run for text files; a code-formatting tool to be run for certain source files; also, you might want to do backups for some other files; and so on.

Some applications provide callback mechanisms or triggers for similar purpose. For example, there could be an "OnSave" callback/trigger that is invoked/triggered each time a file is saved. NppExec does not provide that, mostly because I don't see a good way to avoid deadlocks and/or infinite recursion. Here is an example of the latter: let's imagine we have a certain "OnSave" script triggered each time a file is saved. Now, let's imagine this "OnSave" script contains a command NPP_SAVE or NPP_SAVEALL. This embedded command will trigger another "OnSave" script, which, in its turn, will execute another NPP_SAVE or NPP_SAVEALL, and so on and so forth. Here is a different example: let's imagine the "OnSave" script starts a long-running external process, such as "cmd.exe" without arguments. By its nature, NppExec will wait until this process is finished - and it will be happening while saving the file. So, I'm not saying I don't want to implement this - all I'm saying there are certain problems for which I don't see a good solution yet.

Anyway, let's look at what we can do right now. As always, NppExec allows to do much more than can be expected, but it does not come out-of-the-box - we rather have to do everything manually. Well, let's do it! :-)

At first, let's see what is the alternative to a callback/trigger. It may not look obvious - but let's concentrate on what we usually do to save a file manually. We basically have two options for that: either press the "Save" button on the Toolbar or press Ctrl+S. The latter, Ctrl+S, is the one we are going to use. (Well, I'm aware of the auto-saving feature - and if you rely on it, I'm encouraging you to use Ctrl+S because, well, because it is required for all the further steps of this instruction :-) ).

The basic idea is this: what if we assign Ctrl+S to a certain NppExec's script rather than to the internal Notepad++'s "Save File" command? And once we have thought about that, the solution is immediately seen! Step one: create an NppExec's script that will be handling the file saving operation. Step two: create a menu item for this NppExec's script. Step three: assign a shortcut Ctrl+S to that menu item. Voila!

So, let's implement that step by step.

Let's start from a very simple NppExec's script, such as this one:

// npp_console ?
npp_console local -
npe_console local m- --
npp_console local +

echo Before saving a file "$(FULL_CURRENT_PATH)"

npp_save // saving a file

echo After saving a file "$(FULL_CURRENT_PATH)"

We intentionally use "npe_console local m- --" in the beginning of the script to make it less verbose. As the script may grow with time, it's a pretty good idea to abridge the output.

This simple script prints one message before a file is saved and another message after a file is saved. As the purpose of this script is to be executed on Ctrl+S, let's consider what would happen if we removed the "npp_save" command. Right, the file would have never be saved! Let's keep that in mind.

OK, let's save this script with a name "file_save". (I assume you are already familiar with the "Execute NppExec Script..." dialog [3.6] and NppExec's script [3.7]).

The step one (mentioned above) is done: the script has been created! Let's proceed to the step two.

From Notepad++'s main menu, select Plugins -> NppExec -> Advanced options... Now, in the Advanced Options dialog, click the "Associated script" combo-box, select "file_save" there and click the "Add/Modify" button. You'll see a new item "file_save :: file_save" in the "Menu items" list. Now press "OK" - and you'll see a message "Notepad++ must be restarted to apply some of the options". This is required to add a new menu item "file_save" to Notepad++. So, let's restart Notepad++. This will be the finish of the step two.

After Notepad++ is restarted, a shortcut key can be assigned to the new menu item "file_save". This is the step three. In Notepad++, select Settings -> Shortcut Mapper... and click "Plugin commands". Find the "file_save" command that corresponds to "NppExec.dll" and try to assign Ctrl+S to it. You will see: "CONFLICT FOUND! Main menu | Save ( Ctrl + S )". Oops! Looks like we need to disable this shortcut for the standard command first. OK, let's click the "Main menu" tab, find "Save | Ctrl+S" there, select it and press "Modify". Let's assign some non-used shortcut to it: for example, Ctrl+Alt+Shift+S or whatever you like unless it conflicts with any existing shortcut. After this is done, let's return to the "find_save" command in the "Plugin commands" tab and assign Ctrl+S to it. The step three is done!

Now let's test what we accomplished. Once you press Ctrl+S for any file, you should see these two messages in NppExec's Console: "Before saving a file" and "After saving a file".

Very good so far. Let's make our "file_save" script to do something more usefull. For example, let's backup a file if it is an XML file. Our updated NppExec's script may look like the following:

// npp_console ?
npp_console local -
npe_console local m- --
npp_console local +

echo Before saving a file "$(FULL_CURRENT_PATH)"
if "$(EXT_PART)" ~= ".xml" then
  set local f ~ fileexists $(FULL_CURRENT_PATH)
  if $(f) != 0 then
    set local bakfile = $(FULL_CURRENT_PATH).bak
    cmd /C (if exist "$(bakfile)" del /F "$(bakfile)") & copy "$(FULL_CURRENT_PATH)" "$(bakfile)"
  endif
endif // ".xml"

npp_save // saving a file

echo After saving a file "$(FULL_CURRENT_PATH)"

Now let's assume we want to run XmlLint against the saved XML file and see the results in Notepad++. Also, let's comment out the unneeded messages:

// npp_console ?
npp_console local -
npe_console local m- --
npp_console local +

// echo Before saving a file "$(FULL_CURRENT_PATH)"
if "$(EXT_PART)" ~= ".xml" then
  set local f ~ fileexists $(FULL_CURRENT_PATH)
  if $(f) != 0 then
    set local bakfile = $(FULL_CURRENT_PATH).bak
    cmd /C (if exist "$(bakfile)" del /F "$(bakfile)") & copy "$(FULL_CURRENT_PATH)" "$(bakfile)"
  endif
endif // ".xml"

npp_save // saving a file

// echo After saving a file "$(FULL_CURRENT_PATH)"
if "$(EXT_PART)" ~= ".xml" then
  "xmllint.exe" "$(FULL_CURRENT_PATH)" --output "$(FULL_CURRENT_PATH)"
  npp_sendmsg NPPM_RELOADFILE 0 "$(FULL_CURRENT_PATH)"
endif

This is almost perfect. Each time we press Ctrl+S, a backup copy of the previously saved XML file is created, the updated content is saved to the file and XmlLint is run against the saved file. You may need to specify a full path to "xmllint.exe" to make it work. Or, if you are not interested in XML, maybe you are doing something similar for e.g. C++ using the Artistic Style formatter - in such case you may need to specify a full path to "astyle.exe".

Let's actually add an example for C++ files as well. This requires additional modifications to the script:

// npp_console ?
npp_console local -
npe_console local m- --
npp_console local +

// echo Before saving a file "$(FULL_CURRENT_PATH)"
set local isXml = 0
set local isCpp = 0

if "$(EXT_PART)" ~= ".xml" then
  set local isXml = 1
else if "$(EXT_PART)" ~= ".cpp" then
  set local isCpp = 1
else if "$(EXT_PART)" ~= ".cxx" then
  set local isCpp = 1
else if "$(EXT_PART)" ~= ".cc" then
  set local isCpp = 1
else if "$(EXT_PART)" ~= ".c" then
  set local isCpp = 1
else if "$(EXT_PART)" ~= ".hpp" then
  set local isCpp = 1
else if "$(EXT_PART)" ~= ".hxx" then
  set local isCpp = 1
else if "$(EXT_PART)" ~= ".hh" then
  set local isCpp = 1
else if "$(EXT_PART)" ~= ".h" then
  set local isCpp = 1
endif

set local doBackup = 0
if "$(isXml)" == "1" then
  set local doBackup = 1
else if "$(isCpp)" == "1" then
  set local doBackup = 1
endif

if "$(doBackup)" == "1" then
  set local f ~ fileexists $(FULL_CURRENT_PATH)
  if $(f) != 0 then
    set local bakfile = $(FULL_CURRENT_PATH).bak
    cmd /C (if exist "$(bakfile)" del /F "$(bakfile)") & copy "$(FULL_CURRENT_PATH)" "$(bakfile)"
  endif
endif

npp_save // saving a file

// echo After saving a file "$(FULL_CURRENT_PATH)"
if "$(isXml)" == "1" then
  "xmllint.exe" "$(FULL_CURRENT_PATH)" --output "$(FULL_CURRENT_PATH)"
  npp_sendmsg NPPM_RELOADFILE 0 "$(FULL_CURRENT_PATH)"
else if "$(isCpp)" == "1" then
  "astyle.exe" "$(FULL_CURRENT_PATH)"
  npp_sendmsg NPPM_RELOADFILE 0 "$(FULL_CURRENT_PATH)"
endif

After doing it, you can safely call yourself a guru of NppExec's scripting :-) What was started as a small draft is now a full-functional script that does a lot!

The only remaining thing is the message "This file has been modified by another program" shown each time the XML file is modified by XmlLint (or, in case of a C++ file, each time it is modified by AStyle). To deal with it, we need to modify one Notepad++'s option manually. Go to Settings -> Preferences... -> MISC. -> File Status Auto-Detection. Check the "Update silently" check-box. This will disable the message "This file has been modified by another program". (Ideally, I would prefer a programmatic way to disable and enable this message, but Notepad++ does not currently allow that. There is an internal message NPPM_INTERNAL_ENABLECHECKDOCOPT, though).

Finally, after this NppExec's script is debugged and adjusted enough to do exactly what you want, you can uncomment the very first line "// npp_console ?". After that, the NppExec's Console will not be automatically shown each time you press Ctrl+S.

Well done and good luck!