aboutsummaryrefslogtreecommitdiff
path: root/javascript/app
diff options
context:
space:
mode:
Diffstat (limited to 'javascript/app')
-rw-r--r--javascript/app/app.js15
-rw-r--r--javascript/app/components/.gitkeep0
-rw-r--r--javascript/app/components/bottom-panel.js52
-rw-r--r--javascript/app/components/expression-info.js3
-rw-r--r--javascript/app/components/file-tree.js94
-rw-r--r--javascript/app/components/haskell-module.js492
-rw-r--r--javascript/app/components/identifier-info.js62
-rw-r--r--javascript/app/components/identifier-name.js60
-rw-r--r--javascript/app/components/infinite-list.js50
-rw-r--r--javascript/app/components/info-window.js144
-rw-r--r--javascript/app/components/input-with-autocomplete.js131
-rw-r--r--javascript/app/components/instance-info.js21
-rw-r--r--javascript/app/components/paginated-list.js48
-rw-r--r--javascript/app/components/resizable-panel.js72
-rw-r--r--javascript/app/components/text-file.js67
-rw-r--r--javascript/app/components/type-component.js29
-rw-r--r--javascript/app/components/type-signature-text.js4
-rw-r--r--javascript/app/components/type-signature.js23
-rw-r--r--javascript/app/controllers/application.js21
-rw-r--r--javascript/app/controllers/package.js24
-rw-r--r--javascript/app/controllers/package/index.js14
-rw-r--r--javascript/app/controllers/package/search.js15
-rw-r--r--javascript/app/controllers/package/show/file.js9
-rw-r--r--javascript/app/controllers/packages.js12
-rw-r--r--javascript/app/helpers/.gitkeep0
-rw-r--r--javascript/app/index.html22
-rw-r--r--javascript/app/router.js20
-rw-r--r--javascript/app/routes/application.js3
-rw-r--r--javascript/app/routes/package.js38
-rw-r--r--javascript/app/routes/package/index.js7
-rw-r--r--javascript/app/routes/package/search.js36
-rw-r--r--javascript/app/routes/package/show/file.js27
-rw-r--r--javascript/app/routes/package/show/index.js7
-rw-r--r--javascript/app/routes/packages.js16
-rw-r--r--javascript/app/services/settings.js21
-rw-r--r--javascript/app/services/store.js141
-rw-r--r--javascript/app/styles/app.scss555
-rw-r--r--javascript/app/templates/application.hbs26
-rw-r--r--javascript/app/templates/bad-url.hbs6
-rw-r--r--javascript/app/templates/components/.gitkeep0
-rw-r--r--javascript/app/templates/components/bottom-panel.hbs9
-rw-r--r--javascript/app/templates/components/expression-info.hbs8
-rw-r--r--javascript/app/templates/components/file-tree.hbs8
-rw-r--r--javascript/app/templates/components/haskell-module.hbs57
-rw-r--r--javascript/app/templates/components/identifier-info.hbs39
-rw-r--r--javascript/app/templates/components/identifier-name.hbs12
-rw-r--r--javascript/app/templates/components/infinite-list.hbs3
-rw-r--r--javascript/app/templates/components/info-window.hbs12
-rw-r--r--javascript/app/templates/components/input-with-autocomplete.hbs19
-rw-r--r--javascript/app/templates/components/instance-info.hbs10
-rw-r--r--javascript/app/templates/components/paginated-list.hbs17
-rw-r--r--javascript/app/templates/components/resizable-panel.hbs5
-rw-r--r--javascript/app/templates/components/text-file.hbs3
-rw-r--r--javascript/app/templates/components/type-component.hbs3
-rw-r--r--javascript/app/templates/components/type-signature-text.hbs2
-rw-r--r--javascript/app/templates/components/type-signature.hbs10
-rw-r--r--javascript/app/templates/package.hbs57
-rw-r--r--javascript/app/templates/package/index.hbs12
-rw-r--r--javascript/app/templates/package/search.hbs21
-rw-r--r--javascript/app/templates/package/show.hbs1
-rw-r--r--javascript/app/templates/package/show/file.hbs16
-rw-r--r--javascript/app/templates/packages.hbs26
-rw-r--r--javascript/app/utils/api-urls.js29
-rw-r--r--javascript/app/utils/color-themes.js188
-rw-r--r--javascript/app/utils/go-to-definition.js95
-rw-r--r--javascript/app/utils/line-selection.js111
66 files changed, 3160 insertions, 0 deletions
diff --git a/javascript/app/app.js b/javascript/app/app.js
new file mode 100644
index 0000000..9b24136
--- /dev/null
+++ b/javascript/app/app.js
@@ -0,0 +1,15 @@
+import Ember from 'ember';
+import Resolver from 'ember-resolver';
+import loadInitializers from 'ember-load-initializers';
+import config from './config/environment';
+
+var App;
+
+App = Ember.Application.extend({
+ modulePrefix: config.modulePrefix,
+ Resolver: Resolver
+});
+
+loadInitializers(App, config.modulePrefix);
+
+export default App;
diff --git a/javascript/app/components/.gitkeep b/javascript/app/components/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/javascript/app/components/.gitkeep
diff --git a/javascript/app/components/bottom-panel.js b/javascript/app/components/bottom-panel.js
new file mode 100644
index 0000000..5d8b5bc
--- /dev/null
+++ b/javascript/app/components/bottom-panel.js
@@ -0,0 +1,52 @@
+import Ember from 'ember';
+
+function show(component) {
+ const height = Math.floor(component.$containerElement.height() /2);
+ component.$().css({
+ "display":"block",
+ "top" : height+"px"
+ });
+ component.$topPanelElement.css({
+ "height":height+"px"
+ });
+}
+
+function hide(component) {
+ const height = Math.floor(component.$containerElement.height()/2);
+ component.$().css({
+ "display":"none",
+ "height":height+"px"
+ });
+ component.$topPanelElement.css({
+ "height":"100%"
+ });
+}
+
+export default Ember.Component.extend({
+ classNames:["bottom-panel"],
+ didInsertElement : function () {
+ this._super(...arguments);
+ this.$topPanelElement = Ember.$(this.get('topPanelElementId'));
+ this.$containerElement = Ember.$(this.get('containerElementId'));
+ Ember.run.next(this,() => {
+ Ember.$(this.element).resizable({
+ handles:"n",
+ maxHeight:700,
+ minHeight:200,
+ resize: (event,ui) => {
+ Ember.run.next(this,() => {
+ this.$topPanelElement.css({"height": this.$containerElement.height() - ui.size.height});
+ });
+ }
+ });
+ });
+ },
+ visibilityObserver : Ember.observer('visible',function () {
+ this.get('visible') ? show(this) : hide(this);
+ }),
+ actions : {
+ close () {
+ this.set('visible',false);
+ }
+ }
+});
diff --git a/javascript/app/components/expression-info.js b/javascript/app/components/expression-info.js
new file mode 100644
index 0000000..d403c49
--- /dev/null
+++ b/javascript/app/components/expression-info.js
@@ -0,0 +1,3 @@
+import Ember from 'ember';
+export default Ember.Component.extend({
+});
diff --git a/javascript/app/components/file-tree.js b/javascript/app/components/file-tree.js
new file mode 100644
index 0000000..c86a71d
--- /dev/null
+++ b/javascript/app/components/file-tree.js
@@ -0,0 +1,94 @@
+import Ember from 'ember';
+
+const directoryTreeToJsTree = function (packageId,directoryTree) {
+ return directoryTree.contents.map((node) => {
+ const jsTreeNode = {};
+ jsTreeNode.text = node.name;
+ jsTreeNode.data = node;
+ if(node.path) {
+ jsTreeNode.id = node.path;
+ jsTreeNode.a_attr = {href:"/package/" + packageId + "/show/" + node.path};
+ }
+ if(node.tag === "Dir") {
+ jsTreeNode.children = directoryTreeToJsTree(packageId,node);
+ jsTreeNode.state = {"opened" : containsHaskellModule(node)};
+ } else {
+ if(node.isHaskellModule) {
+ jsTreeNode.icon = "/assets/haskell.ico";
+ jsTreeNode.isHaskellModule = true;
+ } else {
+ jsTreeNode.icon = "jstree-file";
+ jsTreeNode.isHaskellModule = false;
+ }
+ }
+ return jsTreeNode;
+ });
+};
+
+const containsHaskellModule = function(node) {
+ return node.contents.some((n) => {
+ if(n.tag === "File") {
+ return n.isHaskellModule;
+ } else {
+ return containsHaskellModule(n);
+ }
+ });
+}
+
+export default Ember.Component.extend({
+ query: null,
+ didInsertElement : function () {
+ this._super(...arguments);
+ const element = this.element.getElementsByClassName('file-tree')[0];
+
+ const jstreeElement = Ember.$(element).jstree({
+ 'core' : {
+ 'data' : directoryTreeToJsTree(this.get('packageId'),this.get('directoryTree'))
+ },
+ "plugins" : [
+ "search"
+ ],
+ "search": {
+ "case_insensitive": true,
+ "show_only_matches" : true,
+ "show_only_matches_children": true
+ }
+ });
+
+ jstreeElement.on("select_node.jstree",(event,data) => {
+ const file = data.node.data;
+ if(file.tag != "Dir") {
+ this.sendAction('openFile',file.path);
+ }
+ });
+
+ const jstree = jstreeElement.jstree(true);
+
+ if(this.get('currentFile')) {
+ jstree.select_node(this.get('currentFile'));
+ const node = jstree.get_node(this.get('currentFile'),true)[0];
+ if(node) {
+ node.scrollIntoView();
+ }
+ }
+ this.jstree = jstree;
+ },
+ currentFileObserver : Ember.observer('currentFile',function() {
+ Ember.run.next(() => {
+ this.jstree.deselect_all();
+ this.jstree.select_node(this.get('currentFile'));
+ });
+ }),
+ queryObserver : Ember.observer('query',function() {
+ if(this.get('query')) {
+ this.jstree.search(this.get('query'));
+ } else {
+ this.jstree.clear_search();
+ }
+ }),
+ actions : {
+ hide() {
+ this.get('hide')();
+ }
+ }
+});
diff --git a/javascript/app/components/haskell-module.js b/javascript/app/components/haskell-module.js
new file mode 100644
index 0000000..8883a9c
--- /dev/null
+++ b/javascript/app/components/haskell-module.js
@@ -0,0 +1,492 @@
+import Ember from 'ember';
+import {goToDefinition} from '../utils/go-to-definition';
+import {initializeLineSelection} from '../utils/line-selection';
+
+function compareLocations (p1,p2) {
+ if(p1.line === p2.line) {
+ if(p1.column === p2.column) {
+ return 0;
+ } else if(p1.column > p2.column) {
+ return 1;
+ } else {
+ return -1;
+ }
+ } else if(p1.line > p2.line) {
+ return 1;
+ } else {
+ return -1;
+ }
+}
+
+function buildSrcSpan(sourceCodeLines,start,end) {
+ if(sourceCodeLines[start.line] && sourceCodeLines[end.line]) {
+ if(start.line === end.line) {
+ return sourceCodeLines[start.line].slice(start.column-1,end.column-1);
+ } else {
+ const firstLine = sourceCodeLines[start.line];
+ let middleLines = [];
+ for(let i = start.line + 1; i < end.line;i ++) {
+ middleLines.push(sourceCodeLines[i]);
+ }
+ const lastLine = sourceCodeLines[end.line];
+ const minOffset = Math.min(start.column,
+ (middleLines.concat([lastLine]))
+ .map((line) => line.search(/\S/))
+ .reduce((min,value) => Math.min(min,value)));
+ return firstLine.slice(start.column-1,firstLine.length) + "\n"
+ + middleLines.map((line) => line.slice(minOffset,line.length)).join("\n")
+ + (middleLines.length ? "\n" : "") + lastLine.slice(minOffset,end.column-1);
+ }
+ } else {
+ return null;
+ }
+}
+
+function modifyClass(element,on) {
+ if(on) {
+ element.classList.add('highlighted-identifier');
+ } else {
+ element.classList.remove('highlighted-identifier');
+ }
+}
+
+function highlightIdentifiers(parentElement,identifierElement,on) {
+ if(identifierElement.id) {
+ const identifiers = Array.prototype.slice.call(parentElement.querySelectorAll("span[id='"+identifierElement.id+"']"));
+ identifiers.forEach((identifier) => {
+ modifyClass(identifier,on);
+ });
+ } else {
+ modifyClass(identifierElement,on);//Literal
+ }
+}
+
+//divident is a string
+//divident may have any number of digits
+function modulo(divident, divisor) {
+ return Array.from(divident).map(c => parseInt(c))
+ .reduce((acc, value) => {
+ return (acc * 10 + value) % divisor;
+ },0);
+}
+
+function isDefinedInCurrentModule(moduleName,modulePath,identifierInfo) {
+ return (identifierInfo.sort === "External") &&
+ (identifierInfo.locationInfo.modulePath === modulePath
+ || identifierInfo.locationInfo.moduleName === moduleName)
+}
+
+function identifierStyle(identifierElement,
+ identifiers,
+ occurrences,
+ path,
+ colorTheme,
+ moduleName) {
+ const idOcc = occurrences[identifierElement.dataset.occurrence];
+
+ let color = colorTheme.defaultColor;
+ let fontWeight;
+
+ if(idOcc) {
+ if(idOcc.sort.tag === 'TypeId') {
+ color = colorTheme.typeColor;
+ } else if(idOcc.description === "HsLit" ||
+ idOcc.description === "HsOverLit"||
+ idOcc.description === "LitPat" ||
+ idOcc.description === "NPat" ||
+ idOcc.description === "NPlusKPat" ||
+ idOcc.description === "OverLit") {
+ color = colorTheme.literalColor;
+ } else {
+ const idInfo = identifiers[identifierElement.dataset.identifier];
+ if(idInfo) {
+ if(isDefinedInCurrentModule(moduleName,path,idInfo)) {
+ color = colorTheme.topLevelIdFromCurrentModule;
+ } else if(idInfo.sort === "Internal" && idInfo.locationInfo.tag === "ExactLocation") {
+ const colorNumber = modulo(identifierElement.id,colorTheme.localIdentifierColor.length);
+ color = colorTheme.localIdentifierColor[colorNumber];
+ fontWeight = "bold";
+ }
+ }
+ }
+ }
+
+ return "color:"+color+";"
+ +(fontWeight ? "font-weight:" + fontWeight : "")+";"
+ +(idOcc.isBinder ? "text-decoration:underline;" : "");
+}
+
+function initializeIdentifiers (sourceCodeContainerElement,component) {
+ const identifierElements = Array.prototype.slice.call(sourceCodeContainerElement.querySelectorAll("span.identifier"));
+ if(identifierElements.length > 0) {
+ const timeout = 250;//milliseconds
+ let timer = null;
+
+ identifierElements.forEach((identifierElement) => {
+
+ const cssText = identifierStyle(identifierElement,
+ component.get('identifiers'),
+ component.get('occurrences'),
+ component.get('path'),
+ component.get('colorTheme'),
+ component.get('name'));
+
+ identifierElement.style.cssText = cssText;
+
+ //go to definition
+ identifierElement.onmouseup = (event) => {
+ if(timer) {
+ clearTimeout(timer);
+ }
+
+ if(!window.getSelection().isCollapsed) {
+ return;
+ }
+
+ const identifierInfo = component.get('identifiers')[identifierElement.dataset.identifier];
+ const idOccurrenceInfo = component.get('occurrences')[identifierElement.dataset.occurrence];
+
+ const currentLineNumber = parseInt(identifierElement.parentNode.dataset.line);
+
+ if(idOccurrenceInfo.sort.tag === "ModuleId") {
+ goToDefinition(component.get('store'),
+ idOccurrenceInfo.sort.contents,
+ event.which,
+ currentLineNumber);
+ }
+ else {
+ if(!idOccurrenceInfo.isBinder && identifierInfo
+ && (event.which === 1 || event.which === 2)) {
+ goToDefinition(component.get('store'),
+ identifierInfo.locationInfo,
+ event.which,
+ currentLineNumber);
+ }
+ }
+ }
+
+ identifierElement.onmouseover = () => {
+ highlightIdentifiers(sourceCodeContainerElement,identifierElement,true);
+ if(timer) {
+ clearTimeout(timer);
+ }
+ timer = setTimeout(() => {
+ Ember.run.next(component,() => {
+ const identifierInfo = component.get('identifiers')[identifierElement.dataset.identifier];
+ const identifierOccurrence = component.get('occurrences')[identifierElement.dataset.occurrence];
+ console.log(identifierOccurrence);
+ console.log(identifierInfo);
+
+ component.set('selectedIdentifier',identifierElement);
+ component.set('currentLineNumber',parseInt(identifierElement.parentNode.dataset.line) || 1);
+ component.set('identifierInfo',identifierInfo);
+ component.set('identifierOccurrence',identifierOccurrence);
+ component.set('hasSelectedExpression',false);
+ component.set('isHoveredOverIdentifier',true);
+
+ });
+ },timeout);
+ };
+
+ identifierElement.onmouseout = () => {
+ highlightIdentifiers(sourceCodeContainerElement,identifierElement,false);
+
+ if(timer) {
+ clearTimeout(timer);
+ }
+
+ timer = setTimeout (() => {
+ Ember.run.next(component,() => {
+ component.set('isHoveredOverIdentifier',false);
+ });
+ },timeout);
+ };
+ });
+ component.timer = timer;
+ }
+}
+
+
+
+function contains (node, other) {
+ return node === other || !!(node.compareDocumentPosition(other) & 16);
+}
+
+function initializeExpressionInfo(sourceCodeContainerElement,component) {
+ const lineElements = Array.prototype.slice.call(sourceCodeContainerElement.querySelectorAll("td.line-content"));
+ if(lineElements.length > 0) {
+
+ //Line numbers start with 1
+ let sourceCodeLines = [""];
+
+ lineElements.forEach((el) => {
+ sourceCodeLines.push(el.textContent);
+ });
+
+ const allowedNodeNames = ["#text","SPAN","TD"];
+ let isLoading = false;
+ let shouldWait = false;
+ const timeout = 400;//milliseconds
+
+ const onmouseup = function() {
+ Ember.run.next(() => {
+ if(isLoading || shouldWait) {
+ return;
+ }
+ shouldWait = true;
+ setTimeout(() => {shouldWait = false;},timeout);
+
+ component.set('hasSelectedExpression',false);
+
+ const selection = window.getSelection();
+
+ //Selection of multiple lines inside a table doesn't work in Firefox
+ //https://bugzilla.mozilla.org/show_bug.cgi?id=365900
+
+ if(!(selection.anchorNode && selection.focusNode)
+ || !contains(sourceCodeContainerElement,selection.anchorNode)
+ || !contains(sourceCodeContainerElement,selection.focusNode)
+ || (allowedNodeNames.indexOf(selection.anchorNode.nodeName) === -1)
+ || (allowedNodeNames.indexOf(selection.focusNode.nodeName) === -1)
+ || selection.isCollapsed) {
+ return;
+ }
+
+ // Detects whether the selection is backwards
+ const detectionRange = document.createRange();
+ detectionRange.setStart(selection.anchorNode, selection.anchorOffset);
+ detectionRange.setEnd(selection.focusNode, selection.focusOffset);
+ const isBackward = detectionRange.collapsed;
+
+ let startNode,startNodeOffset,endNode,endNodeOffset;
+
+ if(isBackward) {
+ startNode = selection.focusNode;
+ startNodeOffset = selection.focusOffset;
+ endNode = selection.anchorNode;
+ endNodeOffset = selection.anchorOffset;
+ } else {
+ startNode = selection.anchorNode;
+ startNodeOffset = selection.anchorOffset;
+ endNode = selection.focusNode;
+ endNodeOffset = selection.focusOffset;
+ }
+
+ let lineStart,columnStart,lineEnd,columnEnd;
+ let infoWindowTargetElement;
+
+
+ //HTML inside source code container :
+ //<tr><td><span data-start="1" date-end="3">abc</span><span>...</span></td></tr>
+ //<tr>...</tr>
+ if(startNode.nodeName === "#text") {
+ const parent = startNode.parentNode;//<span>
+ columnStart = parseInt(parent.dataset.start) + startNodeOffset;
+ lineStart = parseInt(parent.parentNode.dataset.line);
+
+ if(startNodeOffset === startNode.textContent.length && parent.nextSibling === null) {
+ const tr = startNode.parentNode.parentNode.parentNode;// span -> td -> tr
+
+ //Skipping empty lines
+ let nextLine = tr.nextSibling;
+ while(nextLine.children[1].textContent === "") {
+ nextLine = nextLine.nextSibling;
+ }
+ infoWindowTargetElement = nextLine.children[1].children[0];
+
+ } else {
+ if(!(startNodeOffset === 0) && (parent.nextSibling)) {
+ infoWindowTargetElement = parent.nextSibling;
+ } else {
+ infoWindowTargetElement = parent;
+ }
+ }
+ } else if(startNode.nodeName === "SPAN") {
+ columnStart = 1;
+ lineStart = parseInt(startNode.parentNode.dataset.line);
+
+ const tr = startNode.parentNode.parentNode; // td -> tr
+ let nextLine = tr.nextSibling;
+ while(nextLine.children[1].textContent === "") {
+ nextLine = nextLine.nextSibling;
+ }
+ infoWindowTargetElement = nextLine.children[1].children[0];
+
+ } else if(startNode.nodeName === "TD") {
+ if(startNodeOffset > 0) {
+ const child = startNode.children[startNodeOffset-1];
+ columnStart = parseInt(child.dataset.start);
+ } else {
+ columnStart = 1;
+ }
+ lineStart = parseInt(startNode.id.slice(2));
+ infoWindowTargetElement = startNode.children[0];
+ }
+
+ if(endNode.nodeName === "#text") {
+ columnEnd = parseInt(endNode.parentNode.dataset.start) + endNodeOffset;
+ lineEnd = parseInt(endNode.parentNode.parentNode.dataset.line);
+ } else if(endNode.nodeName === "SPAN") {
+ columnEnd = 1;
+ lineEnd = parseInt(endNode.parentNode.dataset.line);
+ } else if(endNode.nodeName === "TD"){
+ if(endNodeOffset > 0) {
+ const child = endNode.children[endNodeOffset-1];
+ columnEnd = parseInt(child.dataset.start);
+ } else {
+ columnEnd = 1;
+ }
+ lineEnd = parseInt(endNode.id.slice(2));
+ }
+
+ const loadExprPromise = component.get('store').loadExpressions(
+ component.get('packageId'),
+ component.get('path'),
+ lineStart,
+ columnStart,
+ lineEnd,
+ columnEnd);
+ isLoading = true;
+
+ loadExprPromise.then((expressions) => {
+ Ember.run.next(() => {
+ if(expressions && expressions.length > 0) {
+ expressions.sort(function(expr1,expr2) {
+ if( compareLocations(expr1.srcSpan.start,expr2.srcSpan.start) <= 0
+ && compareLocations(expr1.srcSpan.end,expr2.srcSpan.end) >= 0 ) {
+ return -1;
+ } else {
+ return 1;
+ }
+ });
+
+ const expressionsWithSourceCode = expressions.reduce((result,expression) => {
+ const object = Ember.copy(expression);
+ const srcSpan = buildSrcSpan(sourceCodeLines,
+ expression.srcSpan.start,
+ expression.srcSpan.end);
+ if(srcSpan) {
+ object.sourceCode = srcSpan;
+ return result.concat(object);
+ } else {
+ return result;
+ }
+ },[]);
+
+ if(expressionsWithSourceCode.length > 0) {
+ component.set('selectedIdentifier',infoWindowTargetElement);
+ component.set('expressions',expressionsWithSourceCode);
+ component.set('currentLineNumber',parseInt(infoWindowTargetElement.parentNode.dataset.line) || 1);
+ component.set('hasSelectedExpression',true);
+
+ }
+ }
+ isLoading = false;
+ });
+ });
+ });
+ };
+
+ sourceCodeContainerElement.addEventListener('mouseup',onmouseup);
+ component._onmouseup = onmouseup;
+ }
+}
+
+export default Ember.Component.extend({
+ store : Ember.inject.service('store'),
+ selectedIdentifier : null,
+ isHoveredOverIdentifier : false,
+ hasSelectedExpression : false,
+ showDeclarations : true,
+ showDeclarationsLabel : Ember.computed('showDeclarations',function () {
+ return this.get('showDeclarations') ? "Hide" : "Show";
+ }),
+ queryObserver : Ember.observer("query",function() {
+ Ember.run.debounce(this, () => {
+ const regExp = new RegExp(this.get('query'),"i");
+ const filteredDeclarations = this.get('declarations').filter((d) => d.name.search(regExp) != -1);
+ Ember.run.next(() => {
+ this.set('filteredDeclarations',filteredDeclarations);
+ });
+ }, 300);
+ }),
+ identifierLocationInfo : Ember.computed('identifierInfo','identifierOccurrence',function() {
+ const idOcc = this.get('identifierOccurrence');
+ const idInfo = this.get('identifierInfo');
+ if(idOcc) {
+ if(idOcc.sort.tag === "ModuleId") {
+ return idOcc.sort.contents;
+ } else {
+ if(idInfo) {
+ return idInfo.locationInfo;
+ } else {
+ return null;
+ }
+ }
+ }
+ }),
+ themeObserver : Ember.observer('colorTheme',function() {
+ Ember.run.next(this,() => {
+ this.cleanup();
+ this.didInsertElement();
+ });
+ }),
+ fileObserver : Ember.observer('path',function() {
+ Ember.run.next(this,() => {
+ this.cleanup();
+ this.didInsertElement();
+ });
+ }),
+ cleanup() {
+ if(this.timer) {
+ clearTimeout(this.timer);
+ }
+ if(this._onhashchange) {
+ window.removeEventListener('hashchange',this._onhashchange);
+ }
+ if(this._onkeydown) {
+ document.removeEventListener('keydown',this._onkeydown);
+ }
+ if(this._onkeyup) {
+ document.removeEventListener('keyup',this._onkeyup);
+ }
+ if(this._onmouseup) {
+ this.sourceCodeContainerElement.removeEventListener('mouseup',this._onmouseup);
+ }
+ this.set('selectedIdentifier',null);
+ this.set('isHoveredOverIdentifier',false);
+ this.set('hasSelectedExpression',false);
+ this.set('showDeclarations',true);
+ },
+ didReceiveAttrs() {
+ this.set('filteredDeclarations',this.get('declarations'));
+ },
+ didInsertElement() {
+ this._super(...arguments);
+ const sourceCodeContainerElement = this.element.querySelector('.source-code-container');
+ sourceCodeContainerElement.innerHTML = this.get('html');
+ this.sourceCodeContainerElement = sourceCodeContainerElement;
+ this.element.parentNode.scrollTop = 0;
+ const declarations = this.element.querySelector('.declarations-content');
+ this.set('query','');
+ if(declarations) {
+ declarations.scrollTop = 0;
+ }
+ Ember.run.next(this,() => {
+ initializeIdentifiers(sourceCodeContainerElement,this);
+ initializeLineSelection(sourceCodeContainerElement,this);
+ initializeExpressionInfo(sourceCodeContainerElement,this);
+ });
+ },
+ willDestroyElement() {
+ this.cleanup();
+ },
+ actions : {
+ goToLine(lineNumber) {
+ window.location.hash = "L"+lineNumber;
+ },
+ toggleShowDeclarations() {
+ this.toggleProperty('showDeclarations');
+ }
+ }
+});
diff --git a/javascript/app/components/identifier-info.js b/javascript/app/components/identifier-info.js
new file mode 100644
index 0000000..537697a
--- /dev/null
+++ b/javascript/app/components/identifier-info.js
@@ -0,0 +1,62 @@
+import Ember from 'ember';
+import {goToDefinition} from '../utils/go-to-definition';
+
+export default Ember.Component.extend({
+ store : Ember.inject.service('store'),
+ downloadedDocumentation : null,
+ didInsertElement () {
+ const onmouseup = (event) => {
+ if(event.target.dataset.location) {
+ let location;
+ try {
+ location = JSON.parse(event.target.dataset.location);
+ } catch (e) {
+ console.log(e);
+ }
+ if(location) {
+ goToDefinition(this.get('store'),location,event.which,this.get('currentLineNumber'));
+ }
+ }
+ };
+ this.element.addEventListener('mouseup',onmouseup);
+ this._onmouseup = onmouseup;
+ },
+ willDestroyElement : function () {
+ if(this._onmouseup) {
+ this.element.removeEventListener('mouseup',this._onmouseup);
+ }
+ },
+ //Naughty record selectors :
+ //https://github.com/ghc/ghc/blob/ced2cb5e8fbf4493488d1c336da7b00d174923ce/compiler/typecheck/TcTyDecls.hs#L940-L961
+ isNaughtyRecSel : Ember.computed('identifierInfo',function () {
+ const idInfo = this.get('identifierInfo');
+ return idInfo ? (idInfo.details === "RecSelIdNaughty") : false;
+ }),
+ isExternalIdentifier : Ember.computed('identifierInfo',function () {
+ const idInfo = this.get('identifierInfo');
+ return idInfo ? (idInfo.sort === "External") : false;
+ }),
+ identifierObserver : Ember.observer('identifierInfo',function () {
+ this.set("downloadedDocumentation","");
+ const idInfo = this.get('identifierInfo');
+ if(idInfo) {
+ const locationInfo = idInfo.locationInfo;
+ if(locationInfo.tag === "ApproximateLocation") {
+ const packageId = locationInfo.packageId.name + "-" + locationInfo.packageId.version;
+ const currentIdentifier = idInfo;
+
+ this.get('store').loadDefinitionSite(packageId,
+ locationInfo.moduleName,
+ locationInfo.componentId,
+ locationInfo.entity,
+ locationInfo.name)
+ .then((definitionSite) => {
+ Ember.run.next(this,() => {
+ if(currentIdentifier === this.get('identifierInfo')) {
+ this.set('downloadedDocumentation',definitionSite.documentation);
+ }});
+ });
+ }
+ }
+ })
+});
diff --git a/javascript/app/components/identifier-name.js b/javascript/app/components/identifier-name.js
new file mode 100644
index 0000000..950b8c7
--- /dev/null
+++ b/javascript/app/components/identifier-name.js
@@ -0,0 +1,60 @@
+import Ember from 'ember';
+import {goToDefinition} from '../utils/go-to-definition';
+
+export default Ember.Component.extend({
+ store : Ember.inject.service('store'),
+ name : Ember.computed('identifierElement',function() {
+ const element = this.get('identifierElement');
+ if(element) {
+ return element.innerText;
+ }
+ }),
+ style : Ember.computed('identifierElement',function() {
+ const element = this.get('identifierElement');
+ if(element) {
+ return new Ember.String.htmlSafe("color:"+element.style.color);
+ }
+ }),
+ locationInfo : Ember.computed('identifierInfo','identifierOccurrence',function() {
+ if(this.get('identifierOccurrence.sort.tag') === "ModuleId") {
+ return this.get('identifierOccurrence.sort.contents');
+ } else {
+ return this.get('identifierInfo.locationInfo');
+ }
+ }),
+ location : Ember.computed('locationInfo',function() {
+ const loc = this.get('locationInfo');
+ if(loc) {
+ if(loc.tag === "ExactLocation") {
+ return loc.modulePath;
+ } else if(loc.tag === "ApproximateLocation") {
+ if(loc.entity === "Mod") {
+ return loc.packageId.name + "-" + loc.packageId.version;
+ } else {
+ return loc.packageId.name + "-" + loc.packageId.version + " " + loc.moduleName;
+ }
+ } else {
+ return loc.contents;
+ }
+ } else {
+ return "";
+ }
+ }),
+ isExternalIdentifier : Ember.computed('identifierInfo',function () {
+ return (this.get('identifierInfo.sort') === "External");
+ }),
+ actions : {
+ goToDefinition (event) {
+ goToDefinition(this.get('store'),
+ this.get('locationInfo'),
+ event.which,
+ this.get('currentLineNumber'));
+ return false;
+ },
+ findReferences (identifierInfo,currentPackageId) {
+ this.get('findReferences')(currentPackageId,
+ identifierInfo.externalId,
+ identifierInfo.demangledOccName);
+ }
+ }
+});
diff --git a/javascript/app/components/infinite-list.js b/javascript/app/components/infinite-list.js
new file mode 100644
index 0000000..b73b6d4
--- /dev/null
+++ b/javascript/app/components/infinite-list.js
@@ -0,0 +1,50 @@
+import Component from '@ember/component';
+import { run } from '@ember/runloop';
+import { observer } from '@ember/object';
+
+let pageNumber;
+let updating = false;
+
+function initialize(component) {
+ component.set('renderedElements',component.get('elements').slice(0,component.get('perPage')));
+ pageNumber = 1;
+}
+
+export default Component.extend({
+ renderedElements : [],
+ init() {
+ this._super(...arguments);
+ initialize(this);
+ },
+ elementsObserver : observer('elements',function() {
+ initialize(this);
+ const containerElement = document.getElementById(this.get('containerElementId'));
+ if(containerElement) {
+ containerElement.scrollTop = 0;
+ }
+ }),
+ didInsertElement() {
+ const containerElement = document.getElementById(this.get('containerElementId'));
+ if(containerElement) {
+ const component = this;
+ containerElement.onscroll = function() {
+ const perPage = component.get('perPage');
+ const elements = component.get('elements');
+
+ if(!updating &&
+ (pageNumber * perPage < elements.length) &&
+ (containerElement.scrollTop + containerElement.offsetHeight
+ > component.element.offsetHeight - 100)) {
+
+ updating = true;
+ run.next(component,() => {
+ const newElements = elements.slice(pageNumber * perPage,(pageNumber + 1) * perPage);
+ component.get('renderedElements').pushObjects(newElements);
+ pageNumber ++;
+ updating = false;
+ });
+ }
+ }
+ }
+ }
+});
diff --git a/javascript/app/components/info-window.js b/javascript/app/components/info-window.js
new file mode 100644
index 0000000..a011f99
--- /dev/null
+++ b/javascript/app/components/info-window.js
@@ -0,0 +1,144 @@
+import Ember from 'ember';
+
+let resizing = false;
+let dragging = false;
+
+function updatePosition(component) {
+ const targetElement = component.get('targetElement');
+ if(targetElement) {
+ const infoWindowHeight = component.element.offsetHeight;
+ const targetElementHeight = targetElement.offsetHeight;
+
+ const parent = targetElement.parentNode;//<td> element
+ const containerElement = document.querySelector("#" + component.get('containerElementId'));
+
+ //getBoundingClientRect() returns the smallest rectangle which contains
+ //the entire element, with read-only left, top, right, bottom, x, y, width,
+ //and height properties describing the overall border-box in pixels. Properties
+ //other than width and height are relative to the top-left of the *viewport*.
+ const targetTopViewport = targetElement.getBoundingClientRect().top;
+
+ let containerTopViewport;
+ if (containerElement) {
+ containerTopViewport = containerElement.getBoundingClientRect().top;
+ } else {
+ containerTopViewport = 0;
+ }
+
+ let infoWindowTop;
+ if(targetTopViewport < infoWindowHeight + containerTopViewport) {
+ //offsetTop is the number of pixels from the top of the closest relatively
+ //positioned parent element.
+ infoWindowTop = targetElement.offsetTop + parent.offsetTop
+ + targetElementHeight + 10 + "px";
+ } else {
+ infoWindowTop = targetElement.offsetTop + parent.offsetTop
+ - infoWindowHeight + "px";
+ }
+
+ const infoWindowLeft = targetElement.offsetLeft + parent.offsetLeft + "px";
+
+ component.$().css({
+ top:infoWindowTop,
+ left:infoWindowLeft
+ });
+ } else {
+ component.set('isPinned',false);
+ }
+}
+
+export default Ember.Component.extend({
+ classNames : ["info-window-container"],
+ attributeBindings: ['hidden'],
+ isPinned : false,
+ isFocused: false,
+ didInsertElement () {
+ const component = this;
+
+ const $headerElement = Ember.$(component.element.querySelector(".info-window-header"));
+ const $contentElement = Ember.$(component.element.querySelector(".info-window-content"));
+ const $infoWindowElement = Ember.$(component.element.querySelector(".info-window"));
+ const $infoWindowContainerElement = Ember.$(component.element);
+
+ this.$headerElement = $headerElement;
+ this.$contentElement = $contentElement;
+
+ this.$().resizable({
+ handles: "n,w",
+ minHeight: 80,
+ minWidth: 400,
+ start: function() {
+ resizing = true;
+ },
+ stop: function() {
+ resizing = false;
+ },
+ resize : function() {
+ const containerHeight = $infoWindowContainerElement.height();
+ $infoWindowElement.css({
+ "height": containerHeight + 2 + "px"
+ });
+ $contentElement.css({
+ "max-height":(containerHeight - $headerElement.outerHeight(true)) + "px"
+ });
+ }
+ });
+ this.$().draggable({
+ containment:"#" + this.get('containerElementId'),
+ handle: $headerElement,
+ start: function() {
+ dragging = true;
+ },
+ stop: function() {
+ dragging = false;
+ }
+ });
+ },
+ mouseEnter () {
+ if(!this.get('hasSelectedExpression')) {
+ this.set('isFocused',true);
+ }
+ },
+ mouseLeave (event) {
+ //Workaround for a bug in Chrome
+ const element = document.elementFromPoint(event.clientX,event.clientY);
+ if(element && element.classList.contains('link')) {
+ return;
+ }
+ if(!resizing
+ && !dragging
+ && !this.get('isPinned')
+ && !this.get('hasSelectedExpression')) {
+ this.set('isFocused',false);
+ }
+ },
+ hidden : Ember.computed('isHoveredOverIdentifier',
+ 'isFocused',
+ 'hasSelectedExpression',
+ 'isPinned', function() {
+ if (this.$contentElement) {
+ this.$contentElement.scrollTop(0);
+ }
+ if (this.get('isPinned')
+ || this.get('isFocused')
+ || this.get('isHoveredOverIdentifier')
+ || this.get('hasSelectedExpression')) {
+ return false;
+ } else {
+ return true;
+ }
+ }),
+ didUpdate() {
+ updatePosition(this);
+ },
+ actions : {
+ close() {
+ this.set('isPinned',false);
+ this.set('isFocused',false);
+ this.set('hasSelectedExpression',false);
+ },
+ pin() {
+ this.toggleProperty('isPinned');
+ }
+ }
+});
diff --git a/javascript/app/components/input-with-autocomplete.js b/javascript/app/components/input-with-autocomplete.js
new file mode 100644
index 0000000..2b09ea4
--- /dev/null
+++ b/javascript/app/components/input-with-autocomplete.js
@@ -0,0 +1,131 @@
+import Ember from 'ember';
+export default Ember.Component.extend({
+ store : Ember.inject.service('store'),
+ highlightedItemIndex: -1,
+ items : [],
+ query: null,
+ didInsertElement() {
+ const $input = Ember.$(this.element).find(".search-input");
+ const $autocompleteContainer = Ember.$(this.element).find(".autocomplete-container");
+ this.$input = $input;
+ this.$autocompleteContainer = $autocompleteContainer;
+ const width = $input.width() + 300;
+ $autocompleteContainer.css({
+ "width" : width+"px",
+ "top" : $input.outerHeight()
+ });
+ $input.keyup((e) => {
+ if(e.which === 13) {
+ this.onEnter();
+ } else if(e.which === 27) {
+ this.onEsc();
+ } else if(e.which === 40) {
+ this.onDown();
+ } else if(e.which === 38) {
+ this.onUp();
+ }
+ });
+ $input.focusin(() => {
+ this.showAutocompleteList();
+ });
+ $input.focusout(() => {
+ //Timeout is needed to make sure that click event fires
+ Ember.run.later((() => {
+ this.hideAutocompleteList();
+ }), 100);
+ });
+ },
+ willDestroyElement() {
+ this._super(...arguments);
+ this.$input.off('keyup');
+ this.$input.off('focusin');
+ this.$input.off('focusout');
+ },
+ onEnter() {
+ if(this.get('highlightedItemIndex') !== -1) {
+ const item = this.get('items')[this.get('highlightedItemIndex')];
+ if(item) {
+ this.hideAutocompleteList();
+ this.get('selectItem')(item);
+ }
+ } else {
+ this.hideAutocompleteList();
+ this.get('onSubmit')(this.get('query'));
+ }
+ },
+ onEsc() {
+ this.hideAutocompleteList();
+ },
+ onDown() {
+ this.showAutocompleteList();
+ const index = this.get('highlightedItemIndex');
+ const items = this.get('items');
+ const itemsCount = items.length;
+ if(itemsCount > 0) {
+ if(index !== -1) {
+ if(index === itemsCount - 1) {
+ this.set('highlightedItemIndex',0);
+ } else {
+ this.set('highlightedItemIndex',index+1);
+ }
+ } else {
+ this.set('highlightedItemIndex',0);
+ }
+ }
+ },
+ onUp() {
+ this.showAutocompleteList();
+ const index = this.get('highlightedItemIndex');
+ const items = this.get('items');
+ const itemsCount = items.length;
+ if(itemsCount > 0) {
+ if(index !== -1) {
+ if(index === 0) {
+ this.set('highlightedItemIndex',itemsCount - 1);
+ } else {
+ this.set('highlightedItemIndex',index - 1);
+ }
+ } else {
+ this.set('highlightedItemIndex',itemsCount - 1);
+ }
+ }
+ },
+ hideAutocompleteList() {
+ this.set('highlightedItemIndex',-1);
+ this.$autocompleteContainer.css({
+ "display":"none",
+ });
+ },
+ showAutocompleteList() {
+ this.$autocompleteContainer.css({
+ "display":"block"
+ });
+ },
+ queryObserver : Ember.observer("query",function() {
+ if(this.get('query')) {
+ const perPage = this.get('maxItems') ? this.get('maxItems') : 10;
+ const url = this.get('createSearchUrlFunction')(this.get('query')) + "?per_page=" + perPage;
+ Ember.run.debounce(this, () => {
+ this.get('store').loadFromUrlPaginated(url).then((result) => {
+ Ember.run.next(() => {
+ this.set('items',result.items);
+ });
+ });
+ }, 400);
+ this.showAutocompleteList();
+ } else {
+ this.hideAutocompleteList();
+ this.set('items',[]);
+ }
+ }),
+ actions : {
+ onSubmit() {
+ this.hideAutocompleteList();
+ this.get('onSubmit')(this.get('query'));
+ },
+ goToDefinition (item) {
+ this.hideAutocompleteList();
+ this.get('selectItem')(item);
+ }
+ }
+});
diff --git a/javascript/app/components/instance-info.js b/javascript/app/components/instance-info.js
new file mode 100644
index 0000000..a0e04ed
--- /dev/null
+++ b/javascript/app/components/instance-info.js
@@ -0,0 +1,21 @@
+import Ember from 'ember';
+import {goToDefinition} from '../utils/go-to-definition';
+
+export default Ember.Component.extend({
+ store : Ember.inject.service('store'),
+ style : Ember.computed('nestedLevel',function() {
+ return new Ember.String.htmlSafe("margin-left :" + this.get('nestedLevel') * 10 + "px");
+ }),
+ nextNestedLevel : Ember.computed('nestedLevel',function () {
+ return this.get('nestedLevel') + 1;
+ }),
+ actions : {
+ goToDefinition (event) {
+ goToDefinition(this.get('store'),
+ this.get('instance.location'),
+ event.which,
+ this.get('currentLineNumber'));
+ return false;
+ }
+ }
+});
diff --git a/javascript/app/components/paginated-list.js b/javascript/app/components/paginated-list.js
new file mode 100644
index 0000000..9d85610
--- /dev/null
+++ b/javascript/app/components/paginated-list.js
@@ -0,0 +1,48 @@
+import Ember from 'ember';
+function loadItems(store,component,url) {
+ store.loadFromUrlPaginated(url).then((result) => {
+ Ember.run.next(() => {
+ component.set('total',result.total);
+ component.set('items',result.items);
+ component.set('first',result.linkHeader.first);
+ component.set('next',result.linkHeader.next);
+ component.set('prev',result.linkHeader.prev);
+ component.set('last',result.linkHeader.last);
+
+ const pageMatch = url.match(/(&|\?)page=(\d+)/);
+ const perPageMatch = url.match(/(&|\?)per_page=(\d+)/);
+
+ const page = pageMatch ? pageMatch[2] : 1;
+ const perPage = perPageMatch ? perPageMatch[2] : 20;
+
+ if(result.linkHeader.next || result.linkHeader.prev) {
+ component.set('firstItemOnPage',(page - 1) * perPage + 1);
+ if(!result.linkHeader.last) {
+ component.set('lastItemOnPage',result.total);
+ } else {
+ component.set('lastItemOnPage',page * perPage);
+ }
+ }
+ });
+ });
+}
+
+export default Ember.Component.extend({
+ store : Ember.inject.service('store'),
+ init() {
+ this._super(...arguments);
+ if(this.get('url')) {
+ loadItems(this.get('store'),this,this.get('url'));
+ }
+ },
+ urlObserver : Ember.observer('url',function () {
+ loadItems(this.get('store'),this,this.get('url'));
+ this.element.querySelector(".paginated-list-content").scrollTop = 0;
+ }),
+ actions : {
+ update(url) {
+ this.element.querySelector(".paginated-list-content").scrollTop = 0;
+ loadItems(this.get('store'),this,url);
+ }
+ }
+});
diff --git a/javascript/app/components/resizable-panel.js b/javascript/app/components/resizable-panel.js
new file mode 100644
index 0000000..8dae7ed
--- /dev/null
+++ b/javascript/app/components/resizable-panel.js
@@ -0,0 +1,72 @@
+import Ember from 'ember';
+
+function hide (component,byUser) {
+ component.$alsoResizeElement.css({left: 0});
+ component.$().css({width:0});
+ component.set('hidden',true);
+ component.$(".show-left-panel-button").show();
+ if(byUser) {
+ component.set('hiddenByUser',true);
+ }
+}
+
+function show (component,byUser) {
+ component.$alsoResizeElement.css({left: 300});
+ component.$().css({width:300});
+ component.set('hidden',false);
+ component.$(".show-left-panel-button").hide();
+ if(byUser) {
+ component.set('hiddenByUser',false);
+ }
+}
+
+export default Ember.Component.extend({
+ hidden:false,
+ hiddenByUser:false,
+ didInsertElement : function () {
+ this._super(...arguments);
+ Ember.run.next(this,() => {
+ const onresize = () => {
+ if(!this.get('hiddenByUser')) {
+ const width = window.innerWidth;
+ if(!this.get('hidden') && width < 700) {
+ hide(this,false);
+ } else if(this.get('hidden') && width > 700) {
+ show(this,false);
+ }
+ }
+ };
+ this._onresize = onresize;
+ window.addEventListener('resize', onresize);
+ const $alsoResizeElement = Ember.$(this.get('alsoResizeElementId'));
+ Ember.$(this.element).resizable({
+ maxWidth: 800,
+ minWidth: 200,
+ handles: 'e',
+ resize: (event,ui) => {
+ Ember.run.next(this,() => {
+ $alsoResizeElement.css({left: ui.size.width});
+ });
+ }
+ });
+ this.$alsoResizeElement = $alsoResizeElement;
+ });
+ },
+ hideButtonLabel : Ember.computed('hidden',function() {
+ return this.get('hidden') ? "&gt;" : "&lt;";
+ }),
+ willDestroyElement() {
+ if(this._onresize) {
+ window.removeEventListener('resize',this._onresize);
+ }
+ },
+ actions : {
+ hide() {
+ if(this.get('hidden')) {
+ show(this,true);
+ } else {
+ hide(this,true);
+ }
+ }
+ }
+});
diff --git a/javascript/app/components/text-file.js b/javascript/app/components/text-file.js
new file mode 100644
index 0000000..05be31a
--- /dev/null
+++ b/javascript/app/components/text-file.js
@@ -0,0 +1,67 @@
+/* global showdown */
+import Ember from 'ember';
+import {initializeLineSelection} from '../utils/line-selection';
+
+function escapeHtml(text) {
+ return text.replace(/[\"&<>]/g, function (a) {
+ return { '"': '&quot;', '&': '&amp;', '<': '&lt;', '>': '&gt;' }[a];
+ });
+}
+
+function addLineNumbers (text) {
+ const start = "<table class='source-code'><tbody>";
+ const end = "</tbody></table>";
+ let lineNumber = 0;
+ const lines = text.split("\n").map((line) => {
+ lineNumber ++;
+ const lineNumberHtml = "<td id='LN"+lineNumber+"' class='line-number'>"+lineNumber+"</td>";
+ const lineContentHtml = "<td id='LC"+lineNumber+"' class='line-content'>"+escapeHtml(line)+"</td>";
+ return "<tr>"+ lineNumberHtml + lineContentHtml + "</tr>";
+ }).join("");
+ return start + lines + end;
+}
+
+const markdownExtensions = ["markdown", "mdown", "mkdn", "mkd", "md"];
+
+export default Ember.Component.extend({
+ isMarkdown : Ember.computed('path',function() {
+ const maybeExtension = this.get('path').split('.').pop();
+ return markdownExtensions.any((extension) => (maybeExtension === extension));
+ }),
+ html : Ember.computed('path','isMarkdown',function() {
+ if(this.get('isMarkdown')) {
+ return this.markdownConverter.makeHtml(this.get('text'));
+ } else {
+ return addLineNumbers(this.get('text'));
+ }
+ }),
+ init() {
+ this._super(...arguments);
+ this.markdownConverter = new showdown.Converter();
+ },
+ didInsertElement() {
+ const sourceCodeContainerElement = this.element.querySelector('.source-code-container');
+ initializeLineSelection(sourceCodeContainerElement,this);
+ this.element.parentNode.scrollTop = 0;
+ },
+ willDestroyElement : function () {
+ this.cleanup();
+ },
+ cleanup() {
+ if(this._onhashchange) {
+ window.removeEventListener('hashchange',this._onhashchange);
+ }
+ if(this._onkeydown) {
+ document.removeEventListener('keydown',this._onkeydown);
+ }
+ if(this._onkeyup) {
+ document.removeEventListener('keyup',this._onkeyup);
+ }
+ },
+ pathObserver : Ember.observer('path',function() {
+ Ember.run.next(this,() => {
+ this.cleanup();
+ this.didInsertElement();
+ });
+ })
+});
diff --git a/javascript/app/components/type-component.js b/javascript/app/components/type-component.js
new file mode 100644
index 0000000..c19facc
--- /dev/null
+++ b/javascript/app/components/type-component.js
@@ -0,0 +1,29 @@
+import Ember from 'ember';
+import {goToDefinition} from '../utils/go-to-definition';
+
+export default Ember.Component.extend({
+ store : Ember.inject.service('store'),
+ tagName : 'span',
+ classNames: ["type-component"],
+ contextMenu() {//right mouse button click to show kind of a type constructor or type variable
+ if(this.get('identifiers') && this.get('internalId')) {
+ this.set('expanded',true);
+ }
+ return false;
+ },
+ linkClass : Ember.computed('identifierInfo',function() {
+ return this.get('identifierInfo') ? "link" : "";
+ }),
+ identifierInfo : Ember.computed('internalId',function() {
+ return this.get('internalId') ? this.get('identifiers')[this.get('internalId')] : null;
+ }),
+ actions : {
+ onmouseup (event) {
+ if(this.get('identifierInfo') && (event.which !== 3 )) {
+ const locationInfo = this.get('identifierInfo').locationInfo;
+ goToDefinition(this.get('store'),locationInfo,event.which,this.get('currentLineNumber'));
+ return false;
+ }
+ }
+ }
+});
diff --git a/javascript/app/components/type-signature-text.js b/javascript/app/components/type-signature-text.js
new file mode 100644
index 0000000..e4eb200
--- /dev/null
+++ b/javascript/app/components/type-signature-text.js
@@ -0,0 +1,4 @@
+import Ember from 'ember';
+export default Ember.Component.extend({
+ tagName : "span"
+});
diff --git a/javascript/app/components/type-signature.js b/javascript/app/components/type-signature.js
new file mode 100644
index 0000000..ed1849c
--- /dev/null
+++ b/javascript/app/components/type-signature.js
@@ -0,0 +1,23 @@
+import Ember from 'ember';
+export default Ember.Component.extend({
+ tagName : "span",
+ expandTypeSynonyms: false,
+ expandTypeSynonymsLabel : Ember.computed('expandTypeSynonyms',function() {
+ return this.get('expandTypeSynonyms') ? "Show type synonyms" : "Expand type synonyms";
+ }),
+ components : Ember.computed('type','expandTypeSynonyms',function() {
+ if(this.get('expandTypeSynonyms') && this.get('type.componentsExpanded')) {
+ return this.get('type.componentsExpanded');
+ } else {
+ return this.get('type.components');
+ }
+ }),
+ typeObserver : Ember.observer('type',function() {
+ this.set('expandTypeSynonyms',false);
+ }),
+ actions : {
+ toggleExpandTypeSynonyms () {
+ this.toggleProperty('expandTypeSynonyms');
+ }
+ }
+});
diff --git a/javascript/app/controllers/application.js b/javascript/app/controllers/application.js
new file mode 100644
index 0000000..2936011
--- /dev/null
+++ b/javascript/app/controllers/application.js
@@ -0,0 +1,21 @@
+import {updateColorThemeCss,themes} from '../utils/color-themes';
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ settings : Ember.inject.service('settings'),
+ themes: Object.values(themes),
+ init() {
+ this._super(...arguments);
+ updateColorThemeCss(this.get('settings').get('colorTheme'));
+ },
+ currentTheme: Ember.computed('settings',function() {
+ return this.get('settings.colorTheme.id');
+ }),
+ actions : {
+ themeChanged (themeId) {
+ const theme = themes[themeId];
+ this.get('settings').set('colorTheme',theme);
+ updateColorThemeCss(theme);
+ }
+ }
+});
diff --git a/javascript/app/controllers/package.js b/javascript/app/controllers/package.js
new file mode 100644
index 0000000..45a8bab
--- /dev/null
+++ b/javascript/app/controllers/package.js
@@ -0,0 +1,24 @@
+import Ember from 'ember';
+import {goToDefinition} from '../utils/go-to-definition';
+export default Ember.Controller.extend({
+ store : Ember.inject.service('store'),
+ currentFile : null,
+ loadItemsFunction : null,
+ query : null,
+ actions : {
+ searchIdentifier (query) {
+ if(query) {
+ this.set('currentFile',null);
+ document.title = this.get('model.id');
+ this.transitionToRoute('package.search',query);
+ }
+ },
+ showIdentifier (identifierInfo) {
+ goToDefinition(this.get('store'),
+ identifierInfo.locationInfo,
+ 1,//left mouse button
+ null);
+ return false;
+ }
+ }
+});
diff --git a/javascript/app/controllers/package/index.js b/javascript/app/controllers/package/index.js
new file mode 100644
index 0000000..cd02416
--- /dev/null
+++ b/javascript/app/controllers/package/index.js
@@ -0,0 +1,14 @@
+import Ember from 'ember';
+export default Ember.Controller.extend({
+ modulesFiltered : Ember.computed('model','query',function () {
+ const query = this.get('query');
+ const modules = Object.keys(this.get('model.modules')).sort();
+ if(query) {
+ const regExp = new RegExp(query,"i");
+ return modules.filter((p) => p.search(regExp) != -1);
+ } else {
+ return modules;
+ }
+ })
+});
+
diff --git a/javascript/app/controllers/package/search.js b/javascript/app/controllers/package/search.js
new file mode 100644
index 0000000..46f2efd
--- /dev/null
+++ b/javascript/app/controllers/package/search.js
@@ -0,0 +1,15 @@
+import Ember from 'ember';
+import {goToDefinition} from '../../utils/go-to-definition';
+
+export default Ember.Controller.extend({
+ store : Ember.inject.service('store'),
+ actions : {
+ goToDefinition (locationInfo,event) {
+ goToDefinition(this.get('store'),
+ locationInfo,
+ event.which,
+ null);
+ return false;
+ }
+ }
+});
diff --git a/javascript/app/controllers/package/show/file.js b/javascript/app/controllers/package/show/file.js
new file mode 100644
index 0000000..c566e7e
--- /dev/null
+++ b/javascript/app/controllers/package/show/file.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+export default Ember.Controller.extend({
+ settings : Ember.inject.service('settings'),
+ actions : {
+ findReferences(packageId,externalId,occName) {
+ this.send('updateReferences',packageId,externalId,occName);
+ }
+ }
+});
diff --git a/javascript/app/controllers/packages.js b/javascript/app/controllers/packages.js
new file mode 100644
index 0000000..a1724dd
--- /dev/null
+++ b/javascript/app/controllers/packages.js
@@ -0,0 +1,12 @@
+import Ember from 'ember';
+export default Ember.Controller.extend({
+ queryObserver : Ember.observer("query",function() {
+ Ember.run.debounce(this, () => {
+ const regExp = new RegExp(this.get('query'),"i");
+ const packages = this.get('model').filter((p) => p.name.search(regExp) != -1);
+ Ember.run.next(() => {
+ this.set('packages',packages);
+ });
+ }, 300);
+ })
+});
diff --git a/javascript/app/helpers/.gitkeep b/javascript/app/helpers/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/javascript/app/helpers/.gitkeep
diff --git a/javascript/app/index.html b/javascript/app/index.html
new file mode 100644
index 0000000..0c8f882
--- /dev/null
+++ b/javascript/app/index.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <title>Haskell code explorer</title>
+ <meta name="description" content="Haskell code explorer">
+ <meta name="google" content="notranslate">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ {{content-for 'head'}}
+ <link rel="stylesheet" href="/assets/vendor.css">
+ <link rel="stylesheet" href="/assets/haskell-code-explorer.css">
+ {{content-for 'head-footer'}}
+ </head>
+ <body>
+ {{content-for 'body'}}
+ <script pace-src="/assets/vendor.js"></script>
+ <script pace-src="/assets/haskell-code-explorer.js"></script>
+ {{content-for 'body-footer'}}
+ <noscript>You need to enable JavaScript to run this app.</noscript>
+ </body>
+</html>
diff --git a/javascript/app/router.js b/javascript/app/router.js
new file mode 100644
index 0000000..f58e9ab
--- /dev/null
+++ b/javascript/app/router.js
@@ -0,0 +1,20 @@
+import Ember from 'ember';
+import config from './config/environment';
+
+var Router = Ember.Router.extend({
+ location: config.locationType
+});
+
+Router.map(function() {
+ this.route('packages',{path:''});
+ this.route('package', {path:'/package/:packageId'}, function() {
+ this.route('show',function() {
+ this.route('file', {path:'*filePath'}, function() {
+ });
+ });
+ this.route('search',{path:'/search/:query'});
+ });
+ this.route('bad-url', { path: '/*badurl' });
+});
+
+export default Router;
diff --git a/javascript/app/routes/application.js b/javascript/app/routes/application.js
new file mode 100644
index 0000000..279bd32
--- /dev/null
+++ b/javascript/app/routes/application.js
@@ -0,0 +1,3 @@
+import Ember from 'ember';
+export default Ember.Route.extend({
+});
diff --git a/javascript/app/routes/package.js b/javascript/app/routes/package.js
new file mode 100644
index 0000000..92fea03
--- /dev/null
+++ b/javascript/app/routes/package.js
@@ -0,0 +1,38 @@
+import Ember from 'ember';
+import {urls} from '../utils/api-urls';
+
+export default Ember.Route.extend({
+ store : Ember.inject.service('store'),
+ model (params) {
+ return this.get('store').loadPackage(params.packageId)
+ .catch((e) => {console.log(e);this.transitionTo("/package-not-found");});
+ },
+ afterModel(model) {
+ document.title = model.id;
+ },
+ setupController(controller, model) {
+ this._super(controller, model);
+ const packageId = this.modelFor('package').id;
+ controller.set('bottomPanelVisible',false);
+ controller.set('createSearchUrlFunction',(query) => {
+ return urls.identifierSearchUrl(packageId,query);
+ });
+ },
+ actions : {
+ openFile (filePath) {
+ this.transitionTo('package.show.file',filePath);
+ },
+ fileOpened (filePath) {
+ if(this.get('controller')) {
+ this.set('controller.currentFile',filePath);
+ }
+ },
+ updateReferences(packageId,externalId,occName) {
+ this.set('controller.packageId',packageId);
+ this.set('controller.externalId',externalId);
+ this.set('controller.occName',occName);
+ this.set('controller.bottomPanelVisible',true);
+ this.set('controller.referencesUrl',urls.referencesUrl(packageId,externalId)+"?per_page=50");
+ }
+ }
+});
diff --git a/javascript/app/routes/package/index.js b/javascript/app/routes/package/index.js
new file mode 100644
index 0000000..e36abd5
--- /dev/null
+++ b/javascript/app/routes/package/index.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ afterModel : function (model,transition) {
+ transition.send("fileOpened",null);
+ }
+});
diff --git a/javascript/app/routes/package/search.js b/javascript/app/routes/package/search.js
new file mode 100644
index 0000000..7bc711b
--- /dev/null
+++ b/javascript/app/routes/package/search.js
@@ -0,0 +1,36 @@
+import Ember from 'ember';
+import {urls} from '../../utils/api-urls';
+import {goToDefinition} from '../../utils/go-to-definition';
+
+export default Ember.Route.extend({
+ store : Ember.inject.service('store'),
+ model (params) {
+ return {
+ query: params.query,
+ url: urls.identifierSearchUrl(this.modelFor('package').id,params.query)+"?per_page=20"
+ };
+ },
+ afterModel () {
+ const onmouseup = (event) => {
+ // This makes links in documentation clickable
+ if(event.target.dataset.location) {
+ let location;
+ try {
+ location = JSON.parse(event.target.dataset.location);
+ } catch (e) {
+ console.log(e);
+ }
+ if(location) {
+ goToDefinition(this.get('store'),location,event.which);
+ }
+ }
+ };
+ this._onmouseup = onmouseup;
+ document.addEventListener('mouseup',onmouseup);
+ },
+ deactivate() {
+ if(this._onmouseup) {
+ document.removeEventListener('mouseup',this._onmouseup);
+ }
+ }
+});
diff --git a/javascript/app/routes/package/show/file.js b/javascript/app/routes/package/show/file.js
new file mode 100644
index 0000000..ead6ee8
--- /dev/null
+++ b/javascript/app/routes/package/show/file.js
@@ -0,0 +1,27 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ store : Ember.inject.service(),
+ model : function (params) {
+ const packageInfo = this.modelFor('package');
+ if(packageInfo.modules[params.filePath]) {
+ return this.get('store').loadHaskellModule(packageInfo.id,params.filePath)
+ .catch((e) => {console.log(e);this.transitionTo("/not-found");});
+ } else {
+ return this.get('store').loadFile(packageInfo.id,params.filePath)
+ .then((result) => {
+ document.title = packageInfo.id;
+ return result;
+ })
+ .catch((e) => {console.log(e);this.transitionTo("/not-found");});
+ }
+ },
+ afterModel (model) {
+ document.title = model.id + " - " + this.modelFor('package').id;
+ },
+ actions : {
+ didTransition : function () {
+ this.send("fileOpened",this.currentModel.id);
+ }
+ }
+});
diff --git a/javascript/app/routes/package/show/index.js b/javascript/app/routes/package/show/index.js
new file mode 100644
index 0000000..e36abd5
--- /dev/null
+++ b/javascript/app/routes/package/show/index.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ afterModel : function (model,transition) {
+ transition.send("fileOpened",null);
+ }
+});
diff --git a/javascript/app/routes/packages.js b/javascript/app/routes/packages.js
new file mode 100644
index 0000000..2bbf9bc
--- /dev/null
+++ b/javascript/app/routes/packages.js
@@ -0,0 +1,16 @@
+import Ember from 'ember';
+import {urls} from '../utils/api-urls';
+import config from '../config/environment';
+
+export default Ember.Route.extend({
+ model () {
+ return Ember.$.getJSON(urls.packagesUrl);
+ },
+ setupController(controller, model) {
+ this._super(controller, model);
+ controller.set('packages',model);
+ },
+ afterModel () {
+ document.title = config.APP.title;
+ }
+});
diff --git a/javascript/app/services/settings.js b/javascript/app/services/settings.js
new file mode 100644
index 0000000..a79779a
--- /dev/null
+++ b/javascript/app/services/settings.js
@@ -0,0 +1,21 @@
+import Ember from 'ember';
+import {themes} from '../utils/color-themes';
+
+export default Ember.Service.extend({
+ init() {
+ this._super(...arguments);
+ if(localStorage) {
+ const colorThemeId = localStorage.getItem("colorThemeId");
+ const colorTheme = themes[colorThemeId];
+ if(colorThemeId) {
+ this.set('colorTheme',colorTheme);
+ }
+ }
+ },
+ colorTheme : themes["darkTheme"],
+ settingsObserver : Ember.observer("colorTheme",function() {
+ if(localStorage) {
+ localStorage.setItem("colorThemeId",this.get('colorTheme').id);
+ }
+ })
+});
diff --git a/javascript/app/services/store.js b/javascript/app/services/store.js
new file mode 100644
index 0000000..cf48f9d
--- /dev/null
+++ b/javascript/app/services/store.js
@@ -0,0 +1,141 @@
+import Ember from 'ember';
+import RSVP from 'rsvp';
+import {urls} from '../utils/api-urls';
+
+
+//********************************************************************************
+//https://coderwall.com/p/zrlulq/parsing-a-link-header-in-javascript
+function unquote(value) {
+ if (value.charAt(0) == '"' && value.charAt(value.length - 1) == '"') {
+ return value.substring(1, value.length - 1);
+ } else {
+ return value;
+ }
+}
+
+function parseLinkHeader(header) {
+ if(!header) {return {}}
+ var linkexp = /<[^>]*>\s*(\s*;\s*[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*")))*(,|$)/g;
+ var paramexp = /[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*"))/g;
+
+ var matches = header.match(linkexp);
+ var rels = new Object();
+ for (let i = 0; i < matches.length; i++) {
+ var split = matches[i].split('>');
+ var href = split[0].substring(1);
+ var ps = split[1];
+ var link = new Object();
+ link.href = href;
+ var s = ps.match(paramexp);
+ for (let j = 0; j < s.length; j++) {
+ var p = s[j];
+ var paramsplit = p.split('=');
+ var name = paramsplit[0];
+ link[name] = unquote(paramsplit[1]);
+ }
+
+ if (link.rel != undefined) {
+ rels[link.rel] = link;
+ }
+ }
+ return rels;
+}
+//********************************************************************************
+
+
+export default Ember.Service.extend({
+ init() {
+ this.packages = {};
+ this.files = {};
+ this.haskellModules = {};
+ this.definitionSites = {};
+ this.modulePaths = {};
+ this.expressions = {};
+ this.references = {};
+ },
+ loadPackage(packageId) {
+ const packageInfo = this.packages[packageId];
+ if(packageInfo) {
+ return new RSVP.Promise((resolve) => {resolve(packageInfo);});
+ } else {
+ const url = urls.packageInfoUrl(packageId);
+ return Ember.$.getJSON(url).then((packageInfo) => {
+ this.packages[packageId] = packageInfo;
+ return packageInfo;
+ });
+ }
+ },
+ loadFile(packageId,filePath) {
+ const fileId = packageId + "/" + filePath;
+ const file = this.files[fileId];
+ if(file) {
+ return new RSVP.Promise((resolve) => {resolve(file);});
+ } else {
+ const url = urls.fileUrl(packageId,filePath);
+ return Ember.$.get({url:url,dataType:"text"}).then((text) => {
+ const file = {};
+ file.text = text;
+ file.packageId = packageId;
+ file.isHaskellModule = false;
+ file.id = filePath;
+ this.files[fileId] = file;
+ return file;
+ });
+ }
+ },
+ loadHaskellModule(packageId,filePath) {
+ const moduleId = packageId + "/" + filePath ;
+ const module = this.haskellModules[moduleId];
+ if(module) {
+ return new RSVP.Promise((resolve)=>{resolve(module);});
+ } else {
+ const url = urls.haskellModuleUrl(packageId,filePath);
+ return Ember.$.getJSON(url).then((module) => {
+ module.packageId = packageId;
+ module.isHaskellModule = true;
+ this.haskellModules[moduleId] = module;
+ return module;
+ });
+ }
+ },
+ loadDefinitionSite(packageId,moduleName,componentId,entity,name) {
+ const id = packageId + "/"+ componentId + "/" + moduleName + "/" + entity + "/" + name;
+ const definitionSite = this.definitionSites[id];
+ if(definitionSite) {
+ return new RSVP.Promise((resolve)=>{resolve(definitionSite);});
+ } else {
+ const url = urls.identifierDefinitionSiteUrl(packageId,moduleName,componentId,entity,name);
+ return Ember.$.getJSON(url).then((definitionSite) => {
+ this.definitionSites[id] = definitionSite;
+ return definitionSite;
+ });
+ }
+ },
+ loadExpressions(packageId,modulePath,lineStart,columnStart,lineEnd,columnEnd) {
+ const id = packageId + "/" + encodeURIComponent(modulePath)
+ + "/" + lineStart + "/" + columnStart + "/" + lineEnd + "/" + columnEnd;
+ const exprs = this.expressions[id];
+ if(exprs) {
+ return new RSVP.Promise((resolve)=>{resolve(exprs);});
+ } else {
+ const url = urls.expressionsUrl(packageId,modulePath,lineStart,columnStart,lineEnd,columnEnd);
+ return Ember.$.getJSON(url).then((exprs) => {
+ this.expressions[id] = exprs;
+ return exprs;
+ });
+ }
+ },
+ loadFromUrlPaginated(url) {
+ return Ember.$.getJSON(url).then((items,textStatus,jqXHR) => {
+ const linkHeaderText = jqXHR.getResponseHeader('Link');
+ const totalCountHeaderText = jqXHR.getResponseHeader('x-total-count');
+ const linkHeader = parseLinkHeader(linkHeaderText);
+ const total = parseInt(totalCountHeaderText);
+ return {
+ items:items,
+ total:total,
+ linkHeader:linkHeader
+ };
+ });
+ }
+});
diff --git a/javascript/app/styles/app.scss b/javascript/app/styles/app.scss
new file mode 100644
index 0000000..f094187
--- /dev/null
+++ b/javascript/app/styles/app.scss
@@ -0,0 +1,555 @@
+@import "ember-cli-bootstrap-4/bootstrap";
+
+body {
+ font-size:14px;
+}
+
+a, a:visited, a:focus, a:active, a:hover{
+ outline:0 none;
+}
+
+input:focus{
+ outline:none;
+}
+
+p {
+ margin-top:1rem;
+}
+
+.ember-radio-button {
+ cursor:pointer;
+}
+
+label {
+ margin-bottom:0px;
+}
+
+ul {
+ list-style: none;
+ padding : 0;
+}
+
+ol {
+ list-style: none;
+ padding : 0;
+}
+
+li {
+ list-style: none;
+}
+
+.documentation {
+ margin-top:10px;
+ li {
+ list-style: disc outside none;
+ }
+ ul {
+ padding-left:15px;
+ }
+ ol {
+ padding-left:15px;
+ }
+}
+
+.flex-container {
+ display:flex;
+ flex-direction:column;
+ height:100%;
+ width:100%;
+}
+
+.absolute-container {
+ position:absolute;
+ top:0;
+ bottom:0;
+ left:0;
+ right:0;
+}
+
+.header {
+ flex: none;
+ padding:5px;
+ img {
+ float:left;
+ }
+}
+
+.header-item {
+ display:inline-block;
+ margin-top:5px;
+ font-size : 1rem;
+ margin-right: 40px;
+ white-space: nowrap;
+ @media screen and (max-width: 700px) {
+ margin-right: 10px;
+ margin-left: 10px;
+ }
+}
+
+.content {
+ flex: auto;
+ position:relative;
+ overflow:auto;
+}
+
+.packages {
+ flex:auto;
+ overflow-y:auto;
+ li {
+ margin-bottom: 5px;
+ }
+ a {
+ margin-right:5px;
+ }
+}
+
+.package-content {
+ flex:auto;
+ position:relative;
+}
+
+.package-header {
+ padding:5px;
+}
+
+.package-header-input {
+ display:inline-block;
+ margin-right:10px;
+}
+
+.package-modules {
+ margin-left:10px;
+}
+
+.module-name {
+ font-size:0.7rem;
+}
+
+.package-search-form {
+ max-width:500px;
+ margin-bottom:10px;
+ margin-top:20px;
+}
+
+.package-header-filename {
+ white-space: nowrap;
+}
+
+.package-header-package-name {
+ margin-right:10px;
+ font-size:17px;
+}
+
+.module-search-input {
+ max-width:500px;
+ margin:5px;
+}
+
+ul.modules {
+ position:absolute;
+ top:50px;
+ bottom:0px;
+ margin:0px;
+ padding-left:15px;
+ left:0px;
+ right:0px;
+ display:flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ li {
+ margin-right: 10px;
+ }
+ overflow-x:auto;
+}
+
+.left-panel {
+ position: absolute;
+ bottom : 0;
+ top : 0;
+ left : 0;
+ width : 300px;
+}
+
+.show-left-panel-button {
+ position:absolute;
+ top:0px;
+ right:-20px;
+ z-index:2;
+ width:20px;
+ height:20px;
+ text-align:center;
+ vertical-align:middle;
+ a {
+ text-decoration:none;
+ }
+ opacity:0.6;
+ display:none;
+}
+
+.show-left-panel-button:hover {
+ opacity:1;
+ cursor:pointer;
+}
+
+.hide-file-tree {
+ position: relative;
+ top: 8px;
+ left: 3px;
+}
+
+.hide-declarations {
+ margin:7px;
+}
+
+.right-panel {
+ position: absolute;
+ bottom : 0;
+ top : 0;
+ right : 0;
+ left : 300px;
+ overflow-x: auto;
+ overflow-y: auto;
+}
+
+.file-tree-container {
+ overflow-x: hidden;
+ overflow-y: auto;
+ position: absolute;
+ bottom : 0;
+ top : 0;
+ left: 0;
+ right: 0;
+}
+
+.file-container {
+ position: absolute;
+ bottom : 0px;
+ top : 0;
+ right : 0;
+ left : 0;
+ overflow-x: auto;
+ overflow-y: auto;
+}
+
+.bottom-panel {
+ display:none;
+ position: absolute;
+ bottom : 0;
+ right : 0;
+ left : 0;
+ overflow-x: auto;
+ overflow-y: auto;
+ z-index:4;
+}
+
+.bottom-panel-header {
+ position: absolute;
+ top: 0;
+ right : 0;
+ left : 0;
+ height: 35px;
+ padding: 5px;
+}
+
+.bottom-panel-content {
+ position: absolute;
+ bottom : 0;
+ top : 35px;
+ right : 0;
+ left : 0;
+ overflow-x: auto;
+ overflow-y: auto;
+}
+
+.bottom-panel-header-options {
+ float:right;
+}
+
+.bottom-panel-header-content {
+ white-space:nowrap;
+}
+
+.highlighted-identifier {
+ border-radius: 3px;
+ box-shadow: 0 0 0 1px #B4B4B4;
+}
+
+.identifier {
+ cursor:pointer;
+}
+
+td.line-number {
+ cursor:pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ width: 1%;
+ min-width: 60px;
+ text-align: left;
+ padding-left:10px;
+ opacity: 0.8;
+}
+
+td.line-number:hover {
+ opacity : 1;
+}
+
+td.line-content {
+ padding-left:5px;
+
+}
+
+$source-code-font:Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;
+
+.source-code {
+ font-family: $source-code-font;
+ white-space:pre;
+ line-height:1.25;
+ tab-size: 4;
+}
+
+.break-word {
+ word-wrap:break-word;
+}
+
+code,pre {
+ font-family: $source-code-font !important;
+ white-space:pre;
+ line-height:1.25;
+ font-size:100%;
+ color : unset;
+}
+
+.source-code-font {
+ font-family: $source-code-font;
+ line-height:1.25;
+}
+
+.source-code-snippet {
+ padding: 4px;
+ text-decoration:none !important;
+ display:block;
+}
+
+.source-code-snippet > b {
+ border-radius: 3px;
+ border: 1px solid #B4B4B4;
+ margin: -1px;
+}
+
+.source-code-snippet:hover {
+ text-decoration:none;
+}
+
+.source-code-container {
+ margin : 5px;
+}
+
+.declarations {
+ position:fixed;
+ top:150px;
+ right:15px;
+ width: 450px;
+ z-index:1;
+ div {
+ margin : 5px;
+ }
+}
+
+.declarations-header {
+ opacity:0.9;
+ height:50px;
+}
+
+.declarations-content {
+ opacity:0.9;
+ position:absolute;
+ top:50px;
+ left:0px;
+ right:0px;
+ max-height:500px;
+ overflow-y:auto;
+ overflow-x:hidden;
+}
+
+@media screen and (max-width: 1500px) {
+ .declarations {
+ display:none;
+ }
+}
+
+.info-window-container {
+ position:absolute;
+ width:750px;
+ z-index:10;
+}
+
+.info-window-header {
+ margin:5px;
+ width: 100%;
+ cursor: move;
+ cursor: grab;
+ cursor: -moz-grab;
+ cursor: -webkit-grab;
+}
+
+.info-window-content {
+ max-height:200px;
+ overflow-x:auto;
+ overflow-y:auto;
+}
+
+.info-window-options {
+ margin-right:10px;
+ margin-left:10px;
+ float:right;
+}
+
+
+.ui-draggable-dragging .info-window-header {
+ cursor: grabbing;
+ cursor: -moz-grabbing;
+ cursor: -webkit-grabbing;
+}
+
+span.link {
+ cursor:pointer;
+ white-space:nowrap;
+}
+
+span.link:hover {
+ text-decoration:underline;
+}
+
+.ui-resizable-n {
+ height: 13px !important;;
+}
+
+.ui-resizable-w {
+ width: 13px !important;;
+}
+
+.type-info {
+ padding-top: 5px;
+ padding-bottom: 5px;
+}
+
+.type-component {
+ display: inline-block;
+}
+
+dd {
+ margin-left: 1em;
+}
+
+.autocomplete-container {
+ position:absolute;
+ z-index:10;
+ display:none;
+ left:0px;
+}
+
+ul.autocomplete-items {
+ li {
+ cursor:pointer;
+ max-height: 200px;
+ overflow: hidden;
+ }
+}
+
+.autocomplete-item {
+ padding:5px;
+}
+
+.file-tree-header {
+ position:absolute;
+ top:0;
+ left:0;
+ right:0;
+ height:50px;
+ margin:5px;
+}
+
+.file-tree-content {
+ position:absolute;
+ top:50px;
+ left:0;
+ right:0;
+ bottom:0;
+ overflow-x:auto;
+ overflow-x:auto;
+}
+
+button {
+ color:black;
+}
+
+.paginated-list-header {
+ position:absolute;
+ top:0px;
+ left:0px;
+ right:0px;
+ height:40px;
+ padding: 5px;
+ white-space:nowrap;
+}
+
+.paginated-list-content {
+ position:absolute;
+ top:40px;
+ bottom:0px;
+ left:0px;
+ right:0px;
+ overflow:auto;
+}
+
+.pagination-button {
+ font-size: 10px;
+}
+
+.search-results-header {
+ position:absolute;
+ top:0px;
+ left:0px;
+ right:0px;
+ height:35px;
+ padding: 5px;
+ white-space:nowrap;
+}
+
+.search-results-content {
+ position:absolute;
+ top:35px;
+ bottom:0px;
+ left:0px;
+ right:0px;
+}
+
+.search-result {
+ padding:5px;
+}
+
+.expression {
+ margin-bottom:15px;
+}
+
+.expressions {
+ margin:5px;
+}
+
+.file-name {
+ margin-left:5px;
+}
+
+.identifier-info {
+ margin:5px;
+}
+
+.identifier-module {
+ margin-top:10px;
+}
+
+.identifier-menu-item {
+ margin-right:10px;
+}
diff --git a/javascript/app/templates/application.hbs b/javascript/app/templates/application.hbs
new file mode 100644
index 0000000..bd64fdb
--- /dev/null
+++ b/javascript/app/templates/application.hbs
@@ -0,0 +1,26 @@
+<div class="absolute-container">
+ <div class="flex-container">
+ <div class="header">
+ <img src="/assets/haskell.ico">
+ <div class="container">
+ <span class="header-item">{{#link-to 'packages'}}Haskell packages{{/link-to}}</span>
+ <span class="header-item"><a target="_blank" href="https://github.com/alexwl/haskell-code-explorer">About</a></span>
+ <span class="header-item">
+ {{#each themes as |theme|}}
+ {{#radio-button
+ value=theme.id
+ groupValue=currentTheme
+ changed="themeChanged"}}
+ <span>{{theme.name}}</span>
+ {{/radio-button}}
+ {{/each}}
+ </span>
+ </div>
+ </div>
+ <div class="content">
+ <div class="absolute-container">
+ {{outlet}}
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/javascript/app/templates/bad-url.hbs b/javascript/app/templates/bad-url.hbs
new file mode 100644
index 0000000..6b36386
--- /dev/null
+++ b/javascript/app/templates/bad-url.hbs
@@ -0,0 +1,6 @@
+<div class="container">
+ <div style="margin-top:10px">
+ <h1>Not found</h1>
+ <div><a href="/">Main page</a></div>
+ </div>
+</div>
diff --git a/javascript/app/templates/components/.gitkeep b/javascript/app/templates/components/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/javascript/app/templates/components/.gitkeep
diff --git a/javascript/app/templates/components/bottom-panel.hbs b/javascript/app/templates/components/bottom-panel.hbs
new file mode 100644
index 0000000..26b1919
--- /dev/null
+++ b/javascript/app/templates/components/bottom-panel.hbs
@@ -0,0 +1,9 @@
+<div class="bottom-panel-header">
+ <span class="bottom-panel-header-content">{{yield "header"}}</span>
+ <span class="bottom-panel-header-options">
+ <a href="#" {{action "close"}}>Close</a>
+ </span>
+</div>
+<div class="bottom-panel-content">
+ {{yield "body"}}
+</div>
diff --git a/javascript/app/templates/components/expression-info.hbs b/javascript/app/templates/components/expression-info.hbs
new file mode 100644
index 0000000..509fb24
--- /dev/null
+++ b/javascript/app/templates/components/expression-info.hbs
@@ -0,0 +1,8 @@
+<div class="expressions">
+ {{#each expressions as |expression|}}
+ <div class="expression">
+ <span class="source-code">{{expression.sourceCode}}</span>
+ <br/>:: {{type-signature type=expression.info.exprType identifiers=identifiers currentLineNumber=currentLineNumber}}
+ </div>
+ {{/each}}
+</div>
diff --git a/javascript/app/templates/components/file-tree.hbs b/javascript/app/templates/components/file-tree.hbs
new file mode 100644
index 0000000..b5e38ef
--- /dev/null
+++ b/javascript/app/templates/components/file-tree.hbs
@@ -0,0 +1,8 @@
+<div class="file-tree-header">
+ <div class="input-group">
+ {{input class="form-control" value=query placeholder="Filename"}}<a class="hide-file-tree" href="#" {{action "hide"}}>Hide</a>
+ </div>
+</div>
+<div class="file-tree-content">
+ <div class="file-tree"></div>
+</div>
diff --git a/javascript/app/templates/components/haskell-module.hbs b/javascript/app/templates/components/haskell-module.hbs
new file mode 100644
index 0000000..77b9df7
--- /dev/null
+++ b/javascript/app/templates/components/haskell-module.hbs
@@ -0,0 +1,57 @@
+<div class="source-code-container"></div>
+{{#if declarations}}
+ <div class="declarations">
+ <div class="declarations-header">
+ <div class="input-group">
+ {{input class="form-control" value=query placeholder="Identifier"}}<a class="hide-declarations" href="#" {{action "toggleShowDeclarations"}}>{{showDeclarationsLabel}}</a>
+ </div>
+ </div>
+ {{#if showDeclarations}}
+ <div id="declarations-content" class="declarations-content">
+ <ul>
+ {{#infinite-list containerElementId="declarations-content" elements=filteredDeclarations perPage=30 as |declaration|}}
+ <li class="declaration">
+ <div class="declaration">
+ <a {{action "goToLine" declaration.lineNumber}} href="#"><span class="source-code-font">{{declaration.name}}{{#if declaration.declType}} :: {{type-signature-text components=declaration.declType.components}}{{/if}}</span>
+ </a><span>{{#unless declaration.isExported}}<i>(not exported)</i>{{/unless}}</span>
+ </div>
+ </li>
+ {{/infinite-list}}
+ </ul>
+ </div>
+ {{/if}}
+ </div>
+{{/if}}
+{{#info-window
+ targetElement=selectedIdentifier
+ isHoveredOverIdentifier=isHoveredOverIdentifier
+ hasSelectedExpression=hasSelectedExpression
+ containerElementId="right-panel" as |section|}}
+ {{#if (eq section "header")}}
+ {{#if hasSelectedExpression}}
+ <div>Selected expressions</div>
+ {{else}}
+ {{identifier-name
+ identifierInfo=identifierInfo
+ identifierOccurrence=identifierOccurrence
+ identifierElement=selectedIdentifier
+ currentPackageId=packageId
+ isBinder=identifierOccurrence.isBinder
+ currentLineNumber=currentLineNumber
+ findReferences=findReferences}}
+ {{/if}}
+ {{else}}
+ {{#unless hasSelectedExpression}}
+ {{identifier-info
+ identifierInfo=identifierInfo
+ identifierOccurrence=identifierOccurrence
+ identifiers=identifiers
+ currentLineNumber=currentLineNumber}}
+ {{else}}
+ {{expression-info
+ expressions=expressions
+ identifiers=identifiers
+ currentLineNumber=currentLineNumber}}
+ {{/unless}}
+ {{/if}}
+{{/info-window}}
diff --git a/javascript/app/templates/components/identifier-info.hbs b/javascript/app/templates/components/identifier-info.hbs
new file mode 100644
index 0000000..7146a2e
--- /dev/null
+++ b/javascript/app/templates/components/identifier-info.hbs
@@ -0,0 +1,39 @@
+{{#if (or identifierInfo identifierOccurrence.idOccType)}}
+ <div class="identifier-info">
+ {{#if identifierInfo}}
+ {{#if isNaughtyRecSel}}
+ <i>This record selector can never be called because its type mentions a type variable that isn't in the result type of the constructor</i>
+ {{else}}
+ {{type-signature
+ type=identifierInfo.idType
+ identifiers=identifiers
+ currentLineNumber=currentLineNumber}}
+ {{/if}}
+ {{/if}}
+ {{#if (and identifierInfo identifierOccurrence identifierOccurrence.idOccType)}}
+ <div style="height:10px"></div>
+ {{/if}}
+ {{#if (and identifierOccurrence identifierOccurrence.idOccType)}}
+ {{type-signature
+ type=identifierOccurrence.idOccType
+ identifiers=identifiers
+ currentLineNumber=currentLineNumber}}
+ {{/if}}
+ <div class="documentation">
+ {{{identifierInfo.doc}}}
+ {{{downloadedDocumentation}}}
+ </div>
+ {{#if identifierOccurrence.instanceResolution}}
+ <div style="height:10px"></div>
+ {{/if}}
+ <div>
+ {{#if identifierOccurrence.instanceResolution}}
+ {{instance-info
+ instance=identifierOccurrence.instanceResolution
+ identifiers=identifiers
+ nestedLevel=0
+ currentLineNumber=currentLineNumber}}
+ {{/if}}
+ </div>
+ </div>
+{{/if}}
diff --git a/javascript/app/templates/components/identifier-name.hbs b/javascript/app/templates/components/identifier-name.hbs
new file mode 100644
index 0000000..ca16e23
--- /dev/null
+++ b/javascript/app/templates/components/identifier-name.hbs
@@ -0,0 +1,12 @@
+<div>
+ <span class="source-code-font break-word identifier-menu-item" style={{style}}>{{#if identifierInfo.demangledOccName}}{{identifierInfo.demangledOccName}}{{else}}{{name}}{{/if}}</span>
+ {{#unless isBinder}}
+ {{#if location}}
+ <span class="identifier-menu-item">{{location}}</span>
+ <span class="link identifier-menu-item" onmouseup={{action "goToDefinition"}}>Go to definition</span>
+ {{/if}}
+ {{/unless}}
+ {{#if isExternalIdentifier}}
+ <span class="link identifier-menu-item" onmouseup={{action "findReferences" identifierInfo currentPackageId}}>Find references</span>
+ {{/if}}
+</div>
diff --git a/javascript/app/templates/components/infinite-list.hbs b/javascript/app/templates/components/infinite-list.hbs
new file mode 100644
index 0000000..ab103d4
--- /dev/null
+++ b/javascript/app/templates/components/infinite-list.hbs
@@ -0,0 +1,3 @@
+{{#each renderedElements as |element|}}
+ {{yield element}}
+{{/each}}
diff --git a/javascript/app/templates/components/info-window.hbs b/javascript/app/templates/components/info-window.hbs
new file mode 100644
index 0000000..003e2c5
--- /dev/null
+++ b/javascript/app/templates/components/info-window.hbs
@@ -0,0 +1,12 @@
+<div class="info-window">
+ <div class="info-window-header">
+ <div class="info-window-options">
+ <a href="#" {{action "pin"}}>{{#if isPinned}}Unpin{{else}}Pin{{/if}}</a>
+ <a href="#" {{action "close"}}>Close</a>
+ </div>
+ {{yield "header"}}
+ </div>
+ <div class="info-window-content">
+ {{yield "body"}}
+ </div>
+</div>
diff --git a/javascript/app/templates/components/input-with-autocomplete.hbs b/javascript/app/templates/components/input-with-autocomplete.hbs
new file mode 100644
index 0000000..eb0100d
--- /dev/null
+++ b/javascript/app/templates/components/input-with-autocomplete.hbs
@@ -0,0 +1,19 @@
+<div class="input-group">
+ {{input class="form-control search-input" value=query placeholder=placeholder}}
+ <div class="autocomplete-container">
+ {{# if items}}
+ <ul class="autocomplete-items">
+ {{#each items as |item index|}}
+ <li class="{{if (eq index highlightedItemIndex) "highlighted"}}" onclick={{action "goToDefinition" item}}>
+ <div class="autocomplete-item">
+ {{yield item}}
+ </div>
+ </li>
+ {{/each}}
+ </ul>
+ {{/if}}
+ </div>
+ <div class="input-group-append">
+ <button id="search-input" class="btn btn-outline-secondary" {{action "onSubmit" query}}>Search</button>
+ </div>
+</div>
diff --git a/javascript/app/templates/components/instance-info.hbs b/javascript/app/templates/components/instance-info.hbs
new file mode 100644
index 0000000..b5c2fdf
--- /dev/null
+++ b/javascript/app/templates/components/instance-info.hbs
@@ -0,0 +1,10 @@
+{{#if instance}}
+ <div style={{style}}>
+ <span class="source-code-font">
+ instance {{type-signature type=instance.instanceType identifiers=identifiers currentLineNumber=currentLineNumber noExpand=true}}</span>
+ (<span class="link" onclick={{action "goToDefinition"}}>Go to definition</span>)
+ {{#each instance.instances as |inst|}}
+ {{instance-info instance=inst identifiers=identifiers nestedLevel=nextNestedLevel currentLineNumber=currentLineNumber}}
+ {{/each}}
+ </div>
+{{/if}}
diff --git a/javascript/app/templates/components/paginated-list.hbs b/javascript/app/templates/components/paginated-list.hbs
new file mode 100644
index 0000000..2ae9813
--- /dev/null
+++ b/javascript/app/templates/components/paginated-list.hbs
@@ -0,0 +1,17 @@
+<div class="paginated-list-header">
+ <span>Found {{total}}</span>
+ {{#if (or next prev)}}
+ &nbsp;
+ &nbsp;
+ <span>
+ {{#if first}}<button class="btn btn-outline-secondary btn-sm" {{action "update" first.href}}>&lt;&lt;</button>{{/if}}
+ {{#if prev}}<button button class="btn btn-outline-secondary btn-sm" {{action "update" prev.href}}>&lt;</button>{{/if}}
+ {{firstItemOnPage}} - {{lastItemOnPage}}
+ {{#if next}}<button button class="btn btn-outline-secondary btn-sm" {{action "update" next.href}}>&gt;</button>{{/if}}
+ {{#if last}}<button button class="btn btn-outline-secondary btn-sm" {{action "update" last.href}}>&gt;&gt;</button>{{/if}}
+ </span>
+ {{/if}}
+</div>
+<div class="paginated-list-content">
+ {{yield items}}
+</div>
diff --git a/javascript/app/templates/components/resizable-panel.hbs b/javascript/app/templates/components/resizable-panel.hbs
new file mode 100644
index 0000000..0fd3aa7
--- /dev/null
+++ b/javascript/app/templates/components/resizable-panel.hbs
@@ -0,0 +1,5 @@
+<div class="absolute-container">{{yield (action "hide")}}
+ <div onclick={{action "hide"}} class="show-left-panel-button">
+ {{{hideButtonLabel}}}
+ </div>
+</div>
diff --git a/javascript/app/templates/components/text-file.hbs b/javascript/app/templates/components/text-file.hbs
new file mode 100644
index 0000000..6a50453
--- /dev/null
+++ b/javascript/app/templates/components/text-file.hbs
@@ -0,0 +1,3 @@
+<div class="source-code-container">
+{{{html}}}
+</div>
diff --git a/javascript/app/templates/components/type-component.hbs b/javascript/app/templates/components/type-component.hbs
new file mode 100644
index 0000000..775324e
--- /dev/null
+++ b/javascript/app/templates/components/type-component.hbs
@@ -0,0 +1,3 @@
+{{#if (and expanded identifierInfo)}}({{/if}}
+<span class="source-code {{linkClass}}" onmouseup={{action "onmouseup"}}>{{occName}}</span>
+{{#if (and expanded identifierInfo)}} :: {{type-signature type=identifierInfo.idType identifiers=identifiers noExpand=true}}){{/if}}
diff --git a/javascript/app/templates/components/type-signature-text.hbs b/javascript/app/templates/components/type-signature-text.hbs
new file mode 100644
index 0000000..505a466
--- /dev/null
+++ b/javascript/app/templates/components/type-signature-text.hbs
@@ -0,0 +1,2 @@
+{{!-- No newlines to get rid of spaces between spans --}}
+{{#each components as |typeComponent|}}{{#if (eq typeComponent.tag "Text")}}{{typeComponent.contents}}{{else}}{{typeComponent.name}}{{/if}}{{/each}}
diff --git a/javascript/app/templates/components/type-signature.hbs b/javascript/app/templates/components/type-signature.hbs
new file mode 100644
index 0000000..249f1b8
--- /dev/null
+++ b/javascript/app/templates/components/type-signature.hbs
@@ -0,0 +1,10 @@
+{{!-- No newlines to get rid of spaces between spans --}}
+{{#each components as |typeComponent|}}{{#if (eq typeComponent.tag "Text")}}<span class="source-code">{{typeComponent.contents}}</span>{{else}}{{type-component occName=typeComponent.name internalId=typeComponent.internalId identifiers=identifiers currentLineNumber=currentLineNumber}}{{/if}}{{/each}}
+{{#unless noExpand}}
+ {{#if type.componentsExpanded}}
+ <div style="margin-top:5px">
+ <button class="btn btn-outline-secondary btn-sm" {{action "toggleExpandTypeSynonyms"}}>{{expandTypeSynonymsLabel}}
+ </button>
+ </div>
+ {{/if}}
+{{/unless}}
diff --git a/javascript/app/templates/package.hbs b/javascript/app/templates/package.hbs
new file mode 100644
index 0000000..c8ed0dc
--- /dev/null
+++ b/javascript/app/templates/package.hbs
@@ -0,0 +1,57 @@
+<div class="flex-container">
+ <div class="package-header">
+ <span class="package-header-package-name">{{#link-to 'package' model}}{{model.id}}{{/link-to}}</span>
+ <span class="package-header-input">
+ {{#input-with-autocomplete
+ onSubmit=(action 'searchIdentifier')
+ createSearchUrlFunction=createSearchUrlFunction
+ maxItems=10
+ selectItem=(action 'showIdentifier')
+ placeholder="Identifier" as |identifier|}}
+ <span class="source-code-font">{{identifier.demangledOccName}} :: {{type-signature-text components=identifier.idType.components}}</span>
+ <div class="module-name">
+ {{#if identifier.locationInfo.modulePath}}
+ {{identifier.locationInfo.modulePath}}
+ {{else}}
+ {{identifier.locationInfo.moduleName}}
+ {{/if}}
+ </div>
+ {{/input-with-autocomplete}}
+ </span>
+ {{#if currentFile}}
+ <span class="package-header-filename">
+ {{currentFile}}
+ </span>
+ {{/if}}
+ </div>
+ <div class="package-content">
+ {{#resizable-panel class="left-panel" alsoResizeElementId="#right-panel" as |hide|}}
+ <div class="file-tree-container">
+ {{file-tree directoryTree=model.directoryTree openFile="openFile" currentFile=currentFile packageId=model.id hide=hide}}
+ </div>
+ {{/resizable-panel}}
+ <div id="right-panel" class="right-panel">
+ <div id="file-container" class="file-container">
+ {{outlet}}
+ </div>
+ {{#bottom-panel visible=bottomPanelVisible topPanelElementId="#file-container" containerElementId="#right-panel" as |section|}}
+ {{#if (eq section "header")}}
+ References to <b><span class="source-code">{{occName}}</span></b> in <span class="source-code">{{packageId}}</span>
+ {{else}}
+ {{#paginated-list url=referencesUrl as |files|}}
+ <ul>
+ {{#each files as |file|}}
+ <li>
+ <div class="file-name"><a href="/package/{{packageId}}/show/{{file.name}}">{{file.name}}</a></div>
+ {{#each file.references as |reference|}}
+ <a class="source-code source-code-snippet" href="/package/{{packageId}}/show/{{file.name}}#L{{reference.idSrcSpan.line}}">{{{reference.sourceCodeHtml}}}</a>
+ {{/each}}
+ </li>
+ {{/each}}
+ </ul>
+ {{/paginated-list}}
+ {{/if}}
+ {{/bottom-panel}}
+ </div>
+ </div>
+</div>
diff --git a/javascript/app/templates/package/index.hbs b/javascript/app/templates/package/index.hbs
new file mode 100644
index 0000000..e9bc592
--- /dev/null
+++ b/javascript/app/templates/package/index.hbs
@@ -0,0 +1,12 @@
+<div class="package-modules">
+ <div class="module-search-input">
+ {{input class="form-control" type="text" value=query placeholder="Module name"}}
+ </div>
+ <ul class="modules">
+ {{#each modulesFiltered as |module|}}
+ <li>
+ <a href="/package/{{model.id}}/show/{{module}}">{{module}}</a>
+ </li>
+ {{/each}}
+ </ul>
+</div>
diff --git a/javascript/app/templates/package/search.hbs b/javascript/app/templates/package/search.hbs
new file mode 100644
index 0000000..ebc4d52
--- /dev/null
+++ b/javascript/app/templates/package/search.hbs
@@ -0,0 +1,21 @@
+<div class="search-results-header">
+ Query : {{model.query}}
+</div>
+<div class="search-results-content">
+ {{#paginated-list url=model.url as |identifiers|}}
+ <ul>
+ {{#each identifiers as |identifier|}}
+ <li class="search-result">
+ <span class="source-code" >{{identifier.demangledOccName}} :: {{type-signature-text components=identifier.idType.components}}</span>
+ <div><a href="#" onmouseup={{action "goToDefinition" identifier.locationInfo}}>Go to definition</a></div>
+ <div class="identifier-module">
+ {{#if identifier.locationInfo.modulePath}}
+ Defined in <a href="/package/{{identifier.locationInfo.packageId.name}}-{{identifier.locationInfo.packageId.version}}/show/{{identifier.locationInfo.modulePath}}">{{identifier.locationInfo.modulePath}}</a>
+ {{/if}}
+ </div>
+ <div>{{{identifier.doc}}}</div>
+ </li>
+ {{/each}}
+ </ul>
+ {{/paginated-list}}
+</div>
diff --git a/javascript/app/templates/package/show.hbs b/javascript/app/templates/package/show.hbs
new file mode 100644
index 0000000..c24cd68
--- /dev/null
+++ b/javascript/app/templates/package/show.hbs
@@ -0,0 +1 @@
+{{outlet}}
diff --git a/javascript/app/templates/package/show/file.hbs b/javascript/app/templates/package/show/file.hbs
new file mode 100644
index 0000000..b127fd7
--- /dev/null
+++ b/javascript/app/templates/package/show/file.hbs
@@ -0,0 +1,16 @@
+{{#if model.isHaskellModule}}
+ {{haskell-module
+ path=model.id
+ name=model.name
+ packageId=model.packageId
+ componentId=model.componentId
+ html=model.sourceCodeHtml
+ identifiers=model.identifiers
+ occurrences=model.occurrences
+ colorTheme=settings.colorTheme
+ declarations=model.declarations
+ findReferences=(action "findReferences")
+ }}
+{{else}}
+ {{text-file text=model.text path=model.id}}
+{{/if}}
diff --git a/javascript/app/templates/packages.hbs b/javascript/app/templates/packages.hbs
new file mode 100644
index 0000000..6dc4b7b
--- /dev/null
+++ b/javascript/app/templates/packages.hbs
@@ -0,0 +1,26 @@
+<div class="flex-container container">
+ <div>
+ <!-- <p class="lead"><b>Haskell code explorer</b> </p> -->
+ <div class="package-search-form">
+ {{input class="form-control" type="text" value=query placeholder="Package name"}}
+ <span>Number of packages : {{packages.length}}</span>
+ </div>
+ </div>
+ <div id="packages" class="packages">
+ <ul>
+ {{#infinite-list containerElementId="packages" elements=packages perPage=80 as |package|}}
+ <li>
+ {{#each package.versions as |version index|}}
+ {{#link-to 'package' (concat package.name "-" version)}}
+ {{#if (gt index 0)}}
+ {{version}}
+ {{else}}
+ {{package.name}}-{{version}}
+ {{/if}}
+ {{/link-to}}
+ {{/each}}
+ </li>
+ {{/infinite-list}}
+ </ul>
+ </div>
+</div>
diff --git a/javascript/app/utils/api-urls.js b/javascript/app/utils/api-urls.js
new file mode 100644
index 0000000..b2748b6
--- /dev/null
+++ b/javascript/app/utils/api-urls.js
@@ -0,0 +1,29 @@
+import config from '../config/environment';
+
+export const urls = {
+ packageInfoUrl : function(packageId) {
+ return config.APP.staticUrlPrefix+"/"+packageId+"/"+config.APP.haskellCodeExplorerDirectory+"/packageInfo.json";
+ },
+ fileUrl : function(packageId,filePath) {
+ return config.APP.staticUrlPrefix+"/"+packageId+"/"+filePath;
+ },
+ haskellModuleUrl : function (packageId,filePath) {
+ return config.APP.staticUrlPrefix+"/"+packageId+"/"+config.APP.haskellCodeExplorerDirectory+"/"+encodeURIComponent(encodeURIComponent(filePath))+ ".json";
+ },
+ packagesUrl : config.APP.apiUrlPrefix + "/packages",
+ identifierDefinitionSiteUrl : function(packageId,moduleName,componentId,entity,name) {
+ return config.APP.apiUrlPrefix + "/definitionSite/" + packageId+"/"+componentId+"/"+moduleName+"/"+entity+"/"+encodeURIComponent(name).replace(/\./g, '%2E');
+ },
+ modulePathUrl : function (packageId,moduleName,componentId) {
+ return config.APP.apiUrlPrefix + "/modulePath/"+packageId+"/"+componentId+"/"+moduleName;
+ },
+ expressionsUrl : function (packageId,modulePath,lineStart,columnStart,lineEnd,columnEnd) {
+ return config.APP.apiUrlPrefix + "/expressions/"+packageId+"/"+encodeURIComponent(modulePath) +"/"+lineStart+"/"+columnStart+"/"+lineEnd+"/"+columnEnd;
+ },
+ referencesUrl : function (packageId,externalId) {
+ return config.APP.apiUrlPrefix + "/references/"+packageId+"/"+encodeURIComponent(externalId);
+ },
+ identifierSearchUrl : function (packageId,query) {
+ return config.APP.apiUrlPrefix + "/identifiers/"+packageId+"/"+encodeURIComponent(query).replace(/\./g, '%2E');
+ }
+}
diff --git a/javascript/app/utils/color-themes.js b/javascript/app/utils/color-themes.js
new file mode 100644
index 0000000..0a563ff
--- /dev/null
+++ b/javascript/app/utils/color-themes.js
@@ -0,0 +1,188 @@
+function colorThemeToCss(colorTheme) {
+ const css = `
+ body {
+ color: ${colorTheme.defaultColor} !important;
+ background-color: ${colorTheme.backgroundColor} !important;
+ }
+ input {
+ color: ${colorTheme.defaultColor} !important;
+ background-color: ${colorTheme.backgroundColor} !important;
+ border-color: ${colorTheme.borderColor} !important;
+ }
+ .package-content {
+ border-top: 1px solid ${colorTheme.borderColor} !important;
+ }
+ .header a {
+ color : ${colorTheme.menuLinkColor} !important;
+ }
+ a {
+ color: ${colorTheme.typeColor} !important;
+ }
+ span.link {
+ color: ${colorTheme.typeColor} !important;
+ }
+ .header {
+ background-color: ${colorTheme.menuColor} !important;
+ border-bottom: 1px solid ${colorTheme.borderColor} !important;
+ }
+ .declarations-content {
+ background-color: ${colorTheme.navigationPanelColor} !important;
+ border: 1px solid ${colorTheme.borderColor} !important;
+ }
+ .declarations-header {
+ background-color: ${colorTheme.navigationPanelColor} !important;
+ border: 1px solid ${colorTheme.borderColor} !important;
+ }
+ li.declaration {
+ border-bottom: 1px solid ${colorTheme.borderColor} !important;
+ }
+ .left-panel {
+ background-color: ${colorTheme.navigationPanelColor} !important;
+ border-right: 1px solid ${colorTheme.borderColor} !important;
+ }
+ .show-left-panel-button {
+ background-color: ${colorTheme.navigationPanelColor} !important;
+ border-right:1px solid ${colorTheme.borderColor} !important;
+ border-bottom:1px solid ${colorTheme.borderColor} !important;
+ }
+ .right-panel {
+ background-color: ${colorTheme.backgroundColor} !important;
+ }
+ a.jstree-anchor {
+ color: ${colorTheme.defaultColor} !important;
+ }
+ .declaration > a {
+ color: ${colorTheme.defaultColor} !important;
+ }
+ .highlighted-line {
+ background : ${colorTheme.highlightedLineColor} !important;
+ }
+ table.source-code {
+ background-color: ${colorTheme.backgroundColor} !important;
+ color: ${colorTheme.defaultColor} !important;
+ }
+ .jstree-clicked {
+ background-color: ${colorTheme.backgroundColor} !important;
+ }
+ .jstree-hovered {
+ background-color: ${colorTheme.backgroundColor} !important;
+ }
+ ul.autocomplete-items {
+ background-color: ${colorTheme.backgroundColor} !important;
+ border-top: 1px solid ${colorTheme.borderColor} !important;
+ border-left: 1px solid ${colorTheme.borderColor} !important;
+ border-right: 1px solid ${colorTheme.borderColor} !important;
+ }
+ ul.autocomplete-items > li {
+ border-bottom: 1px solid ${colorTheme.borderColor} !important;
+ }
+ ul.autocomplete-items > li:hover {
+ background-color: ${colorTheme.highlightedLineColor} !important;
+ }
+ ul.autocomplete-items > li.highlighted {
+ background-color: ${colorTheme.highlightedLineColor} !important;
+ }
+ .source-code-snippet {
+ color: ${colorTheme.defaultColor} !important;
+ border-bottom: 1px solid ${colorTheme.borderColor} !important;
+ }
+ .source-code-snippet:hover {
+ background-color: ${colorTheme.highlightedLineColor} !important;
+ }
+ .bottom-panel {
+ background-color: ${colorTheme.backgroundColor} !important;
+ border-top: 1px solid ${colorTheme.borderColor} !important;
+ }
+ .bottom-panel-header {
+ border-bottom: 1px solid ${colorTheme.borderColor} !important;
+ }
+ .paginated-list-header {
+ border-bottom: 1px solid ${colorTheme.borderColor} !important;
+ }
+ li.search-result {
+ border-bottom: 1px solid ${colorTheme.borderColor} !important;
+ }
+ .search-results-header {
+ border-bottom: 1px solid ${colorTheme.borderColor} !important;
+ }
+ .info-window-content {
+ border-top: 1px solid ${colorTheme.borderColor} !important;
+ }
+ .info-window {
+ border: 1px solid ${colorTheme.borderColor} !important;
+ background-color:${colorTheme.infoWindowColor} !important;
+ color: ${colorTheme.defaultColor} !important;
+ }
+ .type-info {
+ border-top: 1px solid ${colorTheme.borderColor} !important;
+ }`;
+ return css;
+}
+
+const darkTheme = {
+ id: "darkTheme",
+ name: "Dark theme",
+ description: "Dark theme (Monokai based)",
+ defaultColor: "#F8F8F2",
+ backgroundColor: "#272822",
+ typeColor: "#66D9EF",
+ literalColor: "#E6DB74",
+ topLevelIdFromCurrentModule : "#A6E22E",
+ localIdentifierColor: ["#F0A3FF","#0075DC","#993F00",
+ "#2BCE48","#FFCC99","#808080","#94FFB5","#8F7C00",
+ "#C20088","#FFA405","#FFA8BB","#426600","#FF0010",
+ "#5EF1F2","#00998F","#E0FF66","#FFFF80",
+ "#FFFF00","#FF5005"],
+ menuColor: "#3c3b37",
+ menuLinkColor : "#F8F8F2",
+ infoWindowColor: "#3c3b37",
+ navigationPanelColor: "#3c3b37",
+ linkColor : "#0366d6",
+ borderColor: "#535557",
+ highlightedLineColor: "#4a4a4a"
+};
+
+const lightTheme = {
+ id: "lightTheme",
+ name: "Light theme",
+ description: "Light theme (Github based)",
+ defaultColor: "#24292e",
+ backgroundColor: "#ffffff",
+ typeColor: "#005cc5",
+ literalColor: "#032f62",
+ topLevelIdFromCurrentModule : "#6f42c1",
+ localIdentifierColor: ["#005C31",
+ "#2BCE48","#808080","#8F7C00",
+ "#C20088","#FFA405","#ffa8bb","#426600","#FF0010",
+ "#09d7d8","#00998F","#990000","#FF5005"],
+ menuColor: "#f2f4f8",
+ menuLinkColor : "#24292e",
+ infoWindowColor: "#f2f4f8",
+ navigationPanelColor: "#f2f4f8",
+ linkColor : "#0366d6",
+ borderColor: "#e1e4e8",
+ highlightedLineColor: "#eaeaea"
+};
+
+function updateColorThemeCss (colorTheme) {
+ const newStyle = document.createElement('style');
+ newStyle.type = 'text/css';
+ newStyle.innerHTML = colorThemeToCss(colorTheme);
+ newStyle.id = 'color-theme';
+ const oldStyle = document.querySelector("style#color-theme");
+ if(oldStyle) {
+ oldStyle.parentElement.removeChild(oldStyle);
+ }
+ document.getElementsByTagName('head')[0].appendChild(newStyle);
+}
+
+const themes = {
+ darkTheme: darkTheme,
+ lightTheme: lightTheme
+};
+
+export {
+ updateColorThemeCss,
+ colorThemeToCss,
+ themes
+}
diff --git a/javascript/app/utils/go-to-definition.js b/javascript/app/utils/go-to-definition.js
new file mode 100644
index 0000000..e05f6bf
--- /dev/null
+++ b/javascript/app/utils/go-to-definition.js
@@ -0,0 +1,95 @@
+function exactLocationToUrl(exactLocation) {
+ const modulePath = exactLocation.modulePath;
+ const packageId = exactLocation.packageId.name + "-" + exactLocation.packageId.version;
+ let hash = "";
+ if(exactLocation.startLine != 1) {
+ hash = "#L" + exactLocation.startLine;
+ }
+ return "/package/"+packageId+"/show/"+modulePath+hash;
+}
+
+function hackageUrl(packageId,locationInfo) {
+ const dasherizedModuleName = locationInfo.moduleName.replace(/\./g,'-');
+ let key;
+ if(locationInfo.entity === "Val") {
+ key = "v";
+ } else {
+ key = "t";
+ }
+ let hash = "";
+ if(locationInfo.entity === "Val" || locationInfo.entity === "Typ") {
+ hash = "#"+key+":"+locationInfo.haddockAnchorId;
+ }
+ return "https://hackage.haskell.org/package/"+packageId+"/docs/"+dasherizedModuleName+".html"+hash;
+}
+
+function openUrl(buttonId,url) {
+ if(buttonId === 2) {//middle mouse button
+ window.open(url, '_blank');
+ } else if(buttonId == 1) {//left mouse button
+ window.location = url;
+ }
+ return false;
+}
+
+function saveCurrentLocation(currentLineNumber) {
+ if(currentLineNumber) {
+ const url = window.location.origin + window.location.pathname + "#L" + currentLineNumber;
+ if(location.href != url) {
+ window.location.hash = "#L" + currentLineNumber;
+ }
+ }
+}
+
+function goToDefinition(store,locationInfo,buttonId,currentLineNumber) {
+ if(locationInfo.tag === "ExactLocation") {
+ const url = exactLocationToUrl(locationInfo);
+ if(locationInfo.startLine !== currentLineNumber) {
+ saveCurrentLocation(currentLineNumber);
+ }
+ openUrl(buttonId,url);
+ } else if(locationInfo.tag === "ApproximateLocation") {
+ const packageId = locationInfo.packageId.name+"-"+locationInfo.packageId.version;
+ if(locationInfo.entity === "Mod") {
+ store.loadDefinitionSite(packageId,
+ locationInfo.moduleName,
+ locationInfo.componentId,
+ locationInfo.entity,
+ locationInfo.moduleName)
+ .then((defSite) => {
+ const packageId = defSite.location.packageId.name + "-" + defSite.location.packageId.version;
+ openUrl(buttonId,"/package/" + packageId + "/show/" + defSite.location.modulePath);
+ }).catch(() => {
+ openUrl(buttonId,hackageUrl(packageId,locationInfo));
+ });
+ } else {
+ store.loadDefinitionSite(packageId,
+ locationInfo.moduleName,
+ locationInfo.componentId,
+ locationInfo.entity,
+ locationInfo.name)
+ .then((definitionSite) => {
+ if(definitionSite.location.tag === "ExactLocation") {
+ const url = exactLocationToUrl(definitionSite.location);
+ if(locationInfo.startLine !== currentLineNumber) {
+ saveCurrentLocation(currentLineNumber);
+ }
+ openUrl(buttonId,url);
+ } else {
+ saveCurrentLocation(currentLineNumber);
+ openUrl(buttonId,hackageUrl(packageId,locationInfo));
+ }
+ }).catch((e) => {
+ console.log(e);
+ saveCurrentLocation(currentLineNumber);
+ openUrl(buttonId,hackageUrl(packageId,locationInfo));
+ });
+ }
+ } else {
+ alert('No location info');
+ }
+}
+
+export {
+ goToDefinition,openUrl
+}
diff --git a/javascript/app/utils/line-selection.js b/javascript/app/utils/line-selection.js
new file mode 100644
index 0000000..d6d0976
--- /dev/null
+++ b/javascript/app/utils/line-selection.js
@@ -0,0 +1,111 @@
+function initializeLineSelection(sourceCodeContainerElement,component) {
+ const lineNumbers = Array.prototype.slice.call(sourceCodeContainerElement.querySelectorAll("td.line-number"));
+ if(lineNumbers.length > 0) {
+ const onhashchange = function () {
+ highlightSelectedLines(sourceCodeContainerElement);
+ }
+ window.addEventListener("hashchange",onhashchange);
+ component._onhashchange = onhashchange;
+
+ let shiftPressed;
+ const onkeydown = function (event) {
+ if(event.keyCode === 16) { shiftPressed = true; }
+ };
+ const onkeyup = function (event) {
+ if(event.keyCode === 16) { shiftPressed = false; }
+ };
+
+ document.addEventListener('keydown',onkeydown);
+ document.addEventListener('keyup',onkeyup);
+ component._onkeydown = onkeydown;
+ component._onkeyup = onkeyup;
+
+ let selectedLine1,selectedLine2;
+ lineNumbers.forEach((lineNumberElement) => {
+ lineNumberElement.onclick = function() {
+ const number = parseInt(this.textContent);
+ if(shiftPressed && selectedLine1) {
+ if(selectedLine1 != number) {
+ selectedLine2 = number;
+ if(selectedLine1 < selectedLine2) {
+ highlightLines(sourceCodeContainerElement,selectedLine1,selectedLine2);
+ window.location.hash = "L"+selectedLine1+"-L"+selectedLine2;
+ } else {
+ highlightLines(sourceCodeContainerElement,selectedLine2,selectedLine1);
+ window.location.hash = "L"+selectedLine2+"-L"+selectedLine1;
+ }
+ }
+ } else {
+ selectedLine1 = number;
+ selectedLine2 = null;
+ highlightLines(sourceCodeContainerElement,selectedLine1,selectedLine1);
+ window.location.hash = "L"+number;
+ }
+ }
+ });
+ const lines = highlightSelectedLines(sourceCodeContainerElement);
+ if(lines.length) {
+ selectedLine1 = lines[0];
+ selectedLine2 = lines[1];
+ }
+ }
+}
+
+function highlightSelectedLines (sourceCodeContainerElement) {
+ const lineInfo = window.location.hash.slice(1);
+ if(lineInfo) {
+ if(lineInfo.includes('-')) {
+ const lines = lineInfo.split("-");
+ const lineNumber1 = parseInt(lines[0].substring(1));
+ const lineNumber2 = parseInt(lines[1].substring(1));
+ if(lineNumber1 && lineNumber2 && lineNumber1 <= lineNumber2) {
+ highlightLines(sourceCodeContainerElement,lineNumber1,lineNumber2);
+ const line = sourceCodeContainerElement.querySelector("td#LC"+lineNumber1);
+ if(line) {
+ scrollLineIntoView(line,sourceCodeContainerElement);
+ }
+ return [lineNumber1,lineNumber2];
+ }
+ } else {
+ const lineNumber = parseInt(lineInfo.substring(1));
+ if(lineNumber) {
+ highlightLines(sourceCodeContainerElement,lineNumber,lineNumber);
+ const line = sourceCodeContainerElement.querySelector("td#LC"+lineNumber);
+ if(line) {
+ scrollLineIntoView(line,sourceCodeContainerElement);
+ }
+ return [lineNumber];
+ }
+ }
+ } else {
+ highlightLines(sourceCodeContainerElement,0,0);
+ return [];
+ }
+}
+
+function scrollLineIntoView(lineElement,sourceCodeContainerElement) {
+ lineElement.parentNode.scrollIntoView();
+ const container = sourceCodeContainerElement.parentNode.parentNode;
+ const windowHeight = container.offsetHeight;
+ const fullHeight = sourceCodeContainerElement.offsetHeight;
+
+ if(fullHeight - container.scrollTop > windowHeight) {
+ container.scrollTop = container.scrollTop - (windowHeight/2 - 20);
+ }
+}
+
+function highlightLines(parentElement,startLine,endLine) {
+ const lineElements = Array.prototype.slice.call(parentElement.querySelectorAll("td.line-content"));
+ lineElements.forEach((lineElement) => {
+ const number = parseInt(lineElement.id.substring(2)); //<td "id"="LC10">...</td>
+ if(number >= startLine && number <= endLine) {
+ lineElement.classList.add('highlighted-line');
+ } else {
+ lineElement.classList.remove('highlighted-line');
+ }
+ });
+}
+
+export {
+ initializeLineSelection,highlightLines,highlightSelectedLines
+}