diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..2f05f06 --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015"], + "plugins": ["transform-strict-mode"] +} diff --git a/.gitignore b/.gitignore index c346b13..5b79226 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -bower_components/ +build/ node_modules/ +bower_components/ diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/assets/GitHub-Mark-120px-plus.png b/assets/assets/GitHub-Mark-120px-plus.png similarity index 100% rename from assets/GitHub-Mark-120px-plus.png rename to assets/assets/GitHub-Mark-120px-plus.png diff --git a/assets/GitHub-Mark-32px.png b/assets/assets/GitHub-Mark-32px.png similarity index 100% rename from assets/GitHub-Mark-32px.png rename to assets/assets/GitHub-Mark-32px.png diff --git a/assets/GitHub-Mark-64px.png b/assets/assets/GitHub-Mark-64px.png similarity index 100% rename from assets/GitHub-Mark-64px.png rename to assets/assets/GitHub-Mark-64px.png diff --git a/chrome.js b/chrome.js deleted file mode 100644 index 581246c..0000000 --- a/chrome.js +++ /dev/null @@ -1,72 +0,0 @@ -// Clay Reimann, 2015 -// See repository LICENSE for details - -console.log('Initializing extension'); -function initalizeSuggestionService() { - chrome.storage.sync.get(['githubs'], - function(settings) { - console.log(settings); - var githubsDefs = settings.githubs; - if (githubsDefs.length < 1) { - return; - } - var suggester = new SuggestionService(githubsDefs); - - chrome.omnibox.onInputStarted.addListener( - function() { - console.log('inputStarted'); - chrome.omnibox.setDefaultSuggestion({ - description: 'Type to search for a repository' - }); - } - ); - - chrome.omnibox.onInputChanged.addListener( - function(text, suggest) { - if (text.length < 1) { return; } - var suggestions = suggester.getRepositoriesMatching(text) - .map(function(repo) { - var matches = repo.url.match(new RegExp(text, "i")); - return { - content: repo.name, - description: 'Open ' + repo.url.slice(0, matches.index) + - '' + matches[0] + '' + - repo.url.slice(matches.index + matches[0].length) + '' - }; - }); - console.log('returning ' + suggestions.length + ' suggestions'); - console.log(suggestions); - - chrome.omnibox.setDefaultSuggestion({ - description: suggestions.length + ' results for "%s"' + (suggestions.length > 5 ? '. Enter a more specific query.' : '') - }); - suggest(suggestions); - } - ); - - chrome.omnibox.onInputEntered.addListener( - function(repoName, disposition) { - console.log(repoName + ' ' + disposition); - var url = suggester.getUrlForRepo(repoName) - if (disposition === 'currentTab') { - chrome.tabs.query({active: true, currentWindow: true}, function (tabs) { - chrome.tabs.update(tabs[0].id, {url: url}); - }); - } else if (disposition === 'newForegroundTab') { - chrome.tabs.create({url: url, active: true}); - } else if (disposition === 'newBackgroundTab') { - chrome.tabs.create({url: url}); - } - - } - ); - } - ); -} - -initalizeSuggestionService(); -chrome.runtime.onMessage.addListener(function(message) { - if (message === 'settings-updated') { - initalizeSuggestionService(); - } -}) diff --git a/gulpfile.babel.js b/gulpfile.babel.js new file mode 100644 index 0000000..e1c8c91 --- /dev/null +++ b/gulpfile.babel.js @@ -0,0 +1,105 @@ +/** + * Clay Reimann, 2015 + * See LICENSE.txt for details + */ +'use strict'; + +import gulp from 'gulp'; +import webpack from 'webpack-stream'; + +import del from 'del'; +import sequence from 'run-sequence'; + +const VENDOR_ASSETS = [ + 'bower_components/bootstrap/dist/css/bootstrap.min.css', + 'bower_components/jquery/dist/jquery.min.js', + 'bower_components/bootstrap/dist/js/bootstrap.min.js' +] + +const PLATFORMS = [ + 'chrome' +]; + +gulp.task('build', PLATFORMS.map((p) => `build:${p}`)); + +PLATFORMS.forEach((platform) => { + gulp.task(`build:${platform}`, function(done) { + sequence( + `${platform}:clean`, + `${platform}:move-assets`, + `${platform}:move-vendor`, + `${platform}:compile`, + `${platform}:zip-extension`, + done); + }); +}); + +//////////////////////////// +// Build Chrome extension // +//////////////////////////// +gulp.task('chrome:clean', function(done) { + del('build/chrome/**').then(() => { + done() + }); +}); + +gulp.task('chrome:move-assets', function() { + return gulp.src([ + 'platform/chrome/manifest.json', + 'platform/chrome/options/options.html', + 'assets/**' + ]) + .pipe(gulp.dest('build/chrome')) + ; +}); + +gulp.task('chrome:move-vendor', function() { + return gulp.src(VENDOR_ASSETS) + .pipe(gulp.dest('build/chrome/vendor')) +}) + +var WEBPACK_CONFIG = { + entry: { + chrome: './platform/chrome/chrome.js', + options: './platform/chrome/options/options.js' + }, + output: { + filename: '[name].min.js' + }, + resolve: { + extensions: ['', '.js'] + }, + externals: { + chrome: 'chrome' + }, + module: { + loaders: [ + { + test: /\.js$/, + exclude: /(node_modules|bower_components)/, + loader: 'babel', + query: { + presets: ['es2015'] + } + } + ] + }, + plugins: [ + // new webpack.webpack.optimize.UglifyJsPlugin() + ], + devtool: 'source-map', + stats: { + colors: 'true' + } +}; +gulp.task('chrome:compile', function(done) { + return gulp.src(['platform/chrome/chrome.js', 'platform/chrome/options/options.html']) + .pipe(webpack(WEBPACK_CONFIG)) + .pipe(gulp.dest('build/chrome')) + ; +}); + +gulp.task('chrome:zip-extension', function(done) { + // do stuff + done(); +}) diff --git a/lib/search-service.js b/lib/search-service.js new file mode 100644 index 0000000..5a7266b --- /dev/null +++ b/lib/search-service.js @@ -0,0 +1,110 @@ +/** + * Clay Reimann, 2015 + * See LICENSE.txt for details + */ + +import Github from 'github-api'; + +class SuggestionService { + /** + * Constructs a new SuggestionService + * + * @param {array} githubDescriptors - an array of objects that describe the github instances to be searched + * the objects should be of the format + * { + * url: the github url + * auth: {oauth, basic} to describe the service type + * token: required when auth == oauth + * username: required when auth == basic + * password: required when auth == basic + * } + */ + constructor(githubDescriptors) { + this.__githubs = {}; + githubDescriptors.forEach((descriptor) => { + let opts = { + apiUrl: descriptor.url, + auth: descriptor.auth + }; + if (descriptor.auth === 'oauth') { + opts.token = descriptor.token; + } else { + opts.username = descriptor.username; + opts.password = descriptor.password; + } + this.__githubs[descriptor.url] = new Github(opts); + }); + + this.__repos = []; + } + + isLoaded() { + return this.__repos.length > 0; + } + + addRepository(githubUrl, repository) { + let isAdded = this.__repos.reduce((found, r) => { + return found || (r.githubUrl === githubUrl && r.name === repository.full_name); + }, false); + + if (!isAdded) { + this.__repos.push({ + githubUrl, + name: repository.full_name, + url: repository.html_url + }); + } + } + + addRepositories(githubUrl, list) { + if (!list) { + return; + } + + list.forEach((repository) => { + this.addRepository(githubUrl, repository); + }); + } + + queryGithubsForRepositories() { + this.__repos = []; + Object.keys(this.__githubs).forEach((githubUrl) => { + let github = this.__githubs[githubUrl]; + let user = github.getUser(); + + user.repos((err, repoList) => { + this.addRepositories(githubUrl, repoList); + }) + + user.orgs((err, orgs) => { + if (!orgs) { + return; + } + + orgs.forEach((org) => { + user.orgRepos(org.login, (err, repoList) => { + console.log(`got repos for ${org.login}`); + this.addRepositories(githubUrl, repoList); + }) + }) + }) + }) + } + + getRepositoriesMatching(text) { + return this.__repos.filter(function(repo) { + return repo.name.search(new RegExp(text, 'i')) !== -1; + }).sort(function(a, b) { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + }); + } + + getUrlForRepo(repoFullName) { + return this.__repos.reduce(function(found, repo) { + return found || (repo.name === repoFullName ? repo.url : found); + }, undefined); + } + +} + +module.exports = SuggestionService; diff --git a/package.json b/package.json index e863d20..f325bed 100644 --- a/package.json +++ b/package.json @@ -14,5 +14,18 @@ "productivity" ], "author": "Clay Reimann", - "license": "ISC" + "license": "ISC", + "devDependencies": { + "babel-core": "^6.3.15", + "babel-loader": "^6.2.0", + "babel-plugin-transform-strict-mode": "^6.3.13", + "babel-preset-es2015": "^6.3.13", + "gulp": "^3.9.0", + "run-sequence": "^1.1.5", + "webpack-stream": "^2.3.0" + }, + "dependencies": { + "del": "^2.2.0", + "github-api": "github:clayreimann/github#add-webpack" + } } diff --git a/platform/chrome/chrome.js b/platform/chrome/chrome.js new file mode 100644 index 0000000..0fee64a --- /dev/null +++ b/platform/chrome/chrome.js @@ -0,0 +1,117 @@ +/** + * Clay Reimann, 2015 + * See LICENSE.txt for details + */ +import SuggestionService from '../../lib/search-service'; + +const MORE_RESULTS = ' Append ">" to see the next page.' +let CACHE = {}; + +function initalizeSuggestionService() { + chrome.storage.sync.get(['githubs'], bindCallbacks); +} + +function parseOptions(text) { + let page = 0; + let isMeta = false; + let command = ''; + let chars = text.split(''); + + while (chars[chars.length - 1] === '>') { + page++; + chars.pop() + } + + text = chars.join(''); + + + return { + isMeta, + page, + text: chars.join('') + } +} + +function getSuggestFunction(suggester) { + return function(text, suggest) { + let page; + if (text.length < 1) { return; } + ({page, text} = parseOptions(text)); + + let suggestions; + if (CACHE.hasOwnProperty(text)) { + suggestions = CACHE[text] + } else { + suggestions = suggester.getRepositoriesMatching(text) + .map(function(repo) { + let matches = repo.url.match(new RegExp(text, "i")); + return { + content: repo.name, + description: 'Open ' + repo.url.slice(0, matches.index) + + '' + matches[0] + '' + + repo.url.slice(matches.index + matches[0].length) + '' + }; + }); + CACHE[text] = suggestions; + } + console.log('returning ' + suggestions.length + ' suggestions'); + console.log(suggestions.map((s) => s.content.length)); + console.log(suggestions.map((s) => s.content)); + + + let description = `${suggestions.length} results for query %s.` + if (suggestions.length > 5) { + description += ` Page ${page + 1} of ${Math.ceil(suggestions.length / 5)}. ${MORE_RESULTS}` + } + + for (let i = 0; i < page && suggestions.length > 5; i++) { + suggestions = suggestions.slice(5); + } + chrome.omnibox.setDefaultSuggestion({description}); + suggest(suggestions); + }; +} + +function getCompletionFunction(suggester) { + return function(repoName, disposition) { + console.log(repoName + ' ' + disposition); + let url = suggester.getUrlForRepo(repoName) + if (disposition === 'currentTab') { + chrome.tabs.query({active: true, currentWindow: true}, function (tabs) { + chrome.tabs.update(tabs[0].id, {url: url}); + }); + } else if (disposition === 'newForegroundTab') { + chrome.tabs.create({url: url, active: true}); + } else if (disposition === 'newBackgroundTab') { + chrome.tabs.create({url: url}); + } + + } +} + +function bindCallbacks(settings) { + let githubInstances = settings.githubs || []; + if (githubInstances.length < 1) { + return; + } + let suggester = new SuggestionService(githubInstances); + CACHE = {}; + suggester.queryGithubsForRepositories(); + + chrome.omnibox.onInputChanged.addListener(getSuggestFunction(suggester)); + chrome.omnibox.onInputEntered.addListener(getCompletionFunction(suggester)); +} + +initalizeSuggestionService(); +chrome.omnibox.onInputStarted.addListener(function() { + console.log('inputStarted'); + chrome.omnibox.setDefaultSuggestion({ + description: 'Type to search for a repository' + }); +}); + +chrome.runtime.onMessage.addListener(function(message) { + if (message === 'settings-updated') { + initalizeSuggestionService(); + } +}); diff --git a/manifest.json b/platform/chrome/manifest.json similarity index 76% rename from manifest.json rename to platform/chrome/manifest.json index c81dbe5..af8f785 100644 --- a/manifest.json +++ b/platform/chrome/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Github Repo Search", - "version": "1.0.0", + "version": "0.9.0", "omnibox": { "keyword" : "gh" }, "icons": { "16": "assets/GitHub-Mark-32px.png", @@ -11,7 +11,7 @@ }, "options_ui": { - "page": "options/options.html", + "page": "options.html", "chrome_style": true }, "permissions": [ @@ -21,9 +21,7 @@ "background": { "persistent": true, "scripts": [ - "node_modules/github-api/github.js", - "search.js", - "chrome.js" + "chrome.min.js" ] } } diff --git a/options/options.html b/platform/chrome/options/options.html similarity index 51% rename from options/options.html rename to platform/chrome/options/options.html index 8748ef3..3012b19 100644 --- a/options/options.html +++ b/platform/chrome/options/options.html @@ -8,7 +8,7 @@ Omnibox Github Search - +