brackets-git
Version:
Integration of Git into Brackets
419 lines (353 loc) • 14.4 kB
JavaScript
// this file was composed with a big help from @MiguelCastillo extension Brackets-InteractiveLinter
// @see https://github.com/MiguelCastillo/Brackets-InteractiveLinter
define(function (require, exports) {
// Brackets modules
var _ = brackets.getModule("thirdparty/lodash"),
CommandManager = brackets.getModule("command/CommandManager"),
DocumentManager = brackets.getModule("document/DocumentManager"),
EditorManager = brackets.getModule("editor/EditorManager"),
MainViewManager = brackets.getModule("view/MainViewManager"),
ErrorHandler = require("src/ErrorHandler"),
Events = require("src/Events"),
EventEmitter = require("src/EventEmitter"),
Git = require("src/git/Git"),
Preferences = require("./Preferences"),
Strings = require("strings");
var gitAvailable = false,
gutterName = "brackets-git-gutter",
editorsWithGutters = [],
openWidgets = [];
function clearWidgets() {
var lines = openWidgets.map(function (mark) {
var w = mark.lineWidget;
if (w.visible) {
w.visible = false;
w.widget.clear();
}
return {
cm: mark.cm,
line: mark.line
};
});
openWidgets = [];
return lines;
}
function clearOld(editor) {
var cm = editor._codeMirror;
if (!cm) { return; }
var gutters = cm.getOption("gutters").slice(0),
io = gutters.indexOf(gutterName);
if (io !== -1) {
gutters.splice(io, 1);
cm.clearGutter(gutterName);
cm.setOption("gutters", gutters);
cm.off("gutterClick", gutterClick);
}
delete cm.gitGutters;
clearWidgets();
}
function prepareGutter(editor) {
// add our gutter if its not already available
var cm = editor._codeMirror;
var gutters = cm.getOption("gutters").slice(0);
if (gutters.indexOf(gutterName) === -1) {
gutters.unshift(gutterName);
cm.setOption("gutters", gutters);
cm.on("gutterClick", gutterClick);
}
if (editorsWithGutters.indexOf(editor) === -1) {
editorsWithGutters.push(editor);
}
}
function prepareGutters(editors) {
editors.forEach(function (editor) {
prepareGutter(editor);
});
// clear the rest
var idx = editorsWithGutters.length;
while (idx--) {
if (editors.indexOf(editorsWithGutters[idx]) === -1) {
clearOld(editorsWithGutters[idx]);
editorsWithGutters.splice(idx, 1);
}
}
}
function showGutters(editor, _results) {
prepareGutter(editor);
var cm = editor._codeMirror;
cm.gitGutters = _.sortBy(_results, "line");
// get line numbers of currently opened widgets
var openBefore = clearWidgets();
cm.clearGutter(gutterName);
cm.gitGutters.forEach(function (obj) {
var $marker = $("<div>")
.addClass(gutterName + "-" + obj.type + " gitline-" + (obj.line + 1))
.html(" ");
cm.setGutterMarker(obj.line, gutterName, $marker[0]);
});
// reopen widgets that were opened before refresh
openBefore.forEach(function (obj) {
gutterClick(obj.cm, obj.line, gutterName);
});
}
function gutterClick(cm, lineIndex, gutterId) {
if (!cm) {
return;
}
if (gutterId !== gutterName && gutterId !== "CodeMirror-linenumbers") {
return;
}
var mark = _.find(cm.gitGutters, function (o) { return o.line === lineIndex; });
if (!mark || mark.type === "added") { return; }
// we need to be able to identify cm instance from any mark
mark.cm = cm;
if (mark.parentMark) { mark = mark.parentMark; }
if (!mark.lineWidget) {
mark.lineWidget = {
visible: false,
element: $("<div class='" + gutterName + "-deleted-lines'></div>")
};
var $btn = $("<button/>")
.addClass("brackets-git-gutter-copy-button")
.text("R")
.on("click", function () {
var doc = DocumentManager.getCurrentDocument();
doc.replaceRange(mark.content + "\n", {
line: mark.line,
ch: 0
});
CommandManager.execute("file.save");
refresh();
});
$("<pre/>")
.attr("style", "tab-size:" + cm.getOption("tabSize"))
.text(mark.content || " ")
.append($btn)
.appendTo(mark.lineWidget.element);
}
if (mark.lineWidget.visible !== true) {
mark.lineWidget.visible = true;
mark.lineWidget.widget = cm.addLineWidget(mark.line, mark.lineWidget.element[0], {
coverGutter: false,
noHScroll: false,
above: true,
showIfHidden: false
});
openWidgets.push(mark);
} else {
mark.lineWidget.visible = false;
mark.lineWidget.widget.clear();
var io = openWidgets.indexOf(mark);
if (io !== -1) {
openWidgets.splice(io, 1);
}
}
}
function getEditorFromPane(paneId) {
var currentPath = MainViewManager.getCurrentlyViewedPath(paneId),
doc = currentPath && DocumentManager.getOpenDocumentForPath(currentPath);
return doc && doc._masterEditor;
}
function processDiffResults(editor, diff) {
var added = [],
removed = [],
modified = [],
changesets = diff.split(/\n@@/).map(function (str) { return "@@" + str; });
// remove part before first
changesets.shift();
changesets.forEach(function (str) {
var m = str.match(/^@@ -([,0-9]+) \+([,0-9]+) @@/);
var s1 = m[1].split(",");
var s2 = m[2].split(",");
// removed stuff
var lineRemovedFrom;
var lineFrom = parseInt(s2[0], 10);
var lineCount = parseInt(s1[1], 10);
if (isNaN(lineCount)) { lineCount = 1; }
if (lineCount > 0) {
lineRemovedFrom = lineFrom - 1;
removed.push({
type: "removed",
line: lineRemovedFrom,
content: str.split("\n")
.filter(function (l) { return l.indexOf("-") === 0; })
.map(function (l) { return l.substring(1); })
.join("\n")
});
}
// added stuff
lineFrom = parseInt(s2[0], 10);
lineCount = parseInt(s2[1], 10);
if (isNaN(lineCount)) { lineCount = 1; }
var isModifiedMark = false;
var firstAddedMark = false;
for (var i = lineFrom, lineTo = lineFrom + lineCount; i < lineTo; i++) {
var lineNo = i - 1;
if (lineNo === lineRemovedFrom) {
// modified
var o = removed.pop();
o.type = "modified";
modified.push(o);
isModifiedMark = o;
} else {
var mark = {
type: isModifiedMark ? "modified" : "added",
line: lineNo,
parentMark: isModifiedMark || firstAddedMark || null
};
if (!isModifiedMark && !firstAddedMark) {
firstAddedMark = mark;
}
// added new
added.push(mark);
}
}
});
// fix displaying of removed lines
removed.forEach(function (o) {
o.line = o.line + 1;
});
showGutters(editor, [].concat(added, removed, modified));
}
function refresh() {
if (!gitAvailable) {
return;
}
if (!Preferences.get("useGitGutter")) {
return;
}
var currentGitRoot = Preferences.get("currentGitRoot");
// we get a list of editors, which need to be refreshed
var editors = _.compact(_.map(MainViewManager.getPaneIdList(), function (paneId) {
return getEditorFromPane(paneId);
}));
// we create empty gutters in all of these editors, all other editors lose their gutters
prepareGutters(editors);
// now we launch a diff to fill the gutters in our editors
editors.forEach(function (editor) {
var currentFilePath = null;
if (editor.document && editor.document.file) {
currentFilePath = editor.document.file.fullPath;
}
if (currentFilePath.indexOf(currentGitRoot) !== 0) {
// file is not in the current project
return;
}
var filename = currentFilePath.substring(currentGitRoot.length);
Git.diffFile(filename).then(function (diff) {
processDiffResults(editor, diff);
}).catch(function (err) {
// if this is launched in a non-git repository, just ignore
if (ErrorHandler.contains(err, "Not a git repository")) {
return;
}
// if this file was moved or deleted before this command could be executed, ignore
if (ErrorHandler.contains(err, "No such file or directory")) {
return;
}
ErrorHandler.showError(err, "Refreshing gutter failed!");
});
});
}
function goToPrev() {
var activeEditor = EditorManager.getActiveEditor();
if (!activeEditor) { return; }
var results = activeEditor._codeMirror.gitGutters || [];
var searched = _.filter(results, function (i) { return !i.parentMark; });
var currentPos = activeEditor.getCursorPos();
var i = searched.length;
while (i--) {
if (searched[i].line < currentPos.line) {
break;
}
}
if (i > -1) {
var goToMark = searched[i];
activeEditor.setCursorPos(goToMark.line, currentPos.ch);
}
}
function goToNext() {
var activeEditor = EditorManager.getActiveEditor();
if (!activeEditor) { return; }
var results = activeEditor._codeMirror.gitGutters || [];
var searched = _.filter(results, function (i) { return !i.parentMark; });
var currentPos = activeEditor.getCursorPos();
for (var i = 0, l = searched.length; i < l; i++) {
if (searched[i].line > currentPos.line) {
break;
}
}
if (i < searched.length) {
var goToMark = searched[i];
activeEditor.setCursorPos(goToMark.line, currentPos.ch);
}
}
var _timer;
var $line = $(),
$gitGutterLines = $();
$(document)
.on("mouseenter", ".CodeMirror-linenumber", function (evt) {
var $target = $(evt.target);
// Remove tooltip
$line.attr("title", "");
// Remove any misc gutter hover classes
$(".CodeMirror-linenumber").removeClass("brackets-git-gutter-hover");
$(".brackets-git-gutter-hover").removeClass("brackets-git-gutter-hover");
// Add new gutter hover classes
$gitGutterLines = $(".gitline-" + $target.html()).addClass("brackets-git-gutter-hover");
// Add tooltips if there are any git gutter marks
if ($gitGutterLines.hasClass("brackets-git-gutter-modified") ||
$gitGutterLines.hasClass("brackets-git-gutter-removed")) {
$line = $target.attr("title", Strings.GUTTER_CLICK_DETAILS);
$target.addClass("brackets-git-gutter-hover");
}
})
.on("mouseleave", ".CodeMirror-linenumber", function (evt) {
var $target = $(evt.target);
if (_timer) {
clearTimeout(_timer);
}
_timer = setTimeout(function () {
$(".gitline-" + $target.html()).removeClass("brackets-git-gutter-hover");
$target.removeClass("brackets-git-gutter-hover");
}, 500);
});
// Event handlers
EventEmitter.on(Events.GIT_ENABLED, function () {
gitAvailable = true;
refresh();
});
EventEmitter.on(Events.GIT_DISABLED, function () {
gitAvailable = false;
// calling this with an empty array will remove gutters from all editor instances
prepareGutters([]);
});
EventEmitter.on(Events.BRACKETS_CURRENT_DOCUMENT_CHANGE, function (evt, file) {
// file will be null when switching to an empty pane
if (!file) { return; }
// document change gets launched even when switching panes,
// so we check if the file hasn't already got the gutters
var alreadyOpened = _.filter(editorsWithGutters, function (editor) {
return editor.document.file.fullPath === file.fullPath;
}).length > 0;
if (!alreadyOpened) {
// TODO: here we could sent a particular file to be refreshed only
refresh();
}
});
EventEmitter.on(Events.GIT_COMMITED, function () {
refresh();
});
EventEmitter.on(Events.BRACKETS_FILE_CHANGED, function (evt, file) {
var alreadyOpened = _.filter(editorsWithGutters, function (editor) {
return editor.document.file.fullPath === file.fullPath;
}).length > 0;
if (alreadyOpened) {
// TODO: here we could sent a particular file to be refreshed only
refresh();
}
});
// API
exports.goToPrev = goToPrev;
exports.goToNext = goToNext;
});