How to iterate over the NPP file tabs (open files)?
-
@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.