Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Doc: usage.rst , intro.rst , pythonprimer.rst #296

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
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
119 changes: 84 additions & 35 deletions docs/source/intro.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Introduction
Introduction
============

.. highlight:: python
Expand All @@ -11,20 +11,20 @@ Don't worry if you've not seen, or never even heard of Python_, it's incredibly
To whet your appetite, here's a quick (complete) sample script that shows some of the power of Python Script.::

editor.replace("old", "new")
editor.pyreplace(r"^Code: ([A-Z]{4,8})", r"The code is \1")

notepad.runMenuCommand("TextFX Tools", "Delete Blank Lines")

notepad.runMenuCommand("Line Operations", "Remove Empty Lines")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes that there is a Line Operations menu item that has a Remove Empty Lines submenu item, which is not the case. Instead notepad.menuCommand(MENUCOMMAND.EDIT_REMOVEEMPTYLINES) should be used.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ekopalypse , I double-checked that that menu item I used IS present in Npp 8.5.6 (same as in 8.5.4) by default.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, it may be absent with other languages than English. So then I can add there note to use your command if user's menu language is not English.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runMenuCommand method expects a main menu item like "File, Edit, Search, View ..." as first parameter and a subitem that is directly a child of this main menu item.
Does the call you mentioned actually work for you? With PS3 it doesn't and to be honest, I'd be surprised if it worked with PS2.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the call you mentioned actually work for you?

Yes, it does! (PS2). The old example there, with ("TextFX Tools", "Delete Blank Lines"), didn't work because THAT was not present in the menu at all.
I have verified each of the examples in the doc.
How else do you think I would have dared to make doc changes ? :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried it again after changing the localization to a different language (ex, Romanian) and the command strings in that language, respectively, . At first it didn't work, then fixed it with putting
# coding=utf-8
at the top of the script.
So maybe that was the issue in your case?
Or, maybe you can try setting the 3rd parameter, refreshCache, to True, as explained in the doc.

I suspect why it works (with PS2, at least) is that 1st argument is searched not only in the main-menu entries (file, edit etc), but also sub-entries, as listed in the language.xml files.

In any case...

The doc says:

For built-in menus use notepad.menuCommand(), for non built-in menus (e.g. TextFX and macros you’ve defined), use notepad.runMenuCommand(menuName, menuOption).

because of efficiency reasons, I understand.

So I'll replace with notepad.menuCommand(MENUCOMMAND.EDIT_REMOVEEMPTYLINES), as you suggested.


notepad.save()


Line 1 performs a normal search and replace on the current document, replacing the word "old" with the word "new".

Line 2 also performs a search and replace, but uses Python_ regular expressions, which is a much more complete implementation of regular expressions than is natively supported in Notepad++.
Line 1 performs a normal search and replace on the current document, replacing the word "old" with the word "new".

Line 4 runs the menu command called "Delete Blank Lines" from the TextFX Tools menu.
Line 3 runs the menu command called "Remove Empty Lines" from menu: Edit -> Line Operations.

Line 5 saves the current document (equivalent to clicking File, Save).

WARNING: While for the most part, a PythonScript script will behave like a Python script run by a Python interpreter, one important difference is that ``exit()``, ``quit()`` and related functions like ``sys.exit()`` and ``os._exit(0)``, that normally terminate the script and exit the Python interpreter, in PythonScript they will also abruptly terminate Notepad++ (crash it without saving your unsaved files and other data). So, you best avoid them, and use other techniques for early exiting from your code (like ``return`` inside a function, for example).


Objects
========
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example needs to be modified to make it python2/3 compatible. E.g. in Python3 a str has no decode method.

Copy link
Author

@victorel-petrovich victorel-petrovich Aug 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ekopalypse , which exmple exactly? Please comment directly under the changes that you refer to.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you mean in intro.rst#working-with-unicode-text .

Please correct me if I'm wrong: after merging this doc PR, people will get to see it in their installed PS only after PS project releases a new version.
If so, will the new PS version be already with Python 3?

If yes, then why need to make it compatible with both py 2 and 3, instead of only py 3?

And, also other parts of doc will need adjustments to py 3.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the changes only apply to a new version of PS(2/3).
As far as I know, there is currently only one issue that prevents the project from deprecating the PS2 version, and that is exactly the discussion point of the example. Strings.
In Python2, a string is an array of bytes, while in Python3, a string is now a Unicode object. In Python3, a string has only one encode method to convert it to a byte array, and the bytes object knows only one decode method to create a Unicode object.
I haven't checked if other parts of the document need to be updated as well, but right now I can only think of the "print" function as a possible stumbling block.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't checked if other parts of the document need to be updated as well, but right now I can only think of the "print" function as a possible stumbling block.

Aside from what you say, I noticed a few mentionings of py 2 or 2.7.

If you're not sure that next PS version will be with python3, then let's just push this PR with current changes, then later can make another one specifically to ensure all is for python 3.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or let @chcg weigh in

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the master just python3 is relevant. There is a dedicated branch for the old python2 version, see https://github.com/bruderstein/PythonScript/tree/stable_python27.

Expand Down Expand Up @@ -76,11 +76,13 @@ append ``.decode('utf8')`` to the string. Obviously if your string is in a diffe
To put text back to Scintilla (so editor.something()), use .encode('utf8') from a unicode string.

For example::


# -*- coding: utf-8 -*-

# define a unicode variable
someUnicodeString = u'This häs fünny ünicode chäractêrs in it'

# append the text to the current buffer - assuming the current buffer is set to utf8
# add the string to the current buffer at current position - assuming the current buffer is set to utf8
editor.addText(someUnicodeString.encode('utf8'))

# grab the first line
Expand All @@ -93,7 +95,7 @@ For example::
firstLineUnicode = firstLineUnicode.upper()

# and put the line back
editor.replaceWholeLine(firstLineUnicode.encode('utf8')
editor.replaceWholeLine(0, firstLineUnicode.encode('utf8') )


.. _Notifications:
Expand All @@ -106,14 +108,14 @@ Overview

You can call a Python function when events occur in Notepad++ or Scintilla_. Events in Notepad++ are things like the active document changing, a file being opened or saved etc. Events in Scintilla are things like a character being added, a *save point* being reached, the document being made *dirty* and so on.

Basically, you register in a script a Python_ function to call when an event occurs, and thereafter the function always runs whenever that event occurs. One function can be registered to handle more than one event.
Basically, you register in a script a Python_ function to call when an event occurs, and thereafter the function always runs whenever that event occurs. A function in such a role will be called an event handler or a "callback". One function can be registered to handle more than one event.

You can unregister the callback later, either by using the name of the function, or the event names, or a combination.

A simple example
----------------

Let's register a callback for the FILEBEFORESAVE event - the occurs just before the file is saved,
Let's register a callback for the FILEBEFORESAVE event - which occurs just before the file is saved,
and we'll add a "saved on" log entry to the end of the file, if the filename ends in '.log'.::

import datetime
Expand All @@ -124,19 +126,21 @@ and we'll add a "saved on" log entry to the end of the file, if the filename end

notepad.callback(addSaveStamp, [NOTIFICATION.FILEBEFORESAVE])

Note: the actual registration happens when you run the script. If run this script N times, then N registrations will occur: once the event occurs, N times the callback function will be called. Callbacks will be active until you close Notepad++, or disable them in a script as explained later in this section.

Line 1 imports the datetime module so we can get today's date.

Line 3 defines a function called ``addSaveStamp``.

Line 4 checks that the extension of the file is '.log'.
Line 4 checks that the extension of the currently-active file is '.log'.

Line 5 appends text like ``"File saved on 2009-07-15"`` to the file.

Line 7 registers the callback function for the FILEBEFORESAVE event. Notice the square brackets around the ``NOTIFICATION.FILEBEFORESAVE``. This is a list, and can contain more than one item (so that the function is called when any of the events are triggered).

Really, we should improve this function a little. Currently, it assumes the file being saved is the active document - in the case of using "Save All", it isn't necessarily. However, it's easy to fix...
Really, we should improve this function a little. Currently, it assumes the file being saved is the active document - but in the case of using "Save All", it isn't necessarily. However, it's easy to fix...

The ``args`` parameter to the function is a map (similar a dictionary in C# or a hashmap in Java), that contains the arguments for the event - many events are signalled for a ``BufferID``, which is the Notepad++ internal number for a particular file or tab. We can do things with the bufferID like get the filename, switch to it to make it active and so on.
The ``args`` parameter to the function is a map (similar to a dictionary in C# or a hashmap in Java), that when the callback is registered, will contain the arguments from (details of) the event. Many events are signalled for a specific ``BufferID``, which is the Notepad++ internal number for a particular file or tab. We can do things with the bufferID like get the filename, switch to it to make it active and so on.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe using current BufferID instead of specific is even more accurate.

Copy link
Author

@victorel-petrovich victorel-petrovich Aug 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do that. (together with all modifications from your suggestions).


So, first we'll change it so that we check the filename of the bufferID being saved, rather than the active document.
Then, if the filename has a '.log' extension, we'll change to it and add our "File saved on ....." line.::
Expand All @@ -152,7 +156,7 @@ Then, if the filename has a '.log' extension, we'll change to it and add our "Fi



Great, now it works properly. There's a side effect though, if we do use save-all, we might change the active document,
Great, now it works properly. There's a side effect though, if we do use save-all when the current document is other than a ".log" file, the callback will make that ".log" file the active document,
which might seem a bit strange when we use it. Again, very easy to fix.::


Expand All @@ -169,7 +173,7 @@ which might seem a bit strange when we use it. Again, very easy to fix.::

Now everything works as should, and it's nice and easy to see what's going on, and we leave the user with the same document they had open if they use Save-All.

See the :class:`NOTIFICATION` enum for more details on what arguments are provided for each notification, and the different events that are available.
See the :class:`NOTIFICATION` enum for more details on what arguments are provided from each notification, and the different events that are available.

Cancelling Callbacks
--------------------
Expand All @@ -180,42 +184,87 @@ The simplest form is::

notepad.clearCallbacks()

This unregisters all callbacks for all events. If you want to just clear one or more events, just pass the list of :class:`NOTIFICATION` events you wish to clear.::
This unregisters all callbacks for all new events. If you want to just clear one or more events, just pass the list of :class:`NOTIFICATION` events you wish to clear.::

notepad.clearCallbacks([NOTIFICATION.FILEBEFORESAVE, NOTIFICATION.FILESAVED])

*Note that if you want to clear the callback for just one event, you still need to pass a list (i.e. surrounded with square brackets)*

To unregister a callback for a particular function, just pass the function.::
To unregister all callback for a particular function, just pass the function::
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A callback seems to be more accurate because Notepad has no asynchronous callbacks, which means that there is always one callback to this function at any time.

Copy link
Author

@victorel-petrovich victorel-petrovich Aug 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only one callback will be at work/active at a particular time, yes.
But there may be several callbacks registered: for same function, but for different event types.
Like in:

notepad.callback(myfun, [NOTIFICATION.EVENTTYPE1])
notepad.callback(myfun, [NOTIFICATION.EVENTTYPE2])

# notepad.clearCallbacks(myfun,  [NOTIFICATION.EVENTTYPE1] ) # would only clear the callback for this event type ,1
notepad.callback(myfun)   # will clear both callbacks 

Copy link
Author

@victorel-petrovich victorel-petrovich Aug 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or do we say instead that in lines 1 & 2 above only 1 callback has been registered ? (It's a matter of what's usual terminlogoly / language usage here; you know better than me )

Copy link
Contributor

@Ekopalypse Ekopalypse Aug 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest I never thought about it but you are right, is that two callbacks now or still one? @chcg what do you think? And if we differentiate it between "all callbacks" for the editor object and just "a callback" for the notepad object, that confuses people too.


notepad.clearCallbacks(addSaveStamp)


To unregister a callback for a particular function, for particular events (perhaps you want to keep the function registered for FILEBEFORESAVE, but don't want FILESAVED anymore)
To unregister a callback for a particular function, for particular events (perhaps you want to keep the function registered for FILEBEFORESAVE, but not for FILESAVED anymore)::

notepad.clearCallbacks(addSaveStamp, [NOTIFICATION.FILESAVED])

*Note that redefining the function (in this case ``addSaveStamp``) will mean that this method no longer works, as the function name is now a new object.*
*Note that redefining the function (in this case ``addSaveStamp``) will mean that this method, or the one before it, no longer works, as the function name is now a new object. Same problem if you re-run the script registering the callback several times: calling ``notepad.clearCallbacks(addSaveStamp)`` or ``notepad.clearCallbacks(addSaveStamp, [NOTIFICATION.FILESAVED])`` will only clear the most recently added callback. If these situations occur, you can use one of the other 2 forms of the ``clearCallbacks`` function *


The Callback smallprint
Synchronous and Asynchronous Callbacks
-----------------------

Due to the nature of Scintilla events, they are by default processed internally slightly differently to Notepad++ events.
Notepad++ events are always processed *synchronously*, i.e. your event handler finishes before Python Script lets
Notepad++ continue. Scintilla events are placed in a queue, and your event handlers are called as the queue is *asynchronously* processed
- i.e. the event completes before your event handler is complete (or potentially before your event handler is even called).
By default, Notepad++ events and Scintilla events are, by default, processed internally slightly differently.
Notepad++ events are always processed *synchronously* ("in sync", in step) relative to Notepad++ : your event handler finishes before Python Script lets Notepad++ continue with creating and processing other events. Thus, Notepad++ will appear unresponsive to a new user action for the (usually very short) period until the handler has finished processing current event.
The following script demostrates this::

The only visible difference is that if you have a lot of callbacks registered, or your callbacks perform a lot of work, you might receive
the event some time after it has actually occurred. In normal circumstances the time delay is so small it doesn't matter, but you may
console.clear()
import time

starttime=time.time()

def on_buffer_activated(args):
print("on_buffer_activated")
print((time.time()-starttime)//1) ,
print(" ") ,
time.sleep(4)
print((time.time()-starttime)//1)

notepad.callback(on_buffer_activated, [NOTIFICATION.BUFFERACTIVATED])

time.sleep(20)

notepad.clearCallbacks()

print("\nExperiment is over.")


In case of Scintilla events, when you use ``editor.callback(..)`` to register callbacks for them, their notifications are placed in a queue that is processed *asynchronously* relative to Notepad++ app. This means that while your event handler on one particular notification in the queue, Notepad++ does not wait for the handler to finish before accepting and responding to other user events. As a result, a particular event may happen a long time before your event handler finishes processing that event (notification) (or potentially before your event handler is even called).

In normal circumstances the time delay is so small it doesn't matter, but you may
need to be aware of it if you're doing something time-sensitive.
The script below demonstrates asynchronous processing where the delay is deliberately exaggerated::

console.clear()
import time

starttime=time.time()

def on_update_ui(args):
print("on_update_ui")
print((time.time()-starttime)//1) ,
print(" ") ,
time.sleep(4)
print((time.time()-starttime)//1)

editor.callback(on_update_ui, [SCINTILLANOTIFICATION.UPDATEUI])

time.sleep(20)

editor.clearCallbacks()

print("\nExperiment is over.")

If you tried sufficiently many actions during its run (clicks in text or menu, selections etc), then you would notice that after the script finished, thus the callback unregistered, the console is still outputing print-out messages from the handler. That is because the event handler was STILL processing some past events left on the queue. The ``clearCallbacks(...)`` functions only disable the handler for NEW events (not yet on the queue).

One other reason to be aware of the asynchronous nature of default Scintilla callbacks (besides potential lag in time relative to actual events) is that both your event handler in PythonScript and Notepad++ can access the same variable/state (from different threads), which could lead to unexpected behavior if you are not careful.

However, as of version 1.0, you can use :meth:`Editor.callbackSync` to add a synchronous callback for Scintilla events. This allows you to perform time-sensitive operations in an event handler. In particular, it allows for calling :meth:`Editor.autoCCancel` in a ``SCINTILLANOTIFICATION.AUTOCSELECTION`` notification to cancel the auto-complete.

As of version 1.0, you can use :meth:`Editor.callbackSync` to add a synchronous callback. This allows you to perform time-sensitive
operations in an event handler. It also allows for calling :meth:`Editor.autoCCancel` in a ``SCINTILLANOTIFICATION.AUTOCSELECTION``
notification to cancel the auto-complete. Note that there are certain calls which cannot be made in a synchronous callback -
:meth:`Editor.findText`, :meth:`Editor.searchInTarget` and :meth:`Editor.setDocPointer` are notable examples. :meth:`Notepad.createScintilla`
and :meth:`Notepad.destroyScintilla` are other examples in the ``Notepad`` object - note that this only applies to Scintilla (``Editor``) callbacks,
``Notepad`` callbacks can perform any operation.
Note that there are certain calls which cannot be made in a *synchronous* Scintilla (``Editor``) callback - :meth:`Editor.findText`, :meth:`Editor.searchInTarget` and :meth:`Editor.setDocPointer` are notable examples.
:meth:`Notepad.createScintilla` and :meth:`Notepad.destroyScintilla` are other examples in the ``Notepad`` object.
``Notepad`` callbacks do not have such restrictions.



Expand Down
14 changes: 6 additions & 8 deletions docs/source/notepad.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,6 @@ Notepad++ Object

Unregisters all callbacks

.. method:: notepad.clearCallbacks(function)

Unregisters all callbacks for the given function. Note that this uses the actual function object, so if the function has
been redefined since it was registered, this will fail. If this has happened, use one of the other ``clearCallbacks()``
functions.



.. method:: notepad.clearCallbacks(eventsList)

Expand All @@ -94,9 +87,14 @@ Notepad++ Object

See :class:`NOTIFICATION`


.. method:: notepad.clearCallbacks(function)

Unregisters all callbacks for the given function. Note that this uses the actual function object, so if the function has been redefined since it was registered, or if the script doing the registration of the callback has been re-run, then ``clearCallbacks(function)`` will fail. In such cases, use one of the other ``clearCallbacks`` functions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above, Unregisters a callback seems more accurate to me.


.. method:: notepad.clearCallbacks(function, eventsList)

Unregisters the callback for the given callback function for the list of events.
Unregisters all callbacks for the given callback function for the list of events. Same note as for the previous variant applies.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above, ...



.. method:: notepad.close()
Expand Down
Loading