aboutsummaryrefslogtreecommitdiff
path: root/javascript/app/components
diff options
context:
space:
mode:
Diffstat (limited to 'javascript/app/components')
-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
17 files changed, 1352 insertions, 0 deletions
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');
+ }
+ }
+});