From cf2c56c7061b7ed40fdd3b40a352ddb9c9b7371f Mon Sep 17 00:00:00 2001 From: alexwl Date: Tue, 2 Oct 2018 13:17:04 +0300 Subject: Initial commit --- javascript/app/app.js | 15 + javascript/app/components/.gitkeep | 0 javascript/app/components/bottom-panel.js | 52 ++ javascript/app/components/expression-info.js | 3 + javascript/app/components/file-tree.js | 94 ++++ javascript/app/components/haskell-module.js | 492 ++++++++++++++++++ javascript/app/components/identifier-info.js | 62 +++ javascript/app/components/identifier-name.js | 60 +++ javascript/app/components/infinite-list.js | 50 ++ javascript/app/components/info-window.js | 144 ++++++ .../app/components/input-with-autocomplete.js | 131 +++++ javascript/app/components/instance-info.js | 21 + javascript/app/components/paginated-list.js | 48 ++ javascript/app/components/resizable-panel.js | 72 +++ javascript/app/components/text-file.js | 67 +++ javascript/app/components/type-component.js | 29 ++ javascript/app/components/type-signature-text.js | 4 + javascript/app/components/type-signature.js | 23 + javascript/app/controllers/application.js | 21 + javascript/app/controllers/package.js | 24 + javascript/app/controllers/package/index.js | 14 + javascript/app/controllers/package/search.js | 15 + javascript/app/controllers/package/show/file.js | 9 + javascript/app/controllers/packages.js | 12 + javascript/app/helpers/.gitkeep | 0 javascript/app/index.html | 22 + javascript/app/router.js | 20 + javascript/app/routes/application.js | 3 + javascript/app/routes/package.js | 38 ++ javascript/app/routes/package/index.js | 7 + javascript/app/routes/package/search.js | 36 ++ javascript/app/routes/package/show/file.js | 27 + javascript/app/routes/package/show/index.js | 7 + javascript/app/routes/packages.js | 16 + javascript/app/services/settings.js | 21 + javascript/app/services/store.js | 141 ++++++ javascript/app/styles/app.scss | 555 +++++++++++++++++++++ javascript/app/templates/application.hbs | 26 + javascript/app/templates/bad-url.hbs | 6 + javascript/app/templates/components/.gitkeep | 0 .../app/templates/components/bottom-panel.hbs | 9 + .../app/templates/components/expression-info.hbs | 8 + javascript/app/templates/components/file-tree.hbs | 8 + .../app/templates/components/haskell-module.hbs | 57 +++ .../app/templates/components/identifier-info.hbs | 39 ++ .../app/templates/components/identifier-name.hbs | 12 + .../app/templates/components/infinite-list.hbs | 3 + .../app/templates/components/info-window.hbs | 12 + .../components/input-with-autocomplete.hbs | 19 + .../app/templates/components/instance-info.hbs | 10 + .../app/templates/components/paginated-list.hbs | 17 + .../app/templates/components/resizable-panel.hbs | 5 + javascript/app/templates/components/text-file.hbs | 3 + .../app/templates/components/type-component.hbs | 3 + .../templates/components/type-signature-text.hbs | 2 + .../app/templates/components/type-signature.hbs | 10 + javascript/app/templates/package.hbs | 57 +++ javascript/app/templates/package/index.hbs | 12 + javascript/app/templates/package/search.hbs | 21 + javascript/app/templates/package/show.hbs | 1 + javascript/app/templates/package/show/file.hbs | 16 + javascript/app/templates/packages.hbs | 26 + javascript/app/utils/api-urls.js | 29 ++ javascript/app/utils/color-themes.js | 188 +++++++ javascript/app/utils/go-to-definition.js | 95 ++++ javascript/app/utils/line-selection.js | 111 +++++ 66 files changed, 3160 insertions(+) create mode 100644 javascript/app/app.js create mode 100644 javascript/app/components/.gitkeep create mode 100644 javascript/app/components/bottom-panel.js create mode 100644 javascript/app/components/expression-info.js create mode 100644 javascript/app/components/file-tree.js create mode 100644 javascript/app/components/haskell-module.js create mode 100644 javascript/app/components/identifier-info.js create mode 100644 javascript/app/components/identifier-name.js create mode 100644 javascript/app/components/infinite-list.js create mode 100644 javascript/app/components/info-window.js create mode 100644 javascript/app/components/input-with-autocomplete.js create mode 100644 javascript/app/components/instance-info.js create mode 100644 javascript/app/components/paginated-list.js create mode 100644 javascript/app/components/resizable-panel.js create mode 100644 javascript/app/components/text-file.js create mode 100644 javascript/app/components/type-component.js create mode 100644 javascript/app/components/type-signature-text.js create mode 100644 javascript/app/components/type-signature.js create mode 100644 javascript/app/controllers/application.js create mode 100644 javascript/app/controllers/package.js create mode 100644 javascript/app/controllers/package/index.js create mode 100644 javascript/app/controllers/package/search.js create mode 100644 javascript/app/controllers/package/show/file.js create mode 100644 javascript/app/controllers/packages.js create mode 100644 javascript/app/helpers/.gitkeep create mode 100644 javascript/app/index.html create mode 100644 javascript/app/router.js create mode 100644 javascript/app/routes/application.js create mode 100644 javascript/app/routes/package.js create mode 100644 javascript/app/routes/package/index.js create mode 100644 javascript/app/routes/package/search.js create mode 100644 javascript/app/routes/package/show/file.js create mode 100644 javascript/app/routes/package/show/index.js create mode 100644 javascript/app/routes/packages.js create mode 100644 javascript/app/services/settings.js create mode 100644 javascript/app/services/store.js create mode 100644 javascript/app/styles/app.scss create mode 100644 javascript/app/templates/application.hbs create mode 100644 javascript/app/templates/bad-url.hbs create mode 100644 javascript/app/templates/components/.gitkeep create mode 100644 javascript/app/templates/components/bottom-panel.hbs create mode 100644 javascript/app/templates/components/expression-info.hbs create mode 100644 javascript/app/templates/components/file-tree.hbs create mode 100644 javascript/app/templates/components/haskell-module.hbs create mode 100644 javascript/app/templates/components/identifier-info.hbs create mode 100644 javascript/app/templates/components/identifier-name.hbs create mode 100644 javascript/app/templates/components/infinite-list.hbs create mode 100644 javascript/app/templates/components/info-window.hbs create mode 100644 javascript/app/templates/components/input-with-autocomplete.hbs create mode 100644 javascript/app/templates/components/instance-info.hbs create mode 100644 javascript/app/templates/components/paginated-list.hbs create mode 100644 javascript/app/templates/components/resizable-panel.hbs create mode 100644 javascript/app/templates/components/text-file.hbs create mode 100644 javascript/app/templates/components/type-component.hbs create mode 100644 javascript/app/templates/components/type-signature-text.hbs create mode 100644 javascript/app/templates/components/type-signature.hbs create mode 100644 javascript/app/templates/package.hbs create mode 100644 javascript/app/templates/package/index.hbs create mode 100644 javascript/app/templates/package/search.hbs create mode 100644 javascript/app/templates/package/show.hbs create mode 100644 javascript/app/templates/package/show/file.hbs create mode 100644 javascript/app/templates/packages.hbs create mode 100644 javascript/app/utils/api-urls.js create mode 100644 javascript/app/utils/color-themes.js create mode 100644 javascript/app/utils/go-to-definition.js create mode 100644 javascript/app/utils/line-selection.js (limited to 'javascript/app') 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 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 : + //abc... + //... + if(startNode.nodeName === "#text") { + const parent = startNode.parentNode;// + 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;// 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') ? ">" : "<"; + }), + 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 { '"': '"', '&': '&', '<': '<', '>': '>' }[a]; + }); +} + +function addLineNumbers (text) { + const start = ""; + const end = "
"; + let lineNumber = 0; + const lines = text.split("\n").map((line) => { + lineNumber ++; + const lineNumberHtml = ""+lineNumber+""; + const lineContentHtml = ""+escapeHtml(line)+""; + return ""+ lineNumberHtml + lineContentHtml + ""; + }).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 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 @@ + + + + + + Haskell code explorer + + + + {{content-for 'head'}} + + + {{content-for 'head-footer'}} + + + {{content-for 'body'}} + + + {{content-for 'body-footer'}} + + + 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 @@ +
+
+
+ +
+ {{#link-to 'packages'}}Haskell packages{{/link-to}} + About + + {{#each themes as |theme|}} + {{#radio-button + value=theme.id + groupValue=currentTheme + changed="themeChanged"}} + {{theme.name}} + {{/radio-button}} + {{/each}} + +
+
+
+
+ {{outlet}} +
+
+
+
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 @@ +
+
+

Not found

+
Main page
+
+
diff --git a/javascript/app/templates/components/.gitkeep b/javascript/app/templates/components/.gitkeep new file mode 100644 index 0000000..e69de29 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 @@ +
+ {{yield "header"}} + + Close + +
+
+ {{yield "body"}} +
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 @@ +
+ {{#each expressions as |expression|}} +
+ {{expression.sourceCode}} +
:: {{type-signature type=expression.info.exprType identifiers=identifiers currentLineNumber=currentLineNumber}} +
+ {{/each}} +
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 @@ +
+
+ {{input class="form-control" value=query placeholder="Filename"}}Hide +
+
+
+
+
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 @@ +
+{{#if declarations}} +
+
+
+ {{input class="form-control" value=query placeholder="Identifier"}}{{showDeclarationsLabel}} +
+
+ {{#if showDeclarations}} +
+ +
+ {{/if}} +
+{{/if}} +{{#info-window + targetElement=selectedIdentifier + isHoveredOverIdentifier=isHoveredOverIdentifier + hasSelectedExpression=hasSelectedExpression + containerElementId="right-panel" as |section|}} + {{#if (eq section "header")}} + {{#if hasSelectedExpression}} +
Selected expressions
+ {{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)}} +
+ {{#if identifierInfo}} + {{#if isNaughtyRecSel}} + This record selector can never be called because its type mentions a type variable that isn't in the result type of the constructor + {{else}} + {{type-signature + type=identifierInfo.idType + identifiers=identifiers + currentLineNumber=currentLineNumber}} + {{/if}} + {{/if}} + {{#if (and identifierInfo identifierOccurrence identifierOccurrence.idOccType)}} +
+ {{/if}} + {{#if (and identifierOccurrence identifierOccurrence.idOccType)}} + {{type-signature + type=identifierOccurrence.idOccType + identifiers=identifiers + currentLineNumber=currentLineNumber}} + {{/if}} +
+ {{{identifierInfo.doc}}} + {{{downloadedDocumentation}}} +
+ {{#if identifierOccurrence.instanceResolution}} +
+ {{/if}} +
+ {{#if identifierOccurrence.instanceResolution}} + {{instance-info + instance=identifierOccurrence.instanceResolution + identifiers=identifiers + nestedLevel=0 + currentLineNumber=currentLineNumber}} + {{/if}} +
+
+{{/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 @@ +
+ {{#if identifierInfo.demangledOccName}}{{identifierInfo.demangledOccName}}{{else}}{{name}}{{/if}} + {{#unless isBinder}} + {{#if location}} + {{location}} + Go to definition + {{/if}} + {{/unless}} + {{#if isExternalIdentifier}} + Find references + {{/if}} +
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 @@ +
+
+ + {{yield "header"}} +
+
+ {{yield "body"}} +
+
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 @@ +
+ {{input class="form-control search-input" value=query placeholder=placeholder}} +
+ {{# if items}} +
    + {{#each items as |item index|}} +
  • +
    + {{yield item}} +
    +
  • + {{/each}} +
+ {{/if}} +
+
+ +
+
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}} +
+ + instance {{type-signature type=instance.instanceType identifiers=identifiers currentLineNumber=currentLineNumber noExpand=true}} + (Go to definition) + {{#each instance.instances as |inst|}} + {{instance-info instance=inst identifiers=identifiers nestedLevel=nextNestedLevel currentLineNumber=currentLineNumber}} + {{/each}} +
+{{/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 @@ +
+ Found {{total}} + {{#if (or next prev)}} +   +   + + {{#if first}}{{/if}} + {{#if prev}}{{/if}} + {{firstItemOnPage}} - {{lastItemOnPage}} + {{#if next}}{{/if}} + {{#if last}}{{/if}} + + {{/if}} +
+
+ {{yield items}} +
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 @@ +
{{yield (action "hide")}} +
+ {{{hideButtonLabel}}} +
+
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 @@ +
+{{{html}}} +
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}} +{{occName}} +{{#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")}}{{typeComponent.contents}}{{else}}{{type-component occName=typeComponent.name internalId=typeComponent.internalId identifiers=identifiers currentLineNumber=currentLineNumber}}{{/if}}{{/each}} +{{#unless noExpand}} + {{#if type.componentsExpanded}} +
+ +
+ {{/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 @@ +
+
+ {{#link-to 'package' model}}{{model.id}}{{/link-to}} + + {{#input-with-autocomplete + onSubmit=(action 'searchIdentifier') + createSearchUrlFunction=createSearchUrlFunction + maxItems=10 + selectItem=(action 'showIdentifier') + placeholder="Identifier" as |identifier|}} + {{identifier.demangledOccName}} :: {{type-signature-text components=identifier.idType.components}} +
+ {{#if identifier.locationInfo.modulePath}} + {{identifier.locationInfo.modulePath}} + {{else}} + {{identifier.locationInfo.moduleName}} + {{/if}} +
+ {{/input-with-autocomplete}} +
+ {{#if currentFile}} + + {{currentFile}} + + {{/if}} +
+
+ {{#resizable-panel class="left-panel" alsoResizeElementId="#right-panel" as |hide|}} +
+ {{file-tree directoryTree=model.directoryTree openFile="openFile" currentFile=currentFile packageId=model.id hide=hide}} +
+ {{/resizable-panel}} +
+
+ {{outlet}} +
+ {{#bottom-panel visible=bottomPanelVisible topPanelElementId="#file-container" containerElementId="#right-panel" as |section|}} + {{#if (eq section "header")}} + References to {{occName}} in {{packageId}} + {{else}} + {{#paginated-list url=referencesUrl as |files|}} + + {{/paginated-list}} + {{/if}} + {{/bottom-panel}} +
+
+
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 @@ +
+
+ {{input class="form-control" type="text" value=query placeholder="Module name"}} +
+
    + {{#each modulesFiltered as |module|}} +
  • + {{module}} +
  • + {{/each}} +
+
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 @@ +
+ Query : {{model.query}} +
+
+ {{#paginated-list url=model.url as |identifiers|}} +
    + {{#each identifiers as |identifier|}} +
  • + {{identifier.demangledOccName}} :: {{type-signature-text components=identifier.idType.components}} + +
    + {{#if identifier.locationInfo.modulePath}} + Defined in {{identifier.locationInfo.modulePath}} + {{/if}} +
    +
    {{{identifier.doc}}}
    +
  • + {{/each}} +
+ {{/paginated-list}} +
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 @@ +
+
+ +
+ {{input class="form-control" type="text" value=query placeholder="Package name"}} + Number of packages : {{packages.length}} +
+
+
+
    + {{#infinite-list containerElementId="packages" elements=packages perPage=80 as |package|}} +
  • + {{#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}} +
  • + {{/infinite-list}} +
+
+
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)); //... + if(number >= startLine && number <= endLine) { + lineElement.classList.add('highlighted-line'); + } else { + lineElement.classList.remove('highlighted-line'); + } + }); +} + +export { + initializeLineSelection,highlightLines,highlightSelectedLines +} -- cgit v1.2.3