How to iterate over the NPP file tabs (open files)?
-
Is there a way, within a plugin, to iterate over the file tabs (open files)?
Situation:
I have a number of files that I am opening at the same time and by design, these files do not have an extension.
file1
file2
…
file99I want to set the language for the just opened files without having to 1) Select the file tab 2) Select language 3) select next tab 4) Select language, etc…
I tried creating a plugin for this but I could not find any functions to help. There are View menu commands that would help, but these are not implemented for SendMessage.
i.e.- First Tab
- Next Tab
etc
Thanks in advance.
Paul
-
Just eyeballing the public API, I think what you do (in pseudocode) is this
MAIN_VIEW = 0 SECONDARY_VIEW = 1 def getBuffersInView(int whichView) -> list of buffer IDs: ''' whichView = 0 for main view, 1 for secondary view returns the buffer IDs in that view ''' int numBuffers = NPPM_GETNBOPENFILES(whichView) buffers = new empty bufferID list for (int ii = 0; ii < numBuffers; ii++) { bufferID buf = NPPM_GETBUFFERIDFROMPOS(ii, whichView) add buf to buffers } return buffers def getAllOpenBuffers() -> list of buffer IDs: buffersInMainView = getBuffersInView(MAIN_VIEW) buffersInSecondaryView = getBuffersInView(SECONDARY_VIEW) return buffersInMainView concatenated with buffersInSecondaryView
-
@Paul-Baker said in How to iterate over the NPP file tabs (open files)?:
I want to set the language for the just opened files
[…]
I could not find any functions to helpNPPN_FILEOPENED - a notification that will be sent for each file as it is opened and give you the corresponding buffer ID
NPPM_SETBUFFERLANGTYPE - message you can send to Notepad++ to set the language type for each buffer ID after it is created
That’s if you want to do this automatically as files are opened (as your text suggests). If, as your title suggests, you want to change all already open tabs, then @Mark-Olson’s approach is what you need. Depending on your circumstances, one or the other might make more sense.
-
@Coises @Mark-Olson
Thanks to both of you! I have not tried the notification approach before. I may need some guidance. I’ll check the C# templates and sample plugins for examples. Thanks so much. After I get it working, I’ll post the solution. Thanks again!!! Paul -
@Paul-Baker
Are you using NppCSharpPluginPack as a template? I recommend using that. If you see something wrong, PRs and issues are appreciated. -
@Mark-Olson @Coises
This little snippet did the job:/** * Listener for Notifications */ public static void OnNotification(ScNotification notification) { // File Opened Notification - idFrom header field contains the BufferID if (notification.Header.Code == (uint)NppMsg.NPPN_FILEOPENED) { // Change buffer language type to COBOL - agr1 is BufferID arg2 is LangType Win32.SendMessage(PluginBase.nppData._nppHandle, (uint)NppMsg.NPPM_SETBUFFERLANGTYPE, notification.Header.IdFrom, (int) LangType.L_COBOL); } }
This works great but I would like to read a few lines from the buffer to verify the language type. Is that possible and if so, will you point me in a direction (example, messages)?
Thanks. This is a great forum, so helpful.
Paul -
@Paul-Baker said in How to iterate over the NPP file tabs (open files)?:
I would like to read a few lines from the buffer to verify the language type.
That is a bit tricky. Normally you would use the Scintilla API to read (or modify) data. However, at the point the NPPN_FILEOPENED message is sent, the file isn’t necessarily loaded into a Scintilla control. If you open several files, buffers and tabs are created for all of them, but only one is loaded into the current Scintilla.
One option would be to use NPPM_GETFULLPATHFROMBUFFERID to get the file name, including the path, and just read it like you would read any other file.
A second option would be to delay processing until the buffer is activated. That would mean you would watch for NPPN_BUFFERACTIVATED; when that message comes, the buffer in question is loaded into the active Scintilla control. (In C++, you get the active Scintilla handle this way:
int currentEdit = 0; SendMessage(nppData._nppHandle, NPPM_GETCURRENTSCINTILLA, 0, reinterpret_cast<LPARAM>(¤tEdit)); activeScintilla = currentEdit ? nppData._scintillaSecondHandle : nppData._scintillaMainHandle;
but @Mark-Olson can tell you better than I how to do that in C#.) Once you have that, you would use the Scintilla API to access the file content. Since buffers are activated often (including whenever you switch to the corresponding tab), you would probably want to make a list (std::vector, std::unordered_set, or whatever is used in C#) of buffer IDs when you encounter them in NPPN_FILEOPENED, then in NPPN_BUFFERACTIVATED check the list, and if the ID is there, remove it from the list and do your processing to see if it’s COBOL and set the type if it is.
-
I would like to read a few lines from the buffer to verify the language type
In NppCSharpPluginPack, you can get all the text of the buffer using
Npp.editor.GetText()
, and then you can split up the text of the buffer using C# standard library methods.You could also call Scintilla a bunch of times with some method (IDK which, find it for yourself) that retrieves the i^th line of the file, but I recommend using C# standard library methods rather than Scintilla API calls if speed is important because Scintilla API calls are rather slow.
-
@Mark-Olson @Coises
Thanks again. I will chew on this for awhile.
I’ll post my solution when I get it.
Paul -
Thanks guys, this is the solution I’m going with right now. I appreciate the helpful comments.
static List<IntPtr> openedFiles = new List<IntPtr>(); ... ... /** * Listener for Notifications */ public static void OnNotification(ScNotification notification) { uint code = notification.Header.Code; switch (code) { case (uint) NppMsg.NPPN_FILEOPENED: { openedFiles.Add(notification.Header.IdFrom); break; } case (uint) NppMsg.NPPN_BUFFERACTIVATED: { if (openedFiles.Contains(notification.Header.IdFrom)) { openedFiles.Remove(notification.Header.IdFrom); IScintillaGateway editor = new ScintillaGateway(PluginBase.GetCurrentScintilla()); if (editor.GetText(1600).Contains(ITSConstants.IDENTIFICATION_DIVISION)) { Win32.SendMessage(PluginBase.nppData._nppHandle, (uint)NppMsg.NPPM_SETBUFFERLANGTYPE, notification.Header.IdFrom, (int)LangType.L_COBOL); } } break; } } return; }
-
Some thoughts:
- I think it’s easier to have a
public static IScintillaGateway Editor
property of theMain
class that is refreshed every timeNPPN_BUFFERACTIVATED
is received by callingEditor = new ScintillaGateway(PluginBase.GetCurrentScintilla())
. The alternative is the more labor-intensive and annoying process of creating a new editor every time you need one. - Likewise, you should probably have a
public static INotepadPPGateway Notepad = new NotepadPPGateway()
, which you can use to call convenience methods, rather than having to do uglyWin32.SendMessage
nonsense every time you want to tell Notepad++ to do something. This global singleton only needs to be assigned once, and never needs to be reassigned. - This is partially just personal preference, but I have a strong distaste for creating a list that you need to iterate every time you want to do a lookup. I personally would use a
Dictionary<IntPtr, V>
, whereV
is some indicator that determines whether the current document is being lexed as COBOL. Dictionaries have amortized constant time lookup, which could matter if you have hundreds of buffers open. - I would recommend clearing buffer IDs from your list/dictionary when they are closed. I do this when I receive the
NPPN_FILEBEFORECLOSE
notification, but it’s probably more correct to do this when receiving theNPPN_FILECLOSED
notification.
Putting it all together, I would change what you’ve got to the following.
/// <summary> /// connector to Scintilla /// </summary> public static IScintillaGateway Editor = new ScintillaGateway(PluginBase.GetCurrentScintilla()); /// <summary> /// connector to Notepad++ /// </summary> public static INotepadPPGateway Notepad = new NotepadPPGateway(); private static Dictionary<IntPtr, bool> _isBufferCOBOL = new Dictionary<IntPtr, bool>(); ... ... /** * Listener for Notifications */ public static void OnNotification(ScNotification notification) { uint code = notification.Header.Code; switch (code) { // when switching buffers, if the lexer isn't COBOL, // read the first bit of the file to determine if we should lex it as COBOL. case (uint) NppMsg.NPPN_BUFFERACTIVATED: // When a buffer is activated, we need to reset the connector to the Scintilla editing component. // This is usually unnecessary, but if there are multiple instances or multiple views, // we need to track which of the currently visible buffers are actually being edited. Editor = new ScintillaGateway(PluginBase.GetCurrentScintilla()); IntPtr bufID = notification.Header.IdFrom; // TODO: note that you should probably use NPPM_GETBUFFERLANGTYPE here to see if Notepad++ *already set the lexer language to COBOL*. // You would need to write an unsafe method to do that correctly // (see https://github.com/notepad-plus-plus/notepad-plus-plus/blob/6204f00e881f7fc9459aff7134c12badbe6794bd/PowerEditor/src/MISC/PluginsManager/Notepad_plus_msgs.h#L62) // and I can't be bothered to implement it myself at the moment. if (!_isBufferCOBOL.TryGetValue(idFrom, out bool isCOBOL) || (!isCOBOL // if buffer is already COBOL, don't bother checking && Editor.GetText(1600).Contains(ITSConstants.IDENTIFICATION_DIVISION))) { _isBufferCOBOL[bufID] = true; Notepad.SetCurrentLanguage(LangType.COBOL); } else _isBufferCOBOL[bufID] = false; // not COBOL break; case (uint) NppMsg.NPPN_FILEBEFORECLOSE: _isBufferCOBOL.Remove(notification.Header.IdFrom); break; } return; }
Note: I’m trying to get in the habit of using good naming conventions that help the user figure out whether a class member is public or private, so after my initial post, I renamed all the
public static
members to beCapitalized
and theprivate static
member to be_lowerCase
. I did not change the semantics of my code in any way. - I think it’s easier to have a
-
@Mark-Olson Thanks Mark. Your first two points were no brainers. I do use NotepadPPGateway for my main operations. I had looked at Dictionary. Now I can’t remember why I went with List (was I just lazy?). Anyway, your point was valid and I ended up using HashSet. I’ll have to chew on your last point for awhile. When I switch tabs NPPN_BUFFERACTIVATED is activated each time this is why I am removing the BufferID from the List (Set) immediately. I’ll have to experiment with NPPN_FILEBEFORECLOSE.
Thank again Mark for taking the time to break all that down.
Paul
-
@Mark-Olson I went through your comments and code again. It’s not clear why you are recommending that the buffer id be removed on file close. Is there a technical reason I’m not aware of? Thanks again for your detail comments and code!
-
@Paul-Baker said in How to iterate over the NPP file tabs (open files)?:
It’s not clear why you are recommending that the buffer id be removed on file close
I think it’s good housekeeping to delete all data regarding a file that no longer exists. In this particular case it doesn’t really matter whether you delete data on file close, but I’m sure you can imagine cases where it matters quite a lot.