Skip to content

Instantly share code, notes, and snippets.

@borkxs
Last active August 12, 2016 15:17
Show Gist options
  • Save borkxs/8b400c3430b20169d3e0980b345cd0d4 to your computer and use it in GitHub Desktop.
Save borkxs/8b400c3430b20169d3e0980b345cd0d4 to your computer and use it in GitHub Desktop.
better index.ext handling for atom

There are three things being changed

  1. How atom/fuzzaldrin-plus filters the pathnames. The project does some clever match scoring so that it can support runs of strings as well as acronym searching, so "ctl" could match in "SuperThingCtrl", and "stc" could match "SuperThingCtrl"

    • Right now the modified scoring agressively strips down strings that contain "index" to just the directory that precedes index (this is probably overkill). So if it gets web/components/dropdown/index.tsx it scores against just the substring dropdown, which artificially inflates the score for that pathname.
      • I can't explain exactly why this works but intuitively dropdown should match dropdown better than it matches some/other/stuff/dropdown/stuff/index.tsx
    • Some example scores (after mod), in the order they are performed. We can see that dropdown/index is higher than the others)
    • scores
  2. How atom/fuzzy-finder displays filenames with index

    • strip off the /index of the primary name being displayed, keep full path
    • dropdown_example
  3. How atom/tabs display

    • There's a default that shows the directory name when there are two indexes open but we want it to do that for all indexes.
    • NOTE: the actual formatted title appears to be in some object representing the pane that gets passed to the tabview
    • file_tabs
(function() {
var AcronymResult, PathSeparator, Query, basenameScore, coreChars, countDir, doScore, emptyAcronymResult, isMatch, isSeparator, isWordEnd, isWordStart, miss_coeff, opt_char_re, pos_bonus, scoreAcronyms, scoreCharacter, scoreConsecutives, scoreExact, scoreExactMatch, scorePattern, scorePosition, scoreSize, tau_depth, tau_size, wm;
PathSeparator = require('path').sep;
wm = 150;
pos_bonus = 20;
tau_size = 50;
tau_depth = 13;
miss_coeff = 0.75;
opt_char_re = /[ _\-:\/\\]/g;
exports.coreChars = coreChars = function(query) {
return query.replace(opt_char_re, '');
};
exports.score = function(string, query, prepQuery, allowErrors) {
var score, string_lw;
if (prepQuery == null) {
prepQuery = new Query(query);
}
if (allowErrors == null) {
allowErrors = false;
}
if (!(allowErrors || isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
return 0;
}
string_lw = string.toLowerCase();
// --------- EDITS --------------------
// if we're dealing with an index file. we don't want to artificially
// bump all indexes above good matches that aren't to index files
var regexp = new RegExp("/index.(j|t)sx?$")
var isIndexFile = string.match(regexp)
if (isIndexFile) {
// we're going to score using a string without index at the end
// to artificially increase the score
var withoutIndex = string.replace(regexp, "")
// further, (only in this index case) we're scoring on just the last
// directory name. "/path/to/thing/index.tsx" only gets matched to "thing"
// TODO: this is probably too extreme but it seems to work alright so far
// without totally breaking existing useful matches
var lastPart = withoutIndex.match(/\/(?:.(?!\/))+$/g)[0]
var lastPart_lw = lastPart.toLowerCase()
score = doScore(
lastPart,
lastPart_lw,
prepQuery
);
adjustScore = Math.ceil(basenameScore(lastPart, lastPart_lw, prepQuery, score));
} else {
score = doScore(string, string_lw, prepQuery);
adjustScore = Math.ceil(basenameScore(string, string_lw, prepQuery, score));
}
console.log(adjustScore, string)
return adjustScore
// --------- EDITS --------------------
};
Query = (function() {
function Query(query) {
if (!(query != null ? query.length : void 0)) {
return null;
}
this.query = query;
this.query_lw = query.toLowerCase();
this.core = coreChars(query);
this.core_lw = this.core.toLowerCase();
this.core_up = this.core.toUpperCase();
this.depth = countDir(query, query.length);
}
return Query;
})();
exports.prepQuery = function(query) {
return new Query(query);
};
exports.isMatch = isMatch = function(subject, query_lw, query_up) {
var i, j, m, n, qj_lw, qj_up, si;
m = subject.length;
n = query_lw.length;
if (!m || !n || n > m) {
return false;
}
i = -1;
j = -1;
while (++j < n) {
qj_lw = query_lw[j];
qj_up = query_up[j];
while (++i < m) {
si = subject[i];
if (si === qj_lw || si === qj_up) {
break;
}
}
if (i === m) {
return false;
}
}
return true;
};
doScore = function(subject, subject_lw, prepQuery) {
var acro, acro_score, align, csc_diag, csc_row, csc_score, i, j, m, miss_budget, miss_left, mm, n, pos, query, query_lw, record_miss, score, score_diag, score_row, score_up, si_lw, start, sz;
query = prepQuery.query;
query_lw = prepQuery.query_lw;
m = subject.length;
n = query.length;
acro = scoreAcronyms(subject, subject_lw, query, query_lw);
acro_score = acro.score;
if (acro.count === n) {
return scoreExact(n, m, acro_score, acro.pos);
}
pos = subject_lw.indexOf(query_lw);
if (pos > -1) {
return scoreExactMatch(subject, subject_lw, query, query_lw, pos, n, m);
}
score_row = new Array(n);
csc_row = new Array(n);
sz = scoreSize(n, m);
miss_budget = Math.ceil(miss_coeff * n) + 5;
miss_left = miss_budget;
j = -1;
while (++j < n) {
score_row[j] = 0;
csc_row[j] = 0;
}
i = subject_lw.indexOf(query_lw[0]);
if (i > -1) {
i--;
}
mm = subject_lw.lastIndexOf(query_lw[n - 1], m);
if (mm > i) {
m = mm + 1;
}
while (++i < m) {
score = 0;
score_diag = 0;
csc_diag = 0;
si_lw = subject_lw[i];
record_miss = true;
j = -1;
while (++j < n) {
score_up = score_row[j];
if (score_up > score) {
score = score_up;
}
csc_score = 0;
if (query_lw[j] === si_lw) {
start = isWordStart(i, subject, subject_lw);
csc_score = csc_diag > 0 ? csc_diag : scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score);
if (align > score) {
score = align;
miss_left = miss_budget;
} else {
if (record_miss && --miss_left <= 0) {
return score_row[n - 1] * sz;
}
record_miss = false;
}
}
score_diag = score_up;
csc_diag = csc_row[j];
csc_row[j] = csc_score;
score_row[j] = score;
}
}
return score * sz;
};
exports.isWordStart = isWordStart = function(pos, subject, subject_lw) {
var curr_s, prev_s;
if (pos < 0) {
return false;
}
if (pos === 0) {
return true;
}
curr_s = subject[pos];
prev_s = subject[pos - 1];
return isSeparator(curr_s) || isSeparator(prev_s) || (curr_s !== subject_lw[pos] && prev_s === subject_lw[pos - 1]);
};
exports.isWordEnd = isWordEnd = function(pos, subject, subject_lw, len) {
var next_s;
if (pos > len - 1) {
return false;
}
if (pos === len - 1) {
return true;
}
next_s = subject[pos + 1];
return isSeparator(next_s) || (subject[pos] === subject_lw[pos] && next_s !== subject_lw[pos + 1]);
};
isSeparator = function(c) {
return c === ' ' || c === '.' || c === '-' || c === '_' || c === '/' || c === '\\';
};
scorePosition = function(pos) {
var sc;
if (pos < pos_bonus) {
sc = pos_bonus - pos;
return 100 + sc * sc;
} else {
return 100 + pos_bonus - pos;
}
};
scoreSize = function(n, m) {
return tau_size / (tau_size + Math.abs(m - n));
};
scoreExact = function(n, m, quality, pos) {
return 2 * n * (wm * quality + scorePosition(pos)) * scoreSize(n, m);
};
exports.scorePattern = scorePattern = function(count, len, sameCase, start, end) {
var bonus, sz;
sz = count;
bonus = 6;
if (sameCase === count) {
bonus += 2;
}
if (start) {
bonus += 3;
}
if (end) {
bonus += 1;
}
if (count === len) {
if (start) {
if (sameCase === len) {
sz += 2;
} else {
sz += 1;
}
}
if (end) {
bonus += 1;
}
}
return sameCase + sz * (sz + bonus);
};
exports.scoreCharacter = scoreCharacter = function(i, j, start, acro_score, csc_score) {
var posBonus;
posBonus = scorePosition(i);
if (start) {
return posBonus + wm * ((acro_score > csc_score ? acro_score : csc_score) + 10);
}
return posBonus + wm * csc_score;
};
exports.scoreConsecutives = scoreConsecutives = function(subject, subject_lw, query, query_lw, i, j, start) {
var k, m, mi, n, nj, sameCase, startPos, sz;
m = subject.length;
n = query.length;
mi = m - i;
nj = n - j;
k = mi < nj ? mi : nj;
startPos = i;
sameCase = 0;
sz = 0;
if (query[j] === subject[i]) {
sameCase++;
}
while (++sz < k && query_lw[++j] === subject_lw[++i]) {
if (query[j] === subject[i]) {
sameCase++;
}
}
if (sz === 1) {
return 1 + 2 * sameCase;
}
return scorePattern(sz, n, sameCase, start, isWordEnd(i, subject, subject_lw, m));
};
exports.scoreExactMatch = scoreExactMatch = function(subject, subject_lw, query, query_lw, pos, n, m) {
var end, i, pos2, sameCase, start;
start = isWordStart(pos, subject, subject_lw);
if (!start) {
pos2 = subject_lw.indexOf(query_lw, pos + 1);
if (pos2 > -1) {
start = isWordStart(pos2, subject, subject_lw);
if (start) {
pos = pos2;
}
}
}
i = -1;
sameCase = 0;
while (++i < n) {
if (query[pos + i] === subject[i]) {
sameCase++;
}
}
end = isWordEnd(pos + n - 1, subject, subject_lw, m);
return scoreExact(n, m, scorePattern(n, n, sameCase, start, end), pos);
};
AcronymResult = (function() {
function AcronymResult(score, pos, count) {
this.score = score;
this.pos = pos;
this.count = count;
}
return AcronymResult;
})();
emptyAcronymResult = new AcronymResult(0, 0.1, 0);
exports.scoreAcronyms = scoreAcronyms = function(subject, subject_lw, query, query_lw) {
var count, i, j, m, n, pos, qj_lw, sameCase, score;
m = subject.length;
n = query.length;
if (!(m > 1 && n > 1)) {
return emptyAcronymResult;
}
count = 0;
pos = 0;
sameCase = 0;
i = -1;
j = -1;
while (++j < n) {
qj_lw = query_lw[j];
while (++i < m) {
if (qj_lw === subject_lw[i] && isWordStart(i, subject, subject_lw)) {
if (query[j] === subject[i]) {
sameCase++;
}
pos += i;
count++;
break;
}
}
if (i === m) {
break;
}
}
if (count < 2) {
return emptyAcronymResult;
}
score = scorePattern(count, n, sameCase, true, false);
return new AcronymResult(score, pos / count, count);
};
basenameScore = function(subject, subject_lw, prepQuery, fullPathScore) {
var alpha, basePathScore, basePos, depth, end;
if (fullPathScore === 0) {
return 0;
}
end = subject.length - 1;
while (subject[end] === PathSeparator) {
end--;
}
basePos = subject.lastIndexOf(PathSeparator, end);
if (basePos === -1) {
return fullPathScore;
}
depth = prepQuery.depth;
while (depth-- > 0) {
basePos = subject.lastIndexOf(PathSeparator, basePos - 1);
if (basePos === -1) {
return fullPathScore;
}
}
basePos++;
end++;
basePathScore = doScore(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery);
alpha = 0.5 * tau_depth / (tau_depth + countDir(subject, end + 1));
return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, 0.5 * (end - basePos));
};
exports.countDir = countDir = function(path, end) {
var count, i;
if (end < 1) {
return 0;
}
count = 0;
i = -1;
while (++i < end) {
if (path[i] === PathSeparator) {
++count;
while (++i < end && path[i] === PathSeparator) {
continue;
}
}
}
return count;
};
}).call(this);
path = require 'path'
{Point, CompositeDisposable} = require 'atom'
{$, $$, SelectListView} = require 'atom-space-pen-views'
{repositoryForPath} = require './helpers'
fs = require 'fs-plus'
fuzzaldrin = require 'fuzzaldrin'
fuzzaldrinPlus = require 'fuzzaldrin-plus'
FileIcons = require './file-icons'
module.exports =
class FuzzyFinderView extends SelectListView
filePaths: null
projectRelativePaths: null
subscriptions: null
alternateScoring: false
initialize: ->
super
@addClass('fuzzy-finder')
@setMaxItems(10)
@subscriptions = new CompositeDisposable
splitLeft = => @splitOpenPath (pane) -> pane.splitLeft.bind(pane)
splitRight = => @splitOpenPath (pane) -> pane.splitRight.bind(pane)
splitUp = => @splitOpenPath (pane) -> pane.splitUp.bind(pane)
splitDown = => @splitOpenPath (pane) -> pane.splitDown.bind(pane)
atom.commands.add @element,
'pane:split-left': splitLeft
'pane:split-left-and-copy-active-item': splitLeft
'pane:split-left-and-move-active-item': splitLeft
'pane:split-right': splitRight
'pane:split-right-and-copy-active-item': splitRight
'pane:split-right-and-move-active-item': splitRight
'pane:split-up': splitUp
'pane:split-up-and-copy-active-item': splitUp
'pane:split-up-and-move-active-item': splitUp
'pane:split-down': splitDown
'pane:split-down-and-copy-active-item': splitDown
'pane:split-down-and-move-active-item': splitDown
'fuzzy-finder:invert-confirm': =>
@confirmInvertedSelection()
@alternateScoring = atom.config.get 'fuzzy-finder.useAlternateScoring'
@subscriptions.add atom.config.onDidChange 'fuzzy-finder.useAlternateScoring', ({newValue}) => @alternateScoring = newValue
getFilterKey: ->
'projectRelativePath'
cancel: ->
if atom.config.get('fuzzy-finder.preserveLastSearch')
lastSearch = @getFilterQuery()
super
@filterEditorView.setText(lastSearch)
@filterEditorView.getModel().selectAll()
else
super
destroy: ->
@cancel()
@panel?.destroy()
@subscriptions?.dispose()
@subscriptions = null
viewForItem: ({filePath, projectRelativePath}) ->
# Style matched characters in search results
filterQuery = @getFilterQuery()
if @alternateScoring
matches = fuzzaldrinPlus.match(projectRelativePath, filterQuery)
else
matches = fuzzaldrin.match(projectRelativePath, filterQuery)
$$ ->
highlighter = (path, matches, offsetIndex) =>
lastIndex = 0
matchedChars = [] # Build up a set of matched chars to be more semantic
for matchIndex in matches
matchIndex -= offsetIndex
continue if matchIndex < 0 # If marking up the basename, omit path matches
unmatched = path.substring(lastIndex, matchIndex)
if unmatched
@span matchedChars.join(''), class: 'character-match' if matchedChars.length
matchedChars = []
@text unmatched
matchedChars.push(path[matchIndex])
lastIndex = matchIndex + 1
@span matchedChars.join(''), class: 'character-match' if matchedChars.length
# Remaining characters are plain text
@text path.substring(lastIndex)
@li class: 'two-lines', =>
if (repo = repositoryForPath(filePath))?
status = repo.getCachedPathStatus(filePath)
if repo.isStatusNew(status)
@div class: 'status status-added icon icon-diff-added'
else if repo.isStatusModified(status)
@div class: 'status status-modified icon icon-diff-modified'
typeClass = FileIcons.getService().iconClassForPath(filePath) or []
unless Array.isArray typeClass
typeClass = typeClass?.toString().split(/\s+/g)
fileBasename = path.basename(filePath)
if fileBasename.match(/^index/)
# get last directory of path
dir = path.parse(filePath).dir.match(/\/(?:.(?!\/))+$/g)[0]
fileBasename = dir.slice(1)
baseOffset = projectRelativePath.length - fileBasename.length
@div class: "primary-line file icon #{typeClass.join(' ')}", 'data-name': fileBasename, 'data-path': projectRelativePath, -> highlighter(fileBasename, matches, baseOffset)
@div class: 'secondary-line path no-icon', -> highlighter(projectRelativePath, matches, 0)
openPath: (filePath, lineNumber, openOptions) ->
if filePath
atom.workspace.open(filePath, openOptions).then => @moveToLine(lineNumber)
moveToLine: (lineNumber=-1) ->
return unless lineNumber >= 0
if textEditor = atom.workspace.getActiveTextEditor()
position = new Point(lineNumber)
textEditor.scrollToBufferPosition(position, center: true)
textEditor.setCursorBufferPosition(position)
textEditor.moveToFirstCharacterOfLine()
splitOpenPath: (splitFn) ->
{filePath} = @getSelectedItem() ? {}
lineNumber = @getLineNumber()
if @isQueryALineJump() and editor = atom.workspace.getActiveTextEditor()
pane = atom.workspace.getActivePane()
splitFn(pane)(copyActiveItem: true)
@moveToLine(lineNumber)
else if not filePath
return
else if pane = atom.workspace.getActivePane()
splitFn(pane)()
@openPath(filePath, lineNumber)
else
@openPath(filePath, lineNumber)
populateList: ->
if @isQueryALineJump()
@list.empty()
@setError('Jump to line in active editor')
else if @alternateScoring
@populateAlternateList()
else
super
# Unfortunately SelectListView do not allow inheritor to handle their own filtering.
# That would be required to use external knowledge, for example: give a bonus to recent files.
#
# Or, in this case: test an alternate scoring algorithm.
#
# This is modified copy/paste from SelectListView#populateList, require jQuery!
# Should be temporary
myWeirdMatch: (filter, path) ->
regexp = new RegExp("/index.(j|t)sx?$")
# regexp = new RegExp(filterQuery + "/index.(j|t)sx?$")
return path.match(regexp)?.length
populateAlternateList: ->
return unless @items?
filterQuery = @getFilterQuery()
if filterQuery.length
filteredItems = fuzzaldrinPlus.filter(@items, filterQuery, key: @getFilterKey(), isPath: true)
else
filteredItems = @items
@list.empty()
if filteredItems.length
@setError(null)
for i in [0...Math.min(filteredItems.length, @maxItems)]
item = filteredItems[i]
itemView = $(@viewForItem(item))
itemView.data('select-list-item', item)
@list.append(itemView)
@selectItemView(@list.find('li:first'))
else
@setError(@getEmptyMessage(@items.length, filteredItems.length))
confirmSelection: ->
item = @getSelectedItem()
@confirmed(item, searchAllPanes: atom.config.get('fuzzy-finder.searchAllPanes'))
confirmInvertedSelection: ->
item = @getSelectedItem()
@confirmed(item, searchAllPanes: not atom.config.get('fuzzy-finder.searchAllPanes'))
confirmed: ({filePath}={}, openOptions) ->
if atom.workspace.getActiveTextEditor() and @isQueryALineJump()
lineNumber = @getLineNumber()
@cancel()
@moveToLine(lineNumber)
else if not filePath
@cancel()
else if fs.isDirectorySync(filePath)
@setError('Selected path is a directory')
setTimeout((=> @setError()), 2000)
else
lineNumber = @getLineNumber()
@cancel()
@openPath(filePath, lineNumber, openOptions)
isQueryALineJump: ->
query = @filterEditorView.getModel().getText()
colon = query.indexOf(':')
trimmedPath = @getFilterQuery().trim()
trimmedPath is '' and colon isnt -1
getFilterQuery: ->
query = super
colon = query.indexOf(':')
query = query[0...colon] if colon isnt -1
# Normalize to backslashes on Windows
query = query.replace(/\//g, '\\') if process.platform is 'win32'
query
getLineNumber: ->
query = @filterEditorView.getText()
colon = query.indexOf(':')
if colon is -1
-1
else
parseInt(query[colon+1..]) - 1
setItems: (filePaths) ->
super(@projectRelativePathsForFilePaths(filePaths))
projectRelativePathsForFilePaths: (filePaths) ->
# Don't regenerate project relative paths unless the file paths have changed
if filePaths isnt @filePaths
projectHasMultipleDirectories = atom.project.getDirectories().length > 1
@filePaths = filePaths
@projectRelativePaths = @filePaths.map (filePath) ->
[rootPath, projectRelativePath] = atom.project.relativizePath(filePath)
if rootPath and projectHasMultipleDirectories
projectRelativePath = path.join(path.basename(rootPath), projectRelativePath)
{filePath, projectRelativePath}
@projectRelativePaths
show: ->
@storeFocusedElement()
@panel ?= atom.workspace.addModalPanel(item: this)
@panel.show()
@focusFilterEditor()
hide: ->
@panel?.hide()
cancelled: ->
@hide()
path = require 'path'
{Disposable, CompositeDisposable} = require 'atom'
FileIcons = require './file-icons'
layout = require './layout'
module.exports =
class TabView extends HTMLElement
initialize: (@item, @pane) ->
if typeof @item.getPath is 'function'
@path = @item.getPath()
if ['TextEditor', 'TestView'].indexOf(item.constructor.name) > -1
@classList.add('texteditor')
@classList.add('tab', 'sortable')
@itemTitle = document.createElement('div')
@itemTitle.classList.add('title')
@appendChild(@itemTitle)
closeIcon = document.createElement('div')
closeIcon.classList.add('close-icon')
@appendChild(closeIcon)
@subscriptions = new CompositeDisposable()
@handleEvents()
@updateDataAttributes()
@updateTitle()
@updateIcon()
@updateModifiedStatus()
@setupTooltip()
if @isItemPending()
@itemTitle.classList.add('temp')
@classList.add('pending-tab')
@ondrag = (e) -> layout.drag e
@ondragend = (e) -> layout.end e
handleEvents: ->
titleChangedHandler = =>
@updateTitle()
# TODO: remove else condition once pending API is on stable [MKT]
if typeof @pane.onItemDidTerminatePendingState is 'function'
@subscriptions.add @pane.onItemDidTerminatePendingState (item) =>
@clearPending() if item is @item
else if typeof @item.onDidTerminatePendingState is 'function'
onDidTerminatePendingStateDisposable = @item.onDidTerminatePendingState => @clearPending()
if Disposable.isDisposable(onDidTerminatePendingStateDisposable)
@subscriptions.add(onDidTerminatePendingStateDisposable)
else
console.warn "::onDidTerminatePendingState does not return a valid Disposable!", @item
if typeof @item.onDidChangeTitle is 'function'
onDidChangeTitleDisposable = @item.onDidChangeTitle(titleChangedHandler)
if Disposable.isDisposable(onDidChangeTitleDisposable)
@subscriptions.add(onDidChangeTitleDisposable)
else
console.warn "::onDidChangeTitle does not return a valid Disposable!", @item
else if typeof @item.on is 'function'
#TODO Remove once old events are no longer supported
@item.on('title-changed', titleChangedHandler)
@subscriptions.add dispose: =>
@item.off?('title-changed', titleChangedHandler)
pathChangedHandler = (@path) =>
@updateDataAttributes()
@updateTitle()
@updateTooltip()
if typeof @item.onDidChangePath is 'function'
onDidChangePathDisposable = @item.onDidChangePath(pathChangedHandler)
if Disposable.isDisposable(onDidChangePathDisposable)
@subscriptions.add(onDidChangePathDisposable)
else
console.warn "::onDidChangePath does not return a valid Disposable!", @item
else if typeof @item.on is 'function'
#TODO Remove once old events are no longer supported
@item.on('path-changed', pathChangedHandler)
@subscriptions.add dispose: =>
@item.off?('path-changed', pathChangedHandler)
iconChangedHandler = =>
@updateIcon()
if typeof @item.onDidChangeIcon is 'function'
onDidChangeIconDisposable = @item.onDidChangeIcon? =>
@updateIcon()
if Disposable.isDisposable(onDidChangeIconDisposable)
@subscriptions.add(onDidChangeIconDisposable)
else
console.warn "::onDidChangeIcon does not return a valid Disposable!", @item
else if typeof @item.on is 'function'
#TODO Remove once old events are no longer supported
@item.on('icon-changed', iconChangedHandler)
@subscriptions.add dispose: =>
@item.off?('icon-changed', iconChangedHandler)
modifiedHandler = =>
@updateModifiedStatus()
if typeof @item.onDidChangeModified is 'function'
onDidChangeModifiedDisposable = @item.onDidChangeModified(modifiedHandler)
if Disposable.isDisposable(onDidChangeModifiedDisposable)
@subscriptions.add(onDidChangeModifiedDisposable)
else
console.warn "::onDidChangeModified does not return a valid Disposable!", @item
else if typeof @item.on is 'function'
#TODO Remove once old events are no longer supported
@item.on('modified-status-changed', modifiedHandler)
@subscriptions.add dispose: =>
@item.off?('modified-status-changed', modifiedHandler)
if typeof @item.onDidSave is 'function'
onDidSaveDisposable = @item.onDidSave (event) =>
@terminatePendingState()
if event.path isnt @path
@path = event.path
@setupVcsStatus() if atom.config.get 'tabs.enableVcsColoring'
if Disposable.isDisposable(onDidSaveDisposable)
@subscriptions.add(onDidSaveDisposable)
else
console.warn "::onDidSave does not return a valid Disposable!", @item
@subscriptions.add atom.config.observe 'tabs.showIcons', =>
@updateIconVisibility()
@subscriptions.add atom.config.observe 'tabs.enableVcsColoring', (isEnabled) =>
if isEnabled and @path? then @setupVcsStatus() else @unsetVcsStatus()
setupTooltip: ->
# Defer creating the tooltip until the tab is moused over
onMouseEnter = =>
@mouseEnterSubscription.dispose()
@hasBeenMousedOver = true
@updateTooltip()
# Trigger again so the tooltip shows
@dispatchEvent(new CustomEvent('mouseenter', bubbles: true))
@mouseEnterSubscription = dispose: =>
@removeEventListener('mouseenter', onMouseEnter)
@mouseEnterSubscription = null
@addEventListener('mouseenter', onMouseEnter)
updateTooltip: ->
return unless @hasBeenMousedOver
@destroyTooltip()
if @path
@tooltip = atom.tooltips.add this,
title: @path
html: false
delay:
show: 1000
hide: 100
placement: 'bottom'
destroyTooltip: ->
return unless @hasBeenMousedOver
@tooltip?.dispose()
destroy: ->
@subscriptions?.dispose()
@mouseEnterSubscription?.dispose()
@repoSubscriptions?.dispose()
@destroyTooltip()
@remove()
updateDataAttributes: ->
if @path
@itemTitle.dataset.name = path.basename(@path)
@itemTitle.dataset.path = @path
else
delete @itemTitle.dataset.name
delete @itemTitle.dataset.path
if itemClass = @item.constructor?.name
@dataset.type = itemClass
else
delete @dataset.type
updateTitle: ({updateSiblings, useLongTitle}={}) ->
return if @updatingTitle
@updatingTitle = true
if updateSiblings is false
title = @item.getTitle()
title = @item.getLongTitle?() ? title if useLongTitle
# !!!!
title = @item.getLongTitle?() || title
if (title.match(/\s.*\s/))
title = title.split(/\s.*\s/)[1]
console.log("title", title)
@itemTitle.textContent = title
else
title = @item.getTitle()
useLongTitle = false
for tab in @getTabs() when tab isnt this
if tab.item.getTitle() is title
tab.updateTitle(updateSiblings: false, useLongTitle: true)
useLongTitle = true
# !!!!
title = @item.getLongTitle?() || title
if (title.match(/\s.*\s/))
title = title.split(/\s.*\s/)[1]
console.log("title", title)
# title = @item.getLongTitle?() ? title if useLongTitle
@itemTitle.textContent = title
@updatingTitle = false
updateIcon: ->
if @iconName
names = unless Array.isArray(@iconName) then @iconName.split(/\s+/g) else @iconName
@itemTitle.classList.remove('icon', "icon-#{names[0]}", names...)
if @iconName = @item.getIconName?()
@itemTitle.classList.add('icon', "icon-#{@iconName}")
else if @path? and @iconName = FileIcons.getService().iconClassForPath(@path, this)
unless Array.isArray names = @iconName
names = names.toString().split /\s+/g
@itemTitle.classList.add('icon', names...)
getTabs: ->
@parentElement?.querySelectorAll('.tab') ? []
isItemPending: ->
if @pane.getPendingItem?
@pane.getPendingItem() is @item
else if @item.isPending?
@item.isPending()
terminatePendingState: ->
if @pane.clearPendingItem?
@pane.clearPendingItem() if @pane.getPendingItem() is @item
else if @item.terminatePendingState?
@item.terminatePendingState()
clearPending: ->
@itemTitle.classList.remove('temp')
@classList.remove('pending-tab')
updateIconVisibility: ->
if atom.config.get 'tabs.showIcons'
@itemTitle.classList.remove('hide-icon')
else
@itemTitle.classList.add('hide-icon')
updateModifiedStatus: ->
if @item.isModified?()
@classList.add('modified') unless @isModified
@isModified = true
else
@classList.remove('modified') if @isModified
@isModified = false
setupVcsStatus: ->
return unless @path?
@repoForPath(@path).then (repo) =>
@subscribeToRepo(repo)
@updateVcsStatus(repo)
# Subscribe to the project's repo for changes to the VCS status of the file.
subscribeToRepo: (repo) ->
return unless repo?
# Remove previous repo subscriptions.
@repoSubscriptions?.dispose()
@repoSubscriptions = new CompositeDisposable()
@repoSubscriptions.add repo.onDidChangeStatus (event) =>
@updateVcsStatus(repo, event.pathStatus) if event.path is @path
@repoSubscriptions.add repo.onDidChangeStatuses =>
@updateVcsStatus(repo)
repoForPath: ->
for dir in atom.project.getDirectories()
return atom.project.repositoryForDirectory(dir) if dir.contains @path
Promise.resolve(null)
# Update the VCS status property of this tab using the repo.
updateVcsStatus: (repo, status) ->
return unless repo?
newStatus = null
if repo.isPathIgnored(@path)
newStatus = 'ignored'
else
status = repo.getCachedPathStatus(@path) unless status?
if repo.isStatusModified(status)
newStatus = 'modified'
else if repo.isStatusNew(status)
newStatus = 'added'
if newStatus isnt @status
@status = newStatus
@updateVcsColoring()
updateVcsColoring: ->
@itemTitle.classList.remove('status-ignored', 'status-modified', 'status-added')
if @status and atom.config.get 'tabs.enableVcsColoring'
@itemTitle.classList.add("status-#{@status}")
unsetVcsStatus: ->
@repoSubscriptions?.dispose()
delete @status
@updateVcsColoring()
module.exports = document.registerElement('tabs-tab', prototype: TabView.prototype, extends: 'li')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment