Files
textmate/Frameworks/document/src/document.cc
Allan Odgaard 45763d4afc Don’t require file to get SCM info
We didn’t actually use the file itself, only its parent directory, and there are several places we want SCM info for an untitled file’s project directory, so removing the need for a file simplifies things.
2012-09-18 17:33:02 +02:00

1378 lines
41 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "document.h"
#include "watch.h"
#include "merge.h"
#include "reader.h"
#include "collection.h"
#include <io/io.h>
#include <regexp/glob.h>
#include <bundles/bundles.h>
#include <command/parser.h>
#include <cf/cf.h>
#include <cf/timer.h>
#include <cf/run_loop.h>
#include <file/type.h>
#include <file/path_info.h>
#include <plist/ascii.h>
#include <selection/selection.h>
#include <OakSystem/application.h>
#include <oak/debug.h>
#include <text/utf8.h>
#include <text/ctype.h>
#include <text/hexdump.h>
#include <text/tokenize.h>
#include <oak/duration.h>
#include <oak/server.h>
#include <oak/compat.h>
#include <io/entries.h>
#include <io/resource.h>
#include <scm/scm.h>
#include <settings/volume.h>
OAK_DEBUG_VAR(Document_Scanner);
OAK_DEBUG_VAR(Document_LRU);
OAK_DEBUG_VAR(Document_WatchFS);
OAK_DEBUG_VAR(Document_Replace);
OAK_DEBUG_VAR(Document_Tracker);
OAK_DEBUG_VAR(Document_Backup);
OAK_DEBUG_VAR(Document_Binary);
OAK_DEBUG_VAR(Document);
static std::string session_dir ()
{
std::string const& res = oak::application_t::support("Session");
return path::make_dir(res) ? res : "/tmp";
}
// ==========
// = Backup =
// ==========
static std::string backup_path (std::string displayName)
{
std::replace(displayName.begin(), displayName.end(), '/', ':');
return path::unique(path::join(session_dir(), displayName));
}
namespace
{
struct backup_record_t
{
backup_record_t () : backup_at(DBL_MAX), upper_limit(CFAbsoluteTimeGetCurrent() + 10) { }
cf::timer_ptr timer;
CFAbsoluteTime backup_at;
CFAbsoluteTime upper_limit;
};
}
static std::map<oak::uuid_t, backup_record_t> records;
static void cancel_backup (oak::uuid_t const& docId)
{
D(DBF_Document_Backup, bug("%s\n", to_s(docId).c_str()););
records.erase(docId);
}
static void perform_backup (oak::uuid_t const& docId)
{
D(DBF_Document_Backup, bug("%s\n", to_s(docId).c_str()););
if(document::document_ptr document = document::find(docId, false))
document->backup();
records.erase(docId);
}
static void schedule_backup (oak::uuid_t const& docId)
{
D(DBF_Document_Backup, bug("%s\n", to_s(docId).c_str()););
backup_record_t& record = records[docId];
CFAbsoluteTime backupAt = std::min(CFAbsoluteTimeGetCurrent() + 2, record.upper_limit);
if(!record.timer || record.backup_at < backupAt)
{
record.timer = cf::setup_timer(backupAt - CFAbsoluteTimeGetCurrent(), std::bind(&perform_backup, docId));
record.backup_at = backupAt;
}
}
// ==========
static std::multimap<text::range_t, document::document_t::mark_t> parse_marks (std::string const& str)
{
std::multimap<text::range_t, document::document_t::mark_t> marks;
if(str != NULL_STR)
{
plist::any_t const& plist = plist::parse(str);
if(plist::array_t const* array = boost::get<plist::array_t>(&plist))
{
iterate(bm, *array)
{
if(std::string const* str = boost::get<std::string>(&*bm))
marks.insert(std::make_pair(*str, "bookmark"));
}
}
}
return marks;
}
namespace document
{
// ================
// = File Watcher =
// ================
struct watch_t : watch_base_t
{
WATCH_LEAKS(document::watch_t);
watch_t (std::string const& path, document_weak_ptr document) : watch_base_t(path), document(document) { }
void callback (int flags, std::string const& newPath)
{
if(document_ptr doc = document.lock())
doc->watch_callback(flags, newPath);
}
private:
document_weak_ptr document;
};
// ====================
// = Document Tracker =
// ====================
static OSSpinLock spinlock = 0;
static pthread_t MainThread = pthread_self();
static struct document_tracker_t
{
ssize_t lock_count;
document_tracker_t () : lock_count(0) { }
struct lock_t
{
lock_t (document_tracker_t* tracker) : tracker(tracker), locked(false) { retain(); }
~lock_t () { release(); }
void retain () { OSSpinLockLock(&spinlock); DB(++tracker->lock_count); locked = true; };
void release () { if(!locked) return; DB(--tracker->lock_count); OSSpinLockUnlock(&spinlock); locked = false; }
private:
document_tracker_t* tracker;
bool locked;
};
document_ptr create (std::string const& path, path::identifier_t const& key)
{
lock_t lock(this);
D(DBF_Document_Tracker, bug("%s\n", path.c_str()););
std::map<path::identifier_t, document_weak_ptr>::const_iterator it = documents_by_path.find(key);
if(it != documents_by_path.end())
{
if(document_ptr res = it->second.lock())
{
D(DBF_Document_Tracker, bug("re-use instance (%s)\n", res->path().c_str()););
lock.release();
if(pthread_self() == MainThread)
res->set_path(path);
return res;
}
else
{
D(DBF_Document_Tracker, bug("*** old instance gone\n"););
}
}
document_ptr res = document_ptr(new document_t);
res->_identifier.generate();
res->_path = path;
res->_key = key;
add(res);
return res;
}
document_ptr find (oak::uuid_t const& uuid, bool searchBackups)
{
lock_t lock(this);
D(DBF_Document_Tracker, bug("%s\n", to_s(uuid).c_str()););
std::map<oak::uuid_t, document_weak_ptr>::const_iterator it = documents.find(uuid);
if(it != documents.end())
{
if(document_ptr res = it->second.lock())
{
D(DBF_Document_Tracker, bug("re-use instance\n"););
return res;
}
else
{
D(DBF_Document_Tracker, bug("*** old instance gone\n"););
}
}
path::walker_ptr walker = path::open_for_walk(session_dir());
iterate(path, *walker)
{
std::string const& attr = path::get_attr(*path, "com.macromates.backup.identifier");
if(attr != NULL_STR && uuid == oak::uuid_t(attr))
{
document_ptr res = document_ptr(new document_t);
res->_identifier = uuid;
res->_backup_path = *path;
res->_path = path::get_attr(*path, "com.macromates.backup.path");
res->_key = path::identifier_t(res->_path);
res->_file_type = path::get_attr(*path, "com.macromates.backup.file-type");
res->_disk_encoding = path::get_attr(*path, "com.macromates.backup.encoding");
res->_disk_bom = path::get_attr(*path, "com.macromates.backup.bom") == "YES";
res->_disk_newlines = path::get_attr(*path, "com.macromates.backup.newlines");
res->_untitled_count = atoi(path::get_attr(*path, "com.macromates.backup.untitled-count").c_str());
res->_custom_name = path::get_attr(*path, "com.macromates.backup.custom-name");
res->_modified = path::get_attr(*path, "com.macromates.backup.modified") == "YES";
add(res);
return res;
}
}
D(DBF_Document_Tracker, bug("no instance found\n"););
return document_ptr();
}
void remove (oak::uuid_t const& uuid, path::identifier_t const& key)
{
lock_t lock(this);
D(DBF_Document_Tracker, bug("%s, %s\n", to_s(uuid).c_str(), to_s(key).c_str()););
if(key)
{
std::map<path::identifier_t, document_weak_ptr>::iterator it = documents_by_path.find(key);
ASSERTF(it != documents_by_path.end(), "%s, %s", to_s(key).c_str(), to_s(uuid).c_str());
if(!it->second.lock())
{
documents_by_path.erase(it);
}
else
{
D(DBF_Document_Tracker, bug("*** old instance replaced\n"););
}
}
ASSERT(documents.find(uuid) != documents.end());
documents.erase(uuid);
}
path::identifier_t const& update_path (document_ptr doc, path::identifier_t const& oldKey, path::identifier_t const& newKey)
{
lock_t lock(this);
D(DBF_Document_Tracker, bug("%s → %s\n", to_s(oldKey).c_str(), to_s(newKey).c_str()););
if(oldKey)
{
ASSERT(documents_by_path.find(oldKey) != documents_by_path.end());
documents_by_path.erase(oldKey);
}
if(newKey)
{
ASSERT(documents_by_path.find(newKey) == documents_by_path.end());
documents_by_path.insert(std::make_pair(newKey, doc));
}
return newKey;
}
size_t untitled_counter ()
{
lock_t lock(this);
std::set<size_t> reserved;
iterate(pair, documents)
{
if(document_ptr doc = pair->second.lock())
{
if(doc->path() == NULL_STR)
reserved.insert(doc->untitled_count());
}
}
size_t res = 1;
while(reserved.find(res) != reserved.end())
++res;
return res;
}
std::map<oak::uuid_t, document_weak_ptr> documents;
std::map<path::identifier_t, document_weak_ptr> documents_by_path;
private:
void add (document_ptr doc)
{
ASSERT_EQ(lock_count, 1); // we assert that a lock has been obtained by the caller
documents.insert(std::make_pair(doc->identifier(), doc));
if(doc->_key)
{
D(DBF_Document_Tracker, bug("%s\n", doc->path().c_str()););
std::map<path::identifier_t, document_weak_ptr>::iterator it = documents_by_path.find(doc->_key);
ASSERTF(it == documents_by_path.end() || !it->second.lock(), "%s, %s\n", to_s(doc->_key).c_str(), to_s(doc->identifier()).c_str());
if(it == documents_by_path.end())
documents_by_path.insert(std::make_pair(doc->_key, doc));
else it->second = doc;
}
}
} documents;
document_ptr create (std::string const& rawPath) { std::string const path = path::resolve(rawPath); return path::is_text_clipping(path) ? from_content(path::resource(path, typeUTF8Text, 256)) : documents.create(path, path::identifier_t(path)); }
document_ptr create (std::string const& path, path::identifier_t const& key) { return documents.create(path, key); }
document_ptr find (oak::uuid_t const& uuid, bool searchBackups) { return documents.find(uuid, searchBackups); }
document_ptr from_content (std::string const& content, std::string const& fileType)
{
D(DBF_Document, bug("%s\n", fileType.c_str()););
document_ptr doc = create();
if(fileType != NULL_STR)
doc->set_file_type(fileType);
doc->set_content(io::bytes_ptr(new io::bytes_t(content)));
return doc;
}
// ===============
// = LRU Tracker =
// ===============
static struct lru_tracker_t
{
lru_tracker_t () : did_load(false) { }
oak::date_t get (std::string const& path) const
{
if(path == NULL_STR)
return oak::date_t();
load();
std::map<std::string, oak::date_t>::const_iterator it = map.find(path);
D(DBF_Document_LRU, bug("%s → %s\n", path.c_str(), it != map.end() ? to_s(it->second).c_str() : "not found"););
return it == map.end() ? oak::date_t() : it->second;
}
void set (std::string const& path, oak::date_t const& date)
{
if(path == NULL_STR)
return;
D(DBF_Document_LRU, bug("%s → %s\n", path.c_str(), to_s(date).c_str()););
load();
map[path] = date;
save();
}
private:
void load () const
{
if(did_load)
return;
did_load = true;
if(CFPropertyListRef cfPlist = CFPreferencesCopyAppValue(CFSTR("LRUDocumentPaths"), kCFPreferencesCurrentApplication))
{
plist::dictionary_t const& plist = plist::convert(cfPlist);
D(DBF_Document_LRU, bug("%s\n", to_s(plist).c_str()););
CFRelease(cfPlist);
plist::array_t paths;
if(plist::get_key_path(plist, "paths", paths))
{
oak::date_t t = oak::date_t::now();
iterate(path, paths)
{
if(std::string const* str = boost::get<std::string>(&*path))
map.insert(std::make_pair(*str, t - (1.0 + map.size())));
}
}
}
}
void save () const
{
std::map<oak::date_t, std::string> sorted;
iterate(item, map)
sorted.insert(std::make_pair(item->second, item->first));
std::map< std::string, std::vector<std::string> > plist;
std::vector<std::string>& paths = plist["paths"];
riterate(item, sorted)
{
paths.push_back(item->second);
if(paths.size() == 50)
break;
}
D(DBF_Document_LRU, bug("%s\n", text::join(paths, ", ").c_str()););
CFPreferencesSetAppValue(CFSTR("LRUDocumentPaths"), cf::wrap(plist), kCFPreferencesCurrentApplication);
}
mutable std::map<std::string, oak::date_t> map;
mutable bool did_load;
} lru;
// =========
// = Marks =
// =========
static struct mark_tracker_t
{
typedef std::multimap<text::range_t, document_t::mark_t> marks_t;
marks_t get (std::string const& path)
{
if(path == NULL_STR)
return marks_t();
std::map<std::string, marks_t>::const_iterator it = marks.find(path);
if(it == marks.end())
it = marks.insert(std::make_pair(path, parse_marks(path::get_attr(path, "com.macromates.bookmarks")))).first;
return it->second;
}
void set (std::string const& path, marks_t const& m)
{
if(m.empty())
marks.erase(path);
else marks[path] = m;
}
std::map<std::string, marks_t> marks;
} marks;
// ==============
// = document_t =
// ==============
document_t::~document_t ()
{
D(DBF_Document, bug("%s\n", display_name().c_str()););
if(_grammar)
_grammar->remove_callback(&_grammar_callback);
if(_path != NULL_STR && _buffer)
document::marks.set(_path, marks());
documents.remove(_identifier, _key);
}
std::string document_t::display_name () const
{
if(_custom_name != NULL_STR)
return _custom_name;
if(_path != NULL_STR)
return path::display_name(_path);
if(!_untitled_count)
_untitled_count = documents.untitled_counter();
return _untitled_count == 1 ? "untitled" : text::format("untitled %zu", _untitled_count);
}
std::string document_t::backup_path () const
{
if(_backup_path == NULL_STR)
_backup_path = ::backup_path(display_name());
return _backup_path;
}
std::string document_t::file_type () const
{
D(DBF_Document, bug("%s, %s\n", display_name().c_str(), _file_type.c_str()););
return _file_type;
}
std::map<std::string, std::string> document_t::variables (std::map<std::string, std::string> map, bool sourceFileSystem) const
{
map["TM_DISPLAYNAME"] = display_name();
map["TM_DOCUMENT_UUID"] = to_s(identifier());
if(path() != NULL_STR)
{
map["TM_FILEPATH"] = path();
map["TM_FILENAME"] = path::name(path());
map["TM_DIRECTORY"] = path::parent(path());
map["PWD"] = path::parent(path());
if(scm::info_ptr info = scm::info(path::parent(path())))
{
std::string const& branch = info->branch();
if(branch != NULL_STR)
map["TM_SCM_BRANCH"] = branch;
std::string const& name = info->scm_name();
if(name != NULL_STR)
map["TM_SCM_NAME"] = name;
}
}
return sourceFileSystem ? variables_for_path(path(), scope(), map) : map;
}
void document_t::setup_buffer ()
{
D(DBF_Document, bug("%s, %s\n", display_name().c_str(), _file_type.c_str()););
if(_file_type != NULL_STR)
{
citerate(item, bundles::query(bundles::kFieldGrammarScope, _file_type, scope::wildcard, bundles::kItemTypeGrammar))
{
if(parse::grammar_ptr grammar = parse::parse_grammar(*item))
{
if(_grammar)
_grammar->remove_callback(&_grammar_callback);
_grammar = grammar;
_grammar->add_callback(&_grammar_callback);
_buffer->set_grammar(*item);
break;
}
}
}
settings_t const& settings = this->settings();
_buffer->indent() = text::indent_t(settings.get(kSettingsTabSizeKey, 4), SIZE_T_MAX, settings.get(kSettingsSoftTabsKey, false));
_buffer->set_spelling_language(settings.get(kSettingsSpellingLanguageKey, "en"));
_buffer->set_live_spelling(settings.get(kSettingsSpellCheckingKey, false));
const_cast<document_t*>(this)->broadcast(callback_t::did_change_indent_settings);
D(DBF_Document, bug("done\n"););
}
void document_t::grammar_did_change ()
{
_buffer->set_grammar(bundles::lookup(_grammar->uuid())); // Preferably wed pass _grammar to the buffer but then the buffer couldnt get at the root scope and folding markers. Perhaps this should be exposed by grammar_t, but ideally the buffer itself would setup a callback to be notified about grammar changes. We only moved it to document_t because with a callback, buffer_t cant get copy constructors for free.
}
void document_t::mark_pristine ()
{
ASSERT(_buffer);
_pristine_buffer = _buffer->substr(0, _buffer->size()); // TODO We should use a cheap ng::detail::storage_t copy
}
void document_t::post_load (std::string const& path, io::bytes_ptr content, std::map<std::string, std::string> const& attributes, std::string const& fileType, std::string const& pathAttributes, encoding::type const& encoding)
{
_open_callback.reset();
if(!content)
{
_open_count = 0;
return;
}
_path_attributes = pathAttributes;
_disk_encoding = encoding.charset();
_disk_newlines = encoding.newlines();
_disk_bom = encoding.byte_order_mark();
if(_file_type == NULL_STR)
_file_type = fileType;
if(_selection == NULL_STR)
{
std::map<std::string, std::string>::const_iterator sel = attributes.find("com.macromates.selectionRange");
std::map<std::string, std::string>::const_iterator rect = attributes.find("com.macromates.visibleRect");
_selection = sel != attributes.end() ? sel->second : NULL_STR;
_visible_rect = rect != attributes.end() ? rect->second : NULL_STR;
}
_is_on_disk = _path != NULL_STR && access(_path.c_str(), F_OK) == 0;
if(_is_on_disk)
_file_watcher.reset(new watch_t(_path, shared_from_this()));
_buffer.reset(new ng::buffer_t);
setup_buffer();
if(content)
{
_buffer->insert(0, std::string(content->begin(), content->end()));
setup_marks(path, *_buffer);
std::map<std::string, std::string>::const_iterator folded = attributes.find("com.macromates.folded");
if(folded != attributes.end())
_folded = folded->second;
}
_buffer->bump_revision();
check_modified(_buffer->revision(), _buffer->revision());
mark_pristine();
_undo_manager.reset(new ng::undo_manager_t(buffer()));
broadcast(callback_t::did_change_open_status);
}
void document_t::post_save (std::string const& path, io::bytes_ptr content, std::string const& pathAttributes, encoding::type const& encoding, bool success)
{
if(success)
{
_key = documents.update_path(shared_from_this(), _key, path::identifier_t(_path));
_is_on_disk = true;
_path_attributes = pathAttributes;
_disk_encoding = encoding.charset();
_disk_bom = encoding.byte_order_mark();
_disk_newlines = encoding.newlines();
check_modified(revision(), revision());
mark_pristine();
broadcast(callback_t::did_save);
D(DBF_Document, bug("search for did save hooks in scope %s\n", to_s(scope()).c_str()););
citerate(item, bundles::query(bundles::kFieldSemanticClass, "callback.document.did-save", scope()))
{
D(DBF_Document, bug("%s\n", (*item)->name().c_str()););
document::run(parse_command(*item), buffer(), ng::ranges_t(), shared_from_this());
}
}
if(_is_on_disk)
_file_watcher.reset(new watch_t(_path, shared_from_this()));
}
encoding::type document_t::encoding_for_save_as_path (std::string const& path)
{
encoding::type res = disk_encoding();
settings_t const& settings = settings_for_path(path);
if(!is_on_disk() || res.charset() == kCharsetNoEncoding)
{
res.set_charset(settings.get(kSettingsEncodingKey, kCharsetUTF8));
res.set_byte_order_mark(settings.get(kSettingsUseBOMKey, res.byte_order_mark()));
}
if(!is_on_disk() || res.newlines() == NULL_STR)
res.set_newlines(settings.get(kSettingsLineEndingsKey, "\n"));
return res;
}
void document_t::try_save (document::save_callback_ptr callback)
{
struct save_callback_wrapper_t : file::save_callback_t
{
save_callback_wrapper_t (document::document_ptr doc, document::save_callback_ptr callback, bool close) : _document(doc), _callback(callback), _close(close) { }
void select_path (std::string const& path, io::bytes_ptr content, file::save_context_ptr context) { _callback->select_path(path, content, context); }
void select_make_writable (std::string const& path, io::bytes_ptr content, file::save_context_ptr context) { _callback->select_make_writable(path, content, context); }
void obtain_authorization (std::string const& path, io::bytes_ptr content, osx::authorization_t auth, file::save_context_ptr context) { _callback->obtain_authorization(path, content, auth, context); }
void select_charset (std::string const& path, io::bytes_ptr content, std::string const& charset, file::save_context_ptr context) { _callback->select_charset(path, content, charset, context); }
void did_save (std::string const& path, io::bytes_ptr content, std::string const& pathAttributes, encoding::type const& encoding, bool success, std::string const& message, oak::uuid_t const& filter)
{
_document->post_save(path, content, pathAttributes, encoding, success);
_callback->did_save_document(_document, path, success, message, filter);
if(_close)
_document->close();
}
private:
document::document_ptr _document;
document::save_callback_ptr _callback;
bool _close;
};
D(DBF_Document, bug("save %s\n", _path.c_str()););
bool closeAfterSave = false;
if(!is_open())
{
if(!_content && _backup_path == NULL_STR)
return callback->did_save(_path, io::bytes_ptr(), _path_attributes, encoding::type(_disk_newlines, _disk_encoding, _disk_bom), false, NULL_STR, oak::uuid_t());
open();
closeAfterSave = true;
}
_file_watcher.reset();
io::bytes_ptr bytes(new io::bytes_t(content()));
std::map<std::string, std::string> attributes;
if(volume::settings(_path).extended_attributes())
{
attributes["com.macromates.selectionRange"] = _selection;
attributes["com.macromates.visibleRect"] = _visible_rect;
attributes["com.macromates.bookmarks"] = marks_as_string();
attributes["com.macromates.folded"] = _folded;
}
save_callback_wrapper_t* cb = new save_callback_wrapper_t(shared_from_this(), callback, closeAfterSave);
save_callback_ptr sharedPtr((save_callback_t*)cb);
encoding::type const encoding = encoding_for_save_as_path(_path);
file::save(_path, sharedPtr, _authorization, bytes, attributes, _file_type, encoding, std::vector<oak::uuid_t>() /* binary import filters */, std::vector<oak::uuid_t>() /* text import filters */);
}
bool document_t::save ()
{
struct stall_t : save_callback_t
{
stall_t (bool& res) : _res(res), _run_loop(CFSTR("OakThreadSignalsRunLoopMode")) { }
void did_save_document (document_ptr document, std::string const& path, bool success, std::string const& message, oak::uuid_t const& filter)
{
_res = success;
_run_loop.stop();
}
void wait () { _run_loop.start(); }
private:
bool& _res;
cf::run_loop_t _run_loop;
};
bool res = false;
stall_t* cb = new stall_t(res);
save_callback_ptr sharedPtr((save_callback_t*)cb);
try_save(sharedPtr);
cb->wait();
return res;
}
bool document_t::backup ()
{
ASSERT(_buffer);
std::string const& dst = backup_path();
if(path::set_content(dst, content()))
{
path::set_attr(dst, "com.macromates.backup.path", _path);
path::set_attr(dst, "com.macromates.backup.identifier", to_s(_identifier));
path::set_attr(dst, "com.macromates.selectionRange", _selection);
path::set_attr(dst, "com.macromates.visibleRect", _visible_rect);
path::set_attr(dst, "com.macromates.backup.file-type", _file_type);
path::set_attr(dst, "com.macromates.backup.encoding", _disk_encoding);
path::set_attr(dst, "com.macromates.backup.bom", _disk_bom ? "YES" : "NO");
path::set_attr(dst, "com.macromates.backup.newlines", _disk_newlines);
path::set_attr(dst, "com.macromates.backup.untitled-count", text::format("%zu", _untitled_count));
path::set_attr(dst, "com.macromates.backup.custom-name", _custom_name);
path::set_attr(dst, "com.macromates.bookmarks", marks_as_string());
path::set_attr(dst, "com.macromates.folded", NULL_STR);
if(is_modified())
path::set_attr(dst, "com.macromates.backup.modified", "YES");
// TODO tab size, spell checking, soft wrap, etc. should go into session!?!
_backup_revision = revision();
return true;
}
return false;
}
void document_t::check_modified (ssize_t diskRev, ssize_t rev)
{
_disk_revision = diskRev;
_revision = rev;
set_modified(_revision != _disk_revision && (!buffer().empty() || is_on_disk()));
if(is_modified())
schedule_backup(identifier());
else cancel_backup(identifier());
}
bool document_t::is_modified () const
{
return _modified;
}
void document_t::set_modified (bool flag)
{
if(_modified != flag)
{
_modified = flag;
broadcast(callback_t::did_change_modified_status);
if(!_modified && _backup_path != NULL_STR && access(_backup_path.c_str(), F_OK) == 0)
unlink(_backup_path.c_str());
}
}
void document_t::set_path (std::string const& newPath)
{
std::string const& normalizedPath = path::resolve(newPath);
if(_path == normalizedPath)
return;
_path = normalizedPath;
_key = documents.update_path(shared_from_this(), _key, path::identifier_t(normalizedPath));
if(is_open())
{
_is_on_disk = access(_path.c_str(), F_OK) == 0;
_file_watcher.reset(_is_on_disk ? new watch_t(_path, shared_from_this()) : NULL);
std::string newFileType = file::type(_path, io::bytes_ptr(new io::bytes_t(content())), _virtual_path);
if(newFileType != NULL_STR)
set_file_type(newFileType);
}
_custom_name = NULL_STR;
broadcast(callback_t::did_change_path);
}
bool document_t::try_open (document::open_callback_ptr callback)
{
if(++_open_count == 1)
{
if(_backup_path != NULL_STR)
{
bool modified = _modified;
post_load(_path, io::bytes_ptr(new io::bytes_t(path::content(_backup_path))), path::attributes(_backup_path), _file_type, file::path_attributes(_path), encoding::type(_disk_newlines, _disk_encoding, _disk_bom));
if(modified)
set_revision(buffer().bump_revision());
return true;
}
_open_callback.reset(new open_callback_wrapper_t(shared_from_this(), callback));
file::open(_path, _authorization, _open_callback, _content, _virtual_path);
_content.reset();
return false;
}
else if(_open_callback)
{
_open_callback->add_callback(callback);
return false;
}
else
{
ASSERT(_buffer); // load completed
return true;
}
}
void document_t::open ()
{
struct stall_t : document::open_callback_t
{
stall_t () : _run_loop(CFSTR("OakThreadSignalsRunLoopMode")) { }
void show_document (std::string const& path, document_ptr document) { _run_loop.stop(); }
void show_error (std::string const& path, document_ptr document, std::string const& message, oak::uuid_t const& filter) { _run_loop.stop(); }
void wait () { _run_loop.start(); }
private:
cf::run_loop_t _run_loop;
};
stall_t* cb = new stall_t;
document::open_callback_ptr sharedPtr((document::open_callback_t*)cb);
if(!try_open(sharedPtr))
cb->wait();
}
void document_t::close ()
{
if(--_open_count != 0)
return;
broadcast(callback_t::did_change_open_status);
_file_watcher.reset();
if(_path != NULL_STR && !is_modified() && volume::settings(_path).extended_attributes())
{
D(DBF_Document, bug("save attributes for %s\n", _path.c_str()););
path::set_attr(_path, "com.macromates.selectionRange", _selection);
path::set_attr(_path, "com.macromates.visibleRect", _visible_rect);
path::set_attr(_path, "com.macromates.bookmarks", marks_as_string());
}
if(_backup_path != NULL_STR && access(_backup_path.c_str(), F_OK) == 0)
unlink(_backup_path.c_str());
_backup_path = NULL_STR;
check_modified(-1, -1);
_undo_manager.reset();
_buffer.reset();
_pristine_buffer = NULL_STR;
}
void document_t::show ()
{
_has_lru = true;
document::lru.set(_path, _lru = oak::date_t::now());
}
void document_t::hide ()
{
_has_lru = true;
document::lru.set(_path, _lru = oak::date_t::now());
}
oak::date_t const& document_t::lru () const
{
if(!_has_lru)
{
_has_lru = true;
_lru = document::lru.get(_path);
}
return _lru;
}
void document_t::watch_callback (int flags, std::string const& newPath, bool async)
{
ASSERT(_file_watcher);
ASSERT(is_open());
// NOTE_ATTRIB
if((flags & NOTE_RENAME) == NOTE_RENAME)
{
set_path(newPath);
}
else if((flags & NOTE_DELETE) == NOTE_DELETE)
{
D(DBF_Document_WatchFS, bug("%s deleted\n", _path.c_str()););
if(_is_on_disk && !(_is_on_disk = access(_path.c_str(), F_OK) == 0))
broadcast(callback_t::did_change_on_disk_status);
}
else if((flags & NOTE_WRITE) == NOTE_WRITE || (flags & NOTE_CREATE) == NOTE_CREATE)
{
struct open_callback_t : file::open_callback_t
{
open_callback_t (document::document_ptr doc, bool async) : _document(doc), _wait(!async) { }
void select_charset (std::string const& path, io::bytes_ptr content, file::open_context_ptr context) { context->set_charset(_document->_disk_encoding); }
void select_line_feeds (std::string const& path, io::bytes_ptr content, file::open_context_ptr context) { context->set_line_feeds(_document->_disk_newlines); }
void select_file_type (std::string const& path, io::bytes_ptr content, file::open_context_ptr context) { context->set_file_type(_document->_file_type); }
void show_error (std::string const& path, std::string const& message, oak::uuid_t const& filter) { fprintf(stderr, "%s: %s\n", path.c_str(), message.c_str()); }
void show_content (std::string const& path, io::bytes_ptr content, std::map<std::string, std::string> const& attributes, std::string const& fileType, std::string const& pathAttributes, encoding::type const& encoding, std::vector<oak::uuid_t> const& binaryImportFilters, std::vector<oak::uuid_t> const& textImportFilters)
{
if(!_document->is_open())
return;
std::string const& yours = std::string(content->begin(), content->end());
std::string const& mine = _document->content();
if(yours == mine)
{
D(DBF_Document_WatchFS, bug("yours == mine, marking document as not modified\n"););
_document->set_disk_revision(_document->revision());
_document->mark_pristine();
}
else if(!_document->is_modified())
{
D(DBF_Document_WatchFS, bug("changed on disk and we have no local changes, so reverting to that\n"););
_document->undo_manager().begin_undo_group(ng::ranges_t(0));
_document->_buffer->replace(0, _document->_buffer->size(), yours);
_document->_buffer->bump_revision();
_document->check_modified(_document->_buffer->revision(), _document->_buffer->revision());
_document->mark_pristine();
_document->undo_manager().end_undo_group(ng::ranges_t(0));
}
else
{
bool conflict = false;
std::string const& merged = merge(_document->_pristine_buffer, mine, yours, &conflict);
D(DBF_Document_WatchFS, bug("changed on disk and we have local changes, merge conflict %s.\n%s\n", BSTR(conflict), merged.c_str()););
_document->undo_manager().begin_undo_group(ng::ranges_t(0));
_document->_buffer->replace(0, _document->_buffer->size(), merged);
_document->set_revision(_document->_buffer->bump_revision());
_document->undo_manager().end_undo_group(ng::ranges_t(0));
// TODO if there was a conflict, we shouldnt take the merged content (but ask user what to do)
// TODO mark_pristine() but using yours
}
if(_wait)
_run_loop.stop();
}
void wait () { if(_wait) _run_loop.start(); }
private:
document::document_ptr _document;
bool _wait;
cf::run_loop_t _run_loop;
};
if(!_is_on_disk && (_is_on_disk = access(_path.c_str(), F_OK) == 0))
broadcast(callback_t::did_change_on_disk_status);
open_callback_t* raw = new open_callback_t(shared_from_this(), async);
file::open_callback_ptr cb((file::open_callback_t*)raw);
file::open(_path, _authorization, cb);
raw->wait();
}
}
void document_t::set_file_type (std::string const& newFileType)
{
D(DBF_Document, bug("%s → %s (%s)\n", _file_type.c_str(), newFileType.c_str(), display_name().c_str()););
if(_file_type != newFileType)
{
_file_type = newFileType;
if(_buffer)
setup_buffer();
broadcast(callback_t::did_change_file_type);
}
}
void document_t::set_content (io::bytes_ptr const& bytes)
{
D(DBF_Document, bug("%.*s… (%zu bytes), file type %s\n", std::min<int>(32, bytes->size()), bytes->get(), bytes->size(), _file_type.c_str()););
ASSERT(!_buffer);
_content = bytes;
}
namespace
{
struct file_reader_t : reader::open_t
{
WATCH_LEAKS(file_reader_t);
file_reader_t (document_const_ptr const& document) : reader::open_t(document->path()), document(document) { }
private:
document_const_ptr document;
};
struct buffer_reader_t : document::document_t::reader_t
{
WATCH_LEAKS(buffer_reader_t);
buffer_reader_t (io::bytes_ptr const& data) : _data(data) { }
io::bytes_ptr next ()
{
io::bytes_ptr res = _data;
_data.reset();
return res;
}
private:
io::bytes_ptr _data;
};
}
document_t::reader_ptr document_t::create_reader () const
{
if(is_open())
return reader_ptr(new buffer_reader_t(io::bytes_ptr(new io::bytes_t(content()))));
return reader_ptr(new file_reader_t(shared_from_this()));
}
// ===========
// = Replace =
// ===========
void document_t::replace (std::multimap<text::range_t, std::string> const& replacements)
{
ASSERT(!is_open());
if(replacements.empty())
return;
ASSERT(_path != NULL_STR);
ASSERT(!_buffer)
ng::buffer_t buf;
buf.insert(0, path::content(_path));
riterate(pair, replacements)
{
D(DBF_Document_Replace, bug("replace %s with %s\n", std::string(pair->first).c_str(), pair->second.c_str()););
buf.replace(buf.convert(pair->first.min()), buf.convert(pair->first.max()), pair->second);
}
_content.reset(new io::bytes_t(buf.substr(0, buf.size())));
}
static ng::index_t cap (ng::buffer_t const& buf, text::pos_t const& pos)
{
size_t line = oak::cap<size_t>(0, pos.line, buf.lines()-1);
size_t col = oak::cap<size_t>(0, pos.column, buf.eol(line) - buf.begin(line));
ng::index_t res = buf.sanitize_index(buf.convert(text::pos_t(line, col)));
if(pos.offset && res.index < buf.size() && buf[res.index] == "\n")
res.carry = pos.offset;
return res;
}
// =========
// = Marks =
// =========
void document_t::load_marks (std::string const& src) const
{
if(_did_load_marks)
return;
if(src != NULL_STR)
{
_marks = document::marks.get(src);
document::marks.set(src, std::multimap<text::range_t, document_t::mark_t>());
}
_did_load_marks = true;
}
static void copy_marks (ng::buffer_t& buf, std::multimap<text::range_t, document_t::mark_t> const& marks)
{
iterate(pair, marks)
buf.set_mark(cap(buf, pair->first.from).index, pair->second.type);
}
void document_t::setup_marks (std::string const& src, ng::buffer_t& buf) const
{
if(_did_load_marks)
{
copy_marks(buf, _marks);
}
else if(src != NULL_STR)
{
copy_marks(buf, document::marks.get(src));
document::marks.set(src, std::multimap<text::range_t, document_t::mark_t>());
}
}
std::multimap<text::range_t, document_t::mark_t> document_t::marks () const
{
if(_buffer)
{
std::multimap<text::range_t, document_t::mark_t> res;
citerate(pair, _buffer->get_marks(0, _buffer->size()))
res.insert(std::make_pair(_buffer->convert(pair->first), pair->second));
return res;
}
return document::marks.get(_path);
}
void document_t::add_mark (text::range_t const& range, mark_t const& mark)
{
if(_buffer)
{
_buffer->set_mark(_buffer->convert(range.from), mark.type);
}
else
{
load_marks(_path);
_marks.insert(std::make_pair(range, mark));
}
broadcast(callback_t::did_change_marks);
}
void document_t::remove_all_marks (std::string const& typeToClear)
{
if(_buffer)
{
_buffer->remove_all_marks(typeToClear);
}
else
{
load_marks(_path);
std::multimap<text::range_t, mark_t> newMarks;
if(typeToClear != NULL_STR)
{
iterate(it, _marks)
{
if(it->second.type != typeToClear)
newMarks.insert(*it);
}
}
_marks.swap(newMarks);
}
broadcast(callback_t::did_change_marks);
}
std::string document_t::marks_as_string () const
{
std::vector<std::string> v;
if(_buffer)
{
citerate(pair, _buffer->get_marks(0, _buffer->size()))
{
if(pair->second == "bookmark")
v.push_back(text::format("'%s'", std::string(_buffer->convert(pair->first)).c_str()));
}
}
else
{
load_marks(_path);
iterate(mark, _marks)
{
if(mark->second.type == "bookmark")
v.push_back(text::format("'%s'", std::string(mark->first).c_str()));
}
}
return v.empty() ? NULL_STR : "( " + text::join(v, ", ") + " )";
}
// ===========
// = Symbols =
// ===========
std::map<text::pos_t, std::string> document_t::symbols ()
{
if(!_buffer)
return std::map<text::pos_t, std::string>();
_buffer->wait_for_repair();
std::map<text::pos_t, std::string> res;
citerate(pair, _buffer->symbols())
res.insert(std::make_pair(_buffer->convert(pair->first), pair->second));
return res;
}
// ====================
// = Document scanner =
// ====================
std::vector<document_ptr> scanner_t::open_documents ()
{
std::vector<document_ptr> res;
document_tracker_t::lock_t lock(&document::documents);
iterate(pair, document::documents.documents)
{
document_ptr doc = pair->second.lock();
if(doc && doc->is_open())
res.push_back(doc);
}
return res;
}
scanner_t::scanner_t (std::string const& path, path::glob_list_t const& glob, bool follow_links, bool depth_first) : path(path), glob(glob), follow_links(follow_links), depth_first(depth_first), is_running_flag(true), should_stop_flag(false)
{
D(DBF_Document_Scanner, bug("%s, links %s\n", path.c_str(), BSTR(follow_links)););
document_tracker_t::lock_t lock(&document::documents);
iterate(pair, document::documents.documents)
{
document_ptr doc = pair->second.lock();
if(doc && doc->path() == NULL_STR)
documents.push_back(doc);
}
struct bootstrap_t { static void* main (void* arg) { ((scanner_t*)arg)->thread_main(); return NULL; } };
pthread_mutex_init(&mutex, NULL);
pthread_create(&thread, NULL, &bootstrap_t::main, this);
}
scanner_t::~scanner_t ()
{
D(DBF_Document_Scanner, bug("\n"););
stop();
wait();
pthread_mutex_destroy(&mutex);
}
std::vector<document_ptr> scanner_t::accept_documents ()
{
pthread_mutex_lock(&mutex);
std::vector<document_ptr> res;
res.swap(documents);
pthread_mutex_unlock(&mutex);
return res;
}
std::string scanner_t::get_current_path () const
{
pthread_mutex_lock(&mutex);
std::string res = current_path;
pthread_mutex_unlock(&mutex);
return res;
}
void scanner_t::thread_main ()
{
oak::set_thread_name("document::scanner_t");
scan_dir(path);
D(DBF_Document_Scanner, bug("running %s → NO\n", BSTR(is_running_flag)););
is_running_flag = false;
}
void scanner_t::scan_dir (std::string const& initialPath)
{
D(DBF_Document_Scanner, bug("%s, running %s\n", initialPath.c_str(), BSTR(is_running_flag)););
std::deque<std::string> dirs(1, initialPath);
std::vector<std::string> links;
while(!dirs.empty())
{
std::string dir = dirs.front();
dirs.pop_front();
struct stat buf;
if(lstat(initialPath.c_str(), &buf) != 0) // get st_dev so we dont need to stat each path entry (unless it is a symbolic link)
continue;
ASSERT(S_ISDIR(buf.st_mode) || S_ISLNK(buf.st_mode));
pthread_mutex_lock(&mutex);
current_path = dir;
pthread_mutex_unlock(&mutex);
std::vector<std::string> newDirs;
std::multimap<std::string, path::identifier_t, text::less_t> files;
citerate(it, path::entries(dir))
{
if(should_stop_flag)
break;
std::string const& path = path::join(dir, (*it)->d_name);
if((*it)->d_type == DT_DIR)
{
if(glob.exclude(path, path::kPathItemDirectory))
continue;
if(seen_paths.insert(std::make_pair(buf.st_dev, (*it)->d_ino)).second)
newDirs.push_back(path);
else D(DBF_Document_Scanner, bug("skip known path: %s\n", path.c_str()););
}
else if((*it)->d_type == DT_REG)
{
if(glob.exclude(path, path::kPathItemFile))
continue;
if(seen_paths.insert(std::make_pair(buf.st_dev, (*it)->d_ino)).second)
files.insert(std::make_pair(path, path::identifier_t(true, buf.st_dev, (*it)->d_ino, path)));
else D(DBF_Document_Scanner, bug("skip known path: %s\n", path.c_str()););
}
else if((*it)->d_type == DT_LNK)
{
links.push_back(path); // handle later since link may point to another device plus if link is “local” and will be seen later, we reported the local path rather than this link
}
}
std::sort(newDirs.begin(), newDirs.end(), text::less_t());
dirs.insert(depth_first ? dirs.begin() : dirs.end(), newDirs.begin(), newDirs.end());
if(dirs.empty())
{
iterate(link, links)
{
std::string path = path::resolve(*link);
if(lstat(path.c_str(), &buf) == 0)
{
if(S_ISDIR(buf.st_mode) && follow_links && seen_paths.insert(std::make_pair(buf.st_dev, buf.st_ino)).second)
{
if(glob.exclude(path, path::kPathItemDirectory))
continue;
D(DBF_Document_Scanner, bug("follow link: %s → %s\n", link->c_str(), path.c_str()););
dirs.push_back(path);
}
else if(S_ISREG(buf.st_mode))
{
if(glob.exclude(path, path::kPathItemFile))
continue;
if(seen_paths.insert(std::make_pair(buf.st_dev, buf.st_ino)).second)
files.insert(std::make_pair(path, path::identifier_t(true, buf.st_dev, buf.st_ino, path)));
else D(DBF_Document_Scanner, bug("skip known path: %s\n", path.c_str()););
}
}
}
links.clear();
}
pthread_mutex_lock(&mutex);
iterate(file, files)
documents.push_back(document::create(file->first, file->second));
pthread_mutex_unlock(&mutex);
}
}
} /* document */