Add new feature of using first line of untitled document for its tab name

Fix #3994, fix #16584, close #16585
This commit is contained in:
Anthony Lee Stark 2025-05-23 13:20:09 +07:00 committed by Don Ho
parent 6459905816
commit abc23714db
13 changed files with 143 additions and 6 deletions

View File

@ -1108,6 +1108,7 @@ Translation note:
<Item id="6419" name="New Document"/>
<Item id="6420" name="Apply to opened ANSI files"/>
<Item id="6432" name="Always open a new document in addition at startup"/>
<Item id="6433" name="Use the first line of document as untitled tab name"/>
</NewDoc>
<DefaultDir title="Default Directory">

View File

@ -1108,6 +1108,7 @@ Translation note:
<Item id="6419" name="New Document"/>
<Item id="6420" name="Apply to opened ANSI files"/>
<Item id="6432" name="Always open a new document in addition at startup"/>
<Item id="6433" name="Use the first line of document as untitled tab name"/>
</NewDoc>
<DefaultDir title="Default Directory">

View File

@ -1085,6 +1085,7 @@ Translation note:
<Item id="6419" name="Tài liệu mới"/>
<Item id="6420" name="Áp dụng cho các tệp ANSI đã mở"/>
<Item id="6432" name="Luôn mở thêm một tài liệu mới mỗi khi khởi động"/>
<Item id="6433" name="Dùng dòng đầu tiên làm tên cho các tài liệu chưa có tiêu đề"/>
</NewDoc>
<DefaultDir title="Thư mục mặc định">

View File

@ -6360,7 +6360,7 @@ void Notepad_plus::getCurrentOpenedFiles(Session & session, bool includeUntitled
}
const wchar_t* langName = languageName.c_str();
sessionFileInfo sfi(buf->getFullPathName(), langName, buf->getEncoding(), buf->getUserReadOnly(), buf->isPinned(), buf->getPosition(editView), buf->getBackupFileName().c_str(), buf->getLastModifiedTimestamp(), buf->getMapPosition());
sessionFileInfo sfi(buf->getFullPathName(), langName, buf->getEncoding(), buf->getUserReadOnly(), buf->isPinned(), buf->isUntitledTabRenamed(), buf->getPosition(editView), buf->getBackupFileName().c_str(), buf->getLastModifiedTimestamp(), buf->getMapPosition());
sfi._isMonitoring = buf->isMonitoringOn();
sfi._individualTabColour = docTab[k]->getIndividualTabColourId(static_cast<int>(i));

View File

@ -2118,6 +2118,7 @@ bool Notepad_plus::fileRename(BufferID id)
_pluginsManager.notify(&scnN);
success = true;
buf->setUntitledTabRenamedStatus(true);
bool isSnapshotMode = NppParameters::getInstance().getNppGUI().isSnapshotMode();
if (isSnapshotMode)
@ -2194,6 +2195,8 @@ bool Notepad_plus::fileRenameUntitledPluginAPI(BufferID id, const wchar_t* tabNe
scnN.nmhdr.code = NPPN_FILERENAMED;
_pluginsManager.notify(&scnN);
buf->setUntitledTabRenamedStatus(true);
bool isSnapshotMode = NppParameters::getInstance().getNppGUI().isSnapshotMode();
if (isSnapshotMode)
{
@ -2489,6 +2492,8 @@ bool Notepad_plus::loadSession(Session & session, bool isSnapshotMode, const wch
buf->setUserReadOnly(session._mainViewFiles[i]._isUserReadOnly);
buf->setPinned(session._mainViewFiles[i]._isPinned);
buf->setUntitledTabRenamedStatus(session._mainViewFiles[i]._isUntitledTabRenamed);
if (isSnapshotMode && !session._mainViewFiles[i]._backupFilePath.empty() && doesFileExist(session._mainViewFiles[i]._backupFilePath.c_str()))
buf->setDirty(true);
@ -2624,6 +2629,8 @@ bool Notepad_plus::loadSession(Session & session, bool isSnapshotMode, const wch
buf->setUserReadOnly(session._subViewFiles[k]._isUserReadOnly);
buf->setPinned(session._subViewFiles[k]._isPinned);
buf->setUntitledTabRenamedStatus(session._subViewFiles[k]._isUntitledTabRenamed);
if (isSnapshotMode && !session._subViewFiles[k]._backupFilePath.empty() && doesFileExist(session._subViewFiles[k]._backupFilePath.c_str()))
buf->setDirty(true);

View File

@ -59,6 +59,59 @@ BOOL Notepad_plus::notify(SCNotification *notification)
{
// for the backup system
_pEditView->getCurrentBuffer()->setModifiedStatus(true);
// auto make temporary name for untitled documents
Buffer* buffer = notifyView->getCurrentBuffer();
const NppGUI& nppGui = NppParameters::getInstance().getNppGUI();
const NewDocDefaultSettings& ndds = nppGui.getNewDocDefaultSettings();
intptr_t curLineIndex = _pEditView->execute(SCI_LINEFROMPOSITION, notification->position);
if (curLineIndex == 0 && ndds._useContentAsTabName && buffer->isUntitled() && !buffer->isUntitledTabRenamed())
{
// make a temporary tab name from first line of document
wstring content1stLineTabName = _pEditView->getLine(0);
buffer->normalizeTabName(content1stLineTabName);
// check whether there is any buffer with the same name
BufferID sameNamedBufferId = _pDocTab->findBufferByName(content1stLineTabName.c_str());
if (sameNamedBufferId == BUFFER_INVALID)
sameNamedBufferId = _pNonDocTab->findBufferByName(content1stLineTabName.c_str());
if (!content1stLineTabName.empty() && content1stLineTabName != buffer->getFileName() && sameNamedBufferId == BUFFER_INVALID)
{
// notify tab name changing
SCNotification scnNotif{};
scnNotif.nmhdr.code = NPPN_FILEBEFORERENAME;
scnNotif.nmhdr.hwndFrom = _pPublicInterface->getHSelf();
scnNotif.nmhdr.idFrom = (uptr_t)buffer->getID();
_pluginsManager.notify(&scnNotif);
// backup old file path
wstring oldFileNamePath = buffer->getFullPathName();
// set tab name
buffer->setFileName(content1stLineTabName.c_str());
// notify tab renamed
scnNotif.nmhdr.code = NPPN_FILERENAMED;
_pluginsManager.notify(&scnNotif);
// for the backup system
wstring oldBackUpFileName = buffer->getBackupFileName();
bool isSnapshotMode = nppGui.isSnapshotMode();
if (isSnapshotMode && !oldBackUpFileName.empty())
{
wstring newBackUpFileName = oldBackUpFileName;
newBackUpFileName.replace(newBackUpFileName.rfind(oldFileNamePath), oldFileNamePath.length(), content1stLineTabName);
if (doesFileExist(newBackUpFileName.c_str()))
::ReplaceFile(newBackUpFileName.c_str(), oldBackUpFileName.c_str(), nullptr, REPLACEFILE_IGNORE_MERGE_ERRORS | REPLACEFILE_IGNORE_ACL_ERRORS, 0, 0);
else
::MoveFileEx(oldBackUpFileName.c_str(), newBackUpFileName.c_str(), MOVEFILE_REPLACE_EXISTING);
buffer->setBackupFileName(newBackUpFileName);
}
}
}
}
if (notification->modificationType & SC_MOD_CHANGEINDICATOR)

View File

@ -2449,7 +2449,12 @@ bool NppParameters::getSessionFromXmlTree(TiXmlDocument *pSessionDoc, Session& s
if (boolStrPinned)
isPinned = _wcsicmp(L"yes", boolStrPinned) == 0;
sessionFileInfo sfi(fileName, langName, encStr ? encoding : -1, isUserReadOnly, isPinned, position, pBackupFilePath, fileModifiedTimestamp, mapPosition);
bool isUntitleTabRenamed = false;
const wchar_t* boolStrTabRenamed = (childNode->ToElement())->Attribute(L"untitleTabRenamed");
if (boolStrTabRenamed)
isUntitleTabRenamed = _wcsicmp(L"yes", boolStrTabRenamed) == 0;
sessionFileInfo sfi(fileName, langName, encStr ? encoding : -1, isUserReadOnly, isPinned, isUntitleTabRenamed, position, pBackupFilePath, fileModifiedTimestamp, mapPosition);
const wchar_t* intStrTabColour = (childNode->ToElement())->Attribute(L"tabColourId");
if (intStrTabColour)
@ -3674,6 +3679,9 @@ void NppParameters::writeSession(const Session & session, const wchar_t *fileNam
(fileNameNode->ToElement())->SetAttribute(L"tabColourId", static_cast<int32_t>(viewSessionFiles[i]._individualTabColour));
(fileNameNode->ToElement())->SetAttribute(L"RTL", viewSessionFiles[i]._isRTL ? L"yes" : L"no");
(fileNameNode->ToElement())->SetAttribute(L"tabPinned", viewSessionFiles[i]._isPinned ? L"yes" : L"no");
// Save this info only when it's an untitled entry
if (viewSessionFiles[i]._isUntitledTabRenamed)
(fileNameNode->ToElement())->SetAttribute(L"untitleTabRenamed", L"yes");
// docMap
(fileNameNode->ToElement())->SetAttribute(L"mapFirstVisibleDisplayLine", _i64tot(static_cast<LONGLONG>(viewSessionFiles[i]._mapPos._firstVisibleDisplayLine), szInt64, 10));
@ -5547,6 +5555,10 @@ void NppParameters::feedGUIParameters(TiXmlNode *node)
val = element->Attribute(L"addNewDocumentOnStartup");
if (val)
_nppGUI._newDocDefaultSettings._addNewDocumentOnStartup = (lstrcmp(val, L"yes") == 0);
val = element->Attribute(L"useContentAsTabName");
if (val)
_nppGUI._newDocDefaultSettings._useContentAsTabName = (lstrcmp(val, L"yes") == 0);
}
else if (!lstrcmp(nm, L"langsExcluded"))
@ -7517,7 +7529,7 @@ void NppParameters::createXmlTreeFromGUIParams()
insertGUIConfigBoolNode(newGUIRoot, L"SaveAllConfirm", _nppGUI._saveAllConfirm);
}
// <GUIConfig name = "NewDocDefaultSettings" format = "0" encoding = "0" lang = "3" codepage = "-1" openAnsiAsUTF8 = "no" / >
// <GUIConfig name = "NewDocDefaultSettings" format = "0" encoding = "0" lang = "3" codepage = "-1" openAnsiAsUTF8 = "no" useContentAsTabName = "no" / >
{
TiXmlElement *GUIConfigElement = (newGUIRoot->InsertEndChild(TiXmlElement(L"GUIConfig")))->ToElement();
GUIConfigElement->SetAttribute(L"name", L"NewDocDefaultSettings");
@ -7527,6 +7539,7 @@ void NppParameters::createXmlTreeFromGUIParams()
GUIConfigElement->SetAttribute(L"codepage", _nppGUI._newDocDefaultSettings._codepage);
GUIConfigElement->SetAttribute(L"openAnsiAsUTF8", _nppGUI._newDocDefaultSettings._openAnsiAsUtf8 ? L"yes" : L"no");
GUIConfigElement->SetAttribute(L"addNewDocumentOnStartup", _nppGUI._newDocDefaultSettings._addNewDocumentOnStartup ? L"yes" : L"no");
GUIConfigElement->SetAttribute(L"useContentAsTabName", _nppGUI._newDocDefaultSettings._useContentAsTabName ? L"yes" : L"no");
}
// <GUIConfig name = "langsExcluded" gr0 = "0" gr1 = "0" gr2 = "0" gr3 = "0" gr4 = "0" gr5 = "0" gr6 = "0" gr7 = "0" langMenuCompact = "yes" / >

View File

@ -219,8 +219,8 @@ public:
struct sessionFileInfo : public Position
{
sessionFileInfo(const wchar_t* fn, const wchar_t *ln, int encoding, bool userReadOnly,bool isPinned, const Position& pos, const wchar_t *backupFilePath, FILETIME originalFileLastModifTimestamp, const MapPosition & mapPos) :
Position(pos), _encoding(encoding), _isUserReadOnly(userReadOnly), _isPinned(isPinned), _originalFileLastModifTimestamp(originalFileLastModifTimestamp), _mapPos(mapPos)
sessionFileInfo(const wchar_t* fn, const wchar_t *ln, int encoding, bool userReadOnly,bool isPinned, bool isUntitleTabRenamed, const Position& pos, const wchar_t *backupFilePath, FILETIME originalFileLastModifTimestamp, const MapPosition & mapPos) :
Position(pos), _encoding(encoding), _isUserReadOnly(userReadOnly), _isPinned(isPinned), _isUntitledTabRenamed(isUntitleTabRenamed), _originalFileLastModifTimestamp(originalFileLastModifTimestamp), _mapPos(mapPos)
{
if (fn) _fileName = fn;
if (ln) _langName = ln;
@ -239,6 +239,7 @@ struct sessionFileInfo : public Position
int _individualTabColour = -1;
bool _isRTL = false;
bool _isPinned = false;
bool _isUntitledTabRenamed = false;
std::wstring _backupFilePath;
FILETIME _originalFileLastModifTimestamp {};
@ -609,6 +610,7 @@ struct NewDocDefaultSettings final
LangType _lang = L_TEXT;
int _codepage = -1; // -1 when not using
bool _addNewDocumentOnStartup = false;
bool _useContentAsTabName = false;
};

View File

@ -37,6 +37,10 @@ static const int LF = 0x0A;
long Buffer::_recentTagCtr = 0;
// Invalid characters for a file name
// Refer: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
// Including 'tab' and 'return/new line' characters
const wchar_t* fileNameInvalidChars = L"\\/:*?\"<>|\t\r\n";
namespace // anonymous
{
@ -304,6 +308,44 @@ void Buffer::setFileName(const wchar_t *fn)
doNotify(BufferChangeFilename | BufferChangeTimestamp | lang2Change);
}
void Buffer::normalizeTabName(wstring& tabName)
{
if (!tabName.empty())
{
// remove leading/trailing spaces
trim(tabName);
// remove invalid characters
wstring tempStr;
for (wchar_t ch : tabName)
{
bool isInvalid = false;
for (const wchar_t* p = fileNameInvalidChars; *p != L'\0'; ++p)
{
if (ch == *p)
{
isInvalid = true;
break;
}
}
if (!isInvalid)
tempStr += ch;
}
tabName = tempStr;
// restrict length
if (tabName.length() >= langNameLenMax - 1)
{
tempStr = tabName.substr(0, langNameLenMax - 1);
tabName = tempStr;
}
// remove leading/trailing spaces again
trim(tabName);
}
}
bool Buffer::checkFileState() // returns true if the status has been changed (it can change into DOC_REGULAR too). false otherwise
{

View File

@ -170,6 +170,8 @@ public:
const wchar_t * getFileName() const { return _fileName; }
void normalizeTabName(std::wstring& tabName);
BufferID getID() const { return _id; }
void increaseRecentTag() {
@ -269,6 +271,9 @@ public:
doNotify(BufferChangeLexing);
}
bool isUntitledTabRenamed() const { return _isUntitledTabRenamed; }
void setUntitledTabRenamedStatus(bool isRenamed) { _isUntitledTabRenamed = isRenamed; }
//these two return reference count after operation
int addReference(ScintillaEditView * identifier); //if ID not registered, creates a new Position for that ID and new foldstate
int removeReference(const ScintillaEditView * identifier); //reduces reference. If zero, Document is purged
@ -399,6 +404,8 @@ private:
bool _needLexer = false; // new buffers do not need lexing, Scintilla takes care of that
//these properties have to be duplicated because of multiple references
bool _isUntitledTabRenamed = false;
//All the vectors must have the same size at all times
std::vector<ScintillaEditView *> _referees; // Instances of ScintillaEditView which contain this buffer
std::vector<Position> _positions;

View File

@ -248,7 +248,7 @@ IDD_PREFERENCE_SUB_NEWDOCUMENT DIALOGEX 115, 10, 460, 205
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
GROUPBOX "New Document",IDC_NEWDOCUMENT_GR_STATIC,15,8,424,161,BS_CENTER
GROUPBOX "New Document",IDC_NEWDOCUMENT_GR_STATIC,15,8,424,174,BS_CENTER
GROUPBOX "Format (Line ending)",IDC_FORMAT_GB_STATIC,58,29,129,79,BS_CENTER
CONTROL "Windows (CR LF)",IDC_RADIO_F_WIN,"Button",BS_AUTORADIOBUTTON | WS_GROUP,65,48,70,10
CONTROL "Unix (LF)",IDC_RADIO_F_UNIX,"Button",BS_AUTORADIOBUTTON,65,64,70,10
@ -265,6 +265,7 @@ BEGIN
RTEXT "Default language:",IDC_DEFAULTLANG_STATIC,16,125,77,8
COMBOBOX IDC_COMBO_DEFAULTLANG,98,123,100,140,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP
CONTROL "Always open a new document in addition at startup",IDC_CHECK_ADDNEWDOCONSTARTUP,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,23,152,250,10
CONTROL "Use the first line of document as untitled tab name",IDC_CHECK_USECONTENTASTABNAME,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,23,165,250,10
END

View File

@ -3419,6 +3419,8 @@ intptr_t CALLBACK NewDocumentSubDlg::run_dlgProc(UINT message, WPARAM wParam, LP
::SendDlgItemMessage(_hSelf, IDC_CHECK_ADDNEWDOCONSTARTUP, BM_SETCHECK, ndds._addNewDocumentOnStartup, 0);
::SendDlgItemMessage(_hSelf, IDC_CHECK_USECONTENTASTABNAME, BM_SETCHECK, ndds._useContentAsTabName, 0);
return TRUE;
}
@ -3516,6 +3518,12 @@ intptr_t CALLBACK NewDocumentSubDlg::run_dlgProc(UINT message, WPARAM wParam, LP
return TRUE;
}
case IDC_CHECK_USECONTENTASTABNAME:
{
ndds._useContentAsTabName = isCheckedOrNot(IDC_CHECK_USECONTENTASTABNAME);
return TRUE;
}
default:
{
if (HIWORD(wParam) == CBN_SELCHANGE)

View File

@ -318,6 +318,7 @@
#define IDC_DISPLAY_STATIC 6429
#define IDC_OPENSAVEDIR_CHECK_DROPFOLDEROPENFILES 6431
#define IDC_CHECK_ADDNEWDOCONSTARTUP 6432
#define IDC_CHECK_USECONTENTASTABNAME 6433
#define IDD_PREFERENCE_SUB_DEFAULTDIRECTORY 6450
#define IDD_PREFERENCE_SUB_RECENTFILESHISTORY 6460