Initial commit

This commit is contained in:
alexwl 2018-10-02 13:17:04 +03:00
commit cf2c56c706
158 changed files with 34008 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
dist
dist-*
.stack-work/
.haskell-code-explorer/
.cabal-sandbox/
cabal.sandbox.config
cabal.config
log/
tmp/
TAGS
\#*\#
.\#*

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2018 Alexey Kiryushin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

116
README.md Normal file
View File

@ -0,0 +1,116 @@
# Haskell code explorer
Haskell code explorer is a web application for exploring and understanding Haskell codebases. It provides IDE-like code intelligence features such as types and documentation on hover, "go to definition", "find references" and semantic highlighting.
Examples :
- [https://haskell-code-explorer.mfix.io/package/stm-2.4.5.0/show/Control/Concurrent/STM/TQueue.hs#L87](https://haskell-code-explorer.mfix.io/package/stm-2.4.5.0/show/Control/Concurrent/STM/TQueue.hs#L87)
- [https://haskell-code-explorer.mfix.io/package/async-2.1.1.1/show/Control/Concurrent/Async.hs#L251](https://haskell-code-explorer.mfix.io/package/async-2.1.1.1/show/Control/Concurrent/Async.hs#L251)
- [https://haskell-code-explorer.mfix.io/package/haxl-0.5.1.0/show/Haxl/Core/Monad.hs#L569](https://haskell-code-explorer.mfix.io/package/haxl-0.5.1.0/show/Haxl/Core/Monad.hs#L569)
![Haskell code explorer](https://haskell-code-explorer.mfix.io/screenshot.png)
Haskell code explorer consists of an indexer, an HTTP server, and a JavaScript application. The indexer uses GHC API to create a data structure that contains detailed information about the source code of a Cabal package. The HTTP server reads that data structure into memory and responds to HTTP requests from the JavaScript application.
## Motivation
Reading and understanding code is an essential part of the software development process. Understanding code in any statically typed language is much easier when code intelligence features (types on hover, go-to-definition, etc.) are available. Code intelligence for Haskell is especially useful because types are informative and precise (thanks to Haskell's purity and global type inference).
## Features
* Types (actual type, instantiated type, instance resolution tree) and documentation on hover. Types are interactive : left-click on type constructor -> go to definition, right click on type constructor -> show kind signature.<br />
![Hover](https://haskell-code-explorer.mfix.io/hover.png)
* Go to definition
* Find references
* Type of each expression<br />
![Expression](https://haskell-code-explorer.mfix.io/expressions.png)<br />
Select a piece of text to get the type of each Haskell expression inside the selection.
* Semantic highlighting
## Installation
```bash
git clone https://github.com/alexwl/haskell-code-explorer
cd haskell-code-explorer
```
To build Haskell code explorer Stack ([https://docs.haskellstack.org/en/stable/README/](https://docs.haskellstack.org/en/stable/README/)) is needed.
At the moment Haskell code explorer supports GHC 8.2.2 and 8.0.2.
For GHC 8.2.2:
```bash
stack install
```
For GHC 8.0.2:
```bash
stack --stack-yaml=stack-8.0.2.yaml install
```
## Indexing source code of a Cabal package
`haskell-code-indexer` executable is responsible for indexing packages (by default index is saved to `.haskell-code-explorer` directory).
`haskell-code-indexer` requires globally installed GHC and cabal-install (`cabal`). The reason for this is that `haskell-code-indexer` uses `cabal-helper` library [https://hackage.haskell.org/package/cabal-helper](https://hackage.haskell.org/package/cabal-helper) to get package build information. `cabal-helper` builds (at runtime) an executable linked against a version of Cabal library that was used to configure the package.
If there is no globally installed GHC on the system, then it is possible to use `stack exec` command ([https://docs.haskellstack.org/en/stable/GUIDE/#exec](https://docs.haskellstack.org/en/stable/GUIDE/#exec)) that adds a path to GHC binaries installed by Stack to `PATH` environment variable :
```bash
stack --resolver=lts-11.3 exec haskell-code-indexer -- INDEXER_OPTIONS
```
### Examples :
Show all indexer options:
```bash
haskell-code-indexer -h
```
Index package :
```bash
haskell-code-indexer --package PATH
```
Index package with specific dist directory :
```bash
haskell-code-indexer --package PATH --dist dist-newstyle
```
Index package built by Stack :
```bash
haskell-code-indexer --package PATH --dist $(stack path --dist-dir)
```
## Starting HTTP Server
`haskell-code-server` executable reads the package index created by `haskell-code-indexer` and starts the HTTP server. The HTTP server responds to API requests and serves static assets (JavaScript files that are in `haskell-code-explorer/javascript/release` directory).
### Examples :
Show all server options :
```bash
haskell-code-server -h
```
Load the indexed package and start the server :
```bash
haskell-code-server --package PATH --port 8080 --js-path haskell-code-explorer/javascript/release
```
Load multiple indexed packages and start the server :
```bash
haskell-code-server --package PATH1 --package PATH2 --package PATH3 --port 8080 --js-path haskell-code-explorer/javascript/release
```
Open [http://localhost:8080](http://localhost:8080) in a browser to explore source code of the package.

2
Setup.hs Normal file
View File

@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain

208
app/Indexer.hs Normal file
View File

@ -0,0 +1,208 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Codec.Compression.GZip(compress)
import Control.Exception (SomeException, handle)
import Control.Monad (when)
import Control.Monad.Logger (LogLevel(..), runLoggingT)
import qualified Data.Aeson as A
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Lazy.Char8 as BSC
import qualified Data.HashMap.Strict as HM
import Data.Maybe (fromMaybe)
import Data.Semigroup ((<>))
import qualified Data.Serialize as S
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Time (getZonedTime)
import Data.Version (Version(..),showVersion)
import HaskellCodeExplorer.PackageInfo (createPackageInfo)
import qualified HaskellCodeExplorer.Types as HCE
import Network.URI.Encode (encode)
import Options.Applicative
( Parser
, (<|>)
, execParser
, flag
, fullDesc
, help
, helper
, info
, long
, many
, metavar
, optional
, progDesc
, short
, strOption
)
import Paths_haskell_code_explorer as HSE (version)
import System.Directory (createDirectoryIfMissing)
import System.Exit (ExitCode(..), exitWith)
import System.FilePath ((</>))
import System.Log.FastLogger
( LoggerSet
, ToLogStr(..)
, defaultBufSize
, fromLogStr
, newFileLoggerSet
, newStdoutLoggerSet
, pushLogStrLn
, rmLoggerSet
)
data IndexerConfig = IndexerConfig
{ configPackageDirectoryPath :: FilePath
, configPackageDistDirRelativePath :: Maybe FilePath
, configOutputDirectoryName :: Maybe String
, configLog :: !HCE.Log
, configMinLogLevel :: !LogLevel
, configSourceCodePreprocessing :: !HCE.SourceCodePreprocessing
, configCompression :: !Compression
, configGhcOptions :: [String]
, configIgnoreDirectories :: [String]
} deriving (Show, Eq)
data Compression
= Gzip
| NoCompression
deriving (Show, Eq)
#if MIN_VERSION_GLASGOW_HASKELL(8,2,2,0)
ghcVersion :: Version
ghcVersion = Version {versionBranch = [8, 2, 2, 0], versionTags = []}
#else
ghcVersion :: Version
ghcVersion = Version {versionBranch = [8, 0, 2, 0], versionTags = []}
#endif
versionInfo :: String
versionInfo =
"haskell-code-indexer version " ++
showVersion version ++ ", GHC version " ++ showVersion ghcVersion
main :: IO ()
main = do
let description =
"haskell-code-indexer collects and saves information about the source code of a Cabal package. " ++
versionInfo
config <-
execParser $
info (helper <*> configParser) (fullDesc <> progDesc description)
loggerSet <-
case configLog config of
HCE.ToFile logfile -> newFileLoggerSet defaultBufSize logfile
HCE.StdOut -> newStdoutLoggerSet defaultBufSize
let minLogLevel = configMinLogLevel config
logger loggerSet minLogLevel LevelInfo versionInfo
logger loggerSet minLogLevel LevelDebug $ show config
handle
(\(e :: SomeException) -> do
logger loggerSet minLogLevel LevelError (show e)
rmLoggerSet loggerSet
exitWith (ExitFailure 1)) $ do
packageInfo <-
runLoggingT
(createPackageInfo
(configPackageDirectoryPath config)
(configPackageDistDirRelativePath config)
(configSourceCodePreprocessing config)
(configGhcOptions config)
(configIgnoreDirectories config))
(\_loc _source level msg -> logger loggerSet minLogLevel level msg)
let outputDir =
configPackageDirectoryPath config </>
fromMaybe
HCE.defaultOutputDirectoryName
(configOutputDirectoryName config)
createDirectoryIfMissing False outputDir
logger loggerSet minLogLevel LevelDebug $ "Output directory : " ++ outputDir
BS.writeFile
(outputDir </> HCE.packageInfoBinaryFileName)
(S.encode $ HCE.toCompactPackageInfo packageInfo)
mapM_
(\(HCE.HaskellModulePath path, modInfo) ->
let (compressFunction, compressExtension) =
case configCompression config of
Gzip -> (compress, ".gz")
NoCompression -> (id, "")
filePath =
outputDir </>
(encode (T.unpack path) ++ ".json" ++ compressExtension)
in BSL.writeFile filePath . compressFunction . A.encode $ modInfo) .
HM.toList $
HCE.moduleMap (packageInfo :: HCE.PackageInfo HCE.ModuleInfo)
BSL.writeFile
(outputDir </> HCE.packageInfoJsonFileName)
(A.encode packageInfo)
BSL.writeFile (outputDir </> "version.txt") (BSC.pack $ showVersion version)
logger loggerSet minLogLevel LevelInfo ("Finished" :: T.Text)
rmLoggerSet loggerSet
configParser :: Parser IndexerConfig
configParser =
IndexerConfig <$>
strOption
(long "package" <> short 'p' <> metavar "PATH" <>
help "Path to a Cabal package") <*>
optional
(strOption
(long "dist" <> metavar "RELATIVE_PATH" <>
help "Relative path to a dist directory")) <*>
optional
(strOption
(long "output" <> metavar "DIRECTORY_NAME" <>
help "Output directory (default is '.haskell-code-explorer')")) <*>
(pure HCE.StdOut <|>
(HCE.ToFile <$>
strOption
(long "logfile" <> metavar "PATH" <>
help "Path to a log file (by default log is written to stdout)"))) <*>
flag
LevelInfo
LevelDebug
(long "verbose" <> short 'v' <> help "Write debug information to a log") <*>
flag
HCE.AfterPreprocessing
HCE.BeforePreprocessing
(long "before-preprocessing" <>
help
"Index source code before preprocessor pass (by default source code after preprocessing is indexed)") <*>
flag
Gzip
NoCompression
(long "no-compression" <>
help
"Do not compress json files (by default json files are compressed using gzip)") <*>
many
(strOption
(long "ghc" <> metavar "OPTIONS" <> help "Command-line options for GHC")) <*>
many
(strOption
(long "ignore" <> metavar "DIRECTORY_NAME" <>
help "Directories to ignore (e.g. node_modules)"))
logger :: ToLogStr msg => LoggerSet -> LogLevel -> LogLevel -> msg -> IO ()
logger loggerSet minLogLevel logLevel msg =
when (logLevel >= minLogLevel) $ do
time <- getZonedTime
let showLogLevel :: LogLevel -> T.Text
showLogLevel LevelDebug = "[debug]"
showLogLevel LevelInfo = "[info]"
showLogLevel LevelWarn = "[warn]"
showLogLevel LevelError = "[error]"
showLogLevel (LevelOther t) = T.concat ["[",t,"]"]
text =
T.concat
[ T.pack $ show time
, " : "
, showLogLevel logLevel
, " "
, TE.decodeUtf8 . fromLogStr . toLogStr $ msg
]
pushLogStrLn loggerSet $ toLogStr text

1014
app/Server.hs Normal file

File diff suppressed because it is too large Load Diff

137
haskell-code-explorer.cabal Normal file
View File

@ -0,0 +1,137 @@
name: haskell-code-explorer
version: 0.1.0.0
synopsis: Web application for exploring and understanding Haskell codebases
Category: GHC,Web
description: Please see README.md
license: MIT
license-file: LICENSE
author: Alexey Kiryushin
maintainer: alexey.a.kiryushin@gmail.com
build-type: Simple
cabal-version: >=1.20
library
default-language: Haskell2010
exposed-modules: HaskellCodeExplorer.PackageInfo
, HaskellCodeExplorer.ModuleInfo
, HaskellCodeExplorer.Types
, HaskellCodeExplorer.Preprocessor
, HaskellCodeExplorer.GhcUtils
, HaskellCodeExplorer.AST.RenamedSource
, HaskellCodeExplorer.AST.TypecheckedSource
hs-source-dirs: src
ghc-options: -Wall -O2 -funbox-strict-fields
build-depends: IntervalMap
, aeson
, attoparsec
, base
, blaze-html
, deepseq
, bytestring
, cabal-helper
, cereal
, containers
, directory
, directory-tree
, filemanip
, filepath
, ghc
, ghc-paths
, hashable
, haddock-library
, mtl
, process
, syb
, uniplate
, text
, unordered-containers
, vector
, fast-logger
, monad-logger
, extra
executable haskell-code-indexer
main-is: Indexer.hs
ghc-options: -Wall -rtsopts -O2 -funbox-strict-fields
hs-source-dirs: app
build-depends: IntervalMap
, aeson
, base
, bytestring
, cereal
, directory
, filepath
, optparse-applicative
, text
, time
, unordered-containers
, uri-encode
, fast-logger
, monad-logger
, zlib
, haskell-code-explorer
default-language: Haskell2010
executable haskell-code-server
main-is: Server.hs
ghc-options: -Wall -O2 -rtsopts -funbox-strict-fields -threaded
hs-source-dirs: app,src
other-modules: HaskellCodeExplorer.Types
build-depends: IntervalMap
, aeson
, base
, bytestring
, cereal
, containers
, deepseq
, directory
, filemanip
, filepath
, blaze-html
, text
, unordered-containers
, hashable
, vector
, wai
, syb
, haddock-library
, servant
, servant-server
, mime-types
, mtl
, wai-extra
, wai-middleware-static
, warp
, http-types
, http-api-data
, fast-logger
, monad-logger
, optparse-applicative
, data-default
, pagination
if impl(ghc >= 8.4.3)
build-depends: ghc-compact
test-suite test
default-language: Haskell2010
type: exitcode-stdio-1.0
ghc-options: -Wall
hs-source-dirs: test
main-is: Main.hs
build-depends: IntervalMap
, QuickCheck
, attoparsec
, base
, bytestring
, containers
, directory
, filepath
, hspec
, text
, uniplate
, unordered-containers
, monad-logger
, process
, vector
, syb
, haskell-code-explorer

9
javascript/.ember-cli Normal file
View File

@ -0,0 +1,9 @@
{
/**
Ember CLI sends analytics information by default. The data is completely
anonymous, but there are times when you might want to disable this behavior.
Setting `disableAnalytics` to true will prevent any data from being sent.
*/
"disableAnalytics": true
}

15
javascript/.eslintrc.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 2017,
sourceType: 'module'
},
extends: 'eslint:recommended',
env: {
browser: true
},
rules: {
'no-console':0,
"no-useless-escape": 0
}
};

15
javascript/.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
# compiled output
/dist
/tmp
# dependencies
/node_modules
/bower_components
# misc
/.sass-cache
/connect.lock
/coverage/*
/libpeerconnection.log
npm-debug.log
testem.log

View File

@ -0,0 +1,3 @@
{
"ignore_dirs": ["tmp", "dist"]
}

26
javascript/README.md Normal file
View File

@ -0,0 +1,26 @@
# haskell-code-explorer
## Prerequisites
* [Node.js](https://nodejs.org/) (with NPM)
* [Ember CLI](https://ember-cli.com/)
* [bower](https://bower.io/)
## Installation
* `cd haskell-code-explorer/javascript`
* `npm install`
* `bower install`
## Running / Development
* Start haskell-code-server on port 8080
* `ember server --port=4200 --proxy=http://localhost:8080`
* Visit your app at [http://localhost:4200](http://localhost:4200).
To test the app with real-world data :
* ember server --proxy=https://haskell-code-explorer.mfix.io/
## Running Tests
* `ember server --port=4200`
* Open [http://localhost:4200/tests](http://localhost:4200/tests) in a browser
## Building
* `ember build --environment production`

15
javascript/app/app.js Normal file
View File

@ -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;

View File

View File

@ -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);
}
}
});

View File

@ -0,0 +1,3 @@
import Ember from 'ember';
export default Ember.Component.extend({
});

View File

@ -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')();
}
}
});

View File

@ -0,0 +1,492 @@
import Ember from 'ember';
import {goToDefinition} from '../utils/go-to-definition';
import {initializeLineSelection} from '../utils/line-selection';
function compareLocations (p1,p2) {
if(p1.line === p2.line) {
if(p1.column === p2.column) {
return 0;
} else if(p1.column > p2.column) {
return 1;
} else {
return -1;
}
} else if(p1.line > p2.line) {
return 1;
} else {
return -1;
}
}
function buildSrcSpan(sourceCodeLines,start,end) {
if(sourceCodeLines[start.line] && sourceCodeLines[end.line]) {
if(start.line === end.line) {
return sourceCodeLines[start.line].slice(start.column-1,end.column-1);
} else {
const firstLine = sourceCodeLines[start.line];
let middleLines = [];
for(let i = start.line + 1; i < end.line;i ++) {
middleLines.push(sourceCodeLines[i]);
}
const lastLine = sourceCodeLines[end.line];
const minOffset = Math.min(start.column,
(middleLines.concat([lastLine]))
.map((line) => line.search(/\S/))
.reduce((min,value) => Math.min(min,value)));
return firstLine.slice(start.column-1,firstLine.length) + "\n"
+ middleLines.map((line) => line.slice(minOffset,line.length)).join("\n")
+ (middleLines.length ? "\n" : "") + lastLine.slice(minOffset,end.column-1);
}
} else {
return null;
}
}
function modifyClass(element,on) {
if(on) {
element.classList.add('highlighted-identifier');
} else {
element.classList.remove('highlighted-identifier');
}
}
function highlightIdentifiers(parentElement,identifierElement,on) {
if(identifierElement.id) {
const identifiers = Array.prototype.slice.call(parentElement.querySelectorAll("span[id='"+identifierElement.id+"']"));
identifiers.forEach((identifier) => {
modifyClass(identifier,on);
});
} else {
modifyClass(identifierElement,on);//Literal
}
}
//divident is a string
//divident may have any number of digits
function modulo(divident, divisor) {
return Array.from(divident).map(c => parseInt(c))
.reduce((acc, value) => {
return (acc * 10 + value) % divisor;
},0);
}
function isDefinedInCurrentModule(moduleName,modulePath,identifierInfo) {
return (identifierInfo.sort === "External") &&
(identifierInfo.locationInfo.modulePath === modulePath
|| identifierInfo.locationInfo.moduleName === moduleName)
}
function identifierStyle(identifierElement,
identifiers,
occurrences,
path,
colorTheme,
moduleName) {
const idOcc = occurrences[identifierElement.dataset.occurrence];
let color = colorTheme.defaultColor;
let fontWeight;
if(idOcc) {
if(idOcc.sort.tag === 'TypeId') {
color = colorTheme.typeColor;
} else if(idOcc.description === "HsLit" ||
idOcc.description === "HsOverLit"||
idOcc.description === "LitPat" ||
idOcc.description === "NPat" ||
idOcc.description === "NPlusKPat" ||
idOcc.description === "OverLit") {
color = colorTheme.literalColor;
} else {
const idInfo = identifiers[identifierElement.dataset.identifier];
if(idInfo) {
if(isDefinedInCurrentModule(moduleName,path,idInfo)) {
color = colorTheme.topLevelIdFromCurrentModule;
} else if(idInfo.sort === "Internal" && idInfo.locationInfo.tag === "ExactLocation") {
const colorNumber = modulo(identifierElement.id,colorTheme.localIdentifierColor.length);
color = colorTheme.localIdentifierColor[colorNumber];
fontWeight = "bold";
}
}
}
}
return "color:"+color+";"
+(fontWeight ? "font-weight:" + fontWeight : "")+";"
+(idOcc.isBinder ? "text-decoration:underline;" : "");
}
function initializeIdentifiers (sourceCodeContainerElement,component) {
const identifierElements = Array.prototype.slice.call(sourceCodeContainerElement.querySelectorAll("span.identifier"));
if(identifierElements.length > 0) {
const timeout = 250;//milliseconds
let timer = null;
identifierElements.forEach((identifierElement) => {
const cssText = identifierStyle(identifierElement,
component.get('identifiers'),
component.get('occurrences'),
component.get('path'),
component.get('colorTheme'),
component.get('name'));
identifierElement.style.cssText = cssText;
//go to definition
identifierElement.onmouseup = (event) => {
if(timer) {
clearTimeout(timer);
}
if(!window.getSelection().isCollapsed) {
return;
}
const identifierInfo = component.get('identifiers')[identifierElement.dataset.identifier];
const idOccurrenceInfo = component.get('occurrences')[identifierElement.dataset.occurrence];
const currentLineNumber = parseInt(identifierElement.parentNode.dataset.line);
if(idOccurrenceInfo.sort.tag === "ModuleId") {
goToDefinition(component.get('store'),
idOccurrenceInfo.sort.contents,
event.which,
currentLineNumber);
}
else {
if(!idOccurrenceInfo.isBinder && identifierInfo
&& (event.which === 1 || event.which === 2)) {
goToDefinition(component.get('store'),
identifierInfo.locationInfo,
event.which,
currentLineNumber);
}
}
}
identifierElement.onmouseover = () => {
highlightIdentifiers(sourceCodeContainerElement,identifierElement,true);
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
Ember.run.next(component,() => {
const identifierInfo = component.get('identifiers')[identifierElement.dataset.identifier];
const identifierOccurrence = component.get('occurrences')[identifierElement.dataset.occurrence];
console.log(identifierOccurrence);
console.log(identifierInfo);
component.set('selectedIdentifier',identifierElement);
component.set('currentLineNumber',parseInt(identifierElement.parentNode.dataset.line) || 1);
component.set('identifierInfo',identifierInfo);
component.set('identifierOccurrence',identifierOccurrence);
component.set('hasSelectedExpression',false);
component.set('isHoveredOverIdentifier',true);
});
},timeout);
};
identifierElement.onmouseout = () => {
highlightIdentifiers(sourceCodeContainerElement,identifierElement,false);
if(timer) {
clearTimeout(timer);
}
timer = setTimeout (() => {
Ember.run.next(component,() => {
component.set('isHoveredOverIdentifier',false);
});
},timeout);
};
});
component.timer = timer;
}
}
function contains (node, other) {
return node === other || !!(node.compareDocumentPosition(other) & 16);
}
function initializeExpressionInfo(sourceCodeContainerElement,component) {
const lineElements = Array.prototype.slice.call(sourceCodeContainerElement.querySelectorAll("td.line-content"));
if(lineElements.length > 0) {
//Line numbers start with 1
let sourceCodeLines = [""];
lineElements.forEach((el) => {
sourceCodeLines.push(el.textContent);
});
const allowedNodeNames = ["#text","SPAN","TD"];
let isLoading = false;
let shouldWait = false;
const timeout = 400;//milliseconds
const onmouseup = function() {
Ember.run.next(() => {
if(isLoading || shouldWait) {
return;
}
shouldWait = true;
setTimeout(() => {shouldWait = false;},timeout);
component.set('hasSelectedExpression',false);
const selection = window.getSelection();
//Selection of multiple lines inside a table doesn't work in Firefox
//https://bugzilla.mozilla.org/show_bug.cgi?id=365900
if(!(selection.anchorNode && selection.focusNode)
|| !contains(sourceCodeContainerElement,selection.anchorNode)
|| !contains(sourceCodeContainerElement,selection.focusNode)
|| (allowedNodeNames.indexOf(selection.anchorNode.nodeName) === -1)
|| (allowedNodeNames.indexOf(selection.focusNode.nodeName) === -1)
|| selection.isCollapsed) {
return;
}
// Detects whether the selection is backwards
const detectionRange = document.createRange();
detectionRange.setStart(selection.anchorNode, selection.anchorOffset);
detectionRange.setEnd(selection.focusNode, selection.focusOffset);
const isBackward = detectionRange.collapsed;
let startNode,startNodeOffset,endNode,endNodeOffset;
if(isBackward) {
startNode = selection.focusNode;
startNodeOffset = selection.focusOffset;
endNode = selection.anchorNode;
endNodeOffset = selection.anchorOffset;
} else {
startNode = selection.anchorNode;
startNodeOffset = selection.anchorOffset;
endNode = selection.focusNode;
endNodeOffset = selection.focusOffset;
}
let lineStart,columnStart,lineEnd,columnEnd;
let infoWindowTargetElement;
//HTML inside source code container :
//<tr><td><span data-start="1" date-end="3">abc</span><span>...</span></td></tr>
//<tr>...</tr>
if(startNode.nodeName === "#text") {
const parent = startNode.parentNode;//<span>
columnStart = parseInt(parent.dataset.start) + startNodeOffset;
lineStart = parseInt(parent.parentNode.dataset.line);
if(startNodeOffset === startNode.textContent.length && parent.nextSibling === null) {
const tr = startNode.parentNode.parentNode.parentNode;// span -> td -> tr
//Skipping empty lines
let nextLine = tr.nextSibling;
while(nextLine.children[1].textContent === "") {
nextLine = nextLine.nextSibling;
}
infoWindowTargetElement = nextLine.children[1].children[0];
} else {
if(!(startNodeOffset === 0) && (parent.nextSibling)) {
infoWindowTargetElement = parent.nextSibling;
} else {
infoWindowTargetElement = parent;
}
}
} else if(startNode.nodeName === "SPAN") {
columnStart = 1;
lineStart = parseInt(startNode.parentNode.dataset.line);
const tr = startNode.parentNode.parentNode; // td -> tr
let nextLine = tr.nextSibling;
while(nextLine.children[1].textContent === "") {
nextLine = nextLine.nextSibling;
}
infoWindowTargetElement = nextLine.children[1].children[0];
} else if(startNode.nodeName === "TD") {
if(startNodeOffset > 0) {
const child = startNode.children[startNodeOffset-1];
columnStart = parseInt(child.dataset.start);
} else {
columnStart = 1;
}
lineStart = parseInt(startNode.id.slice(2));
infoWindowTargetElement = startNode.children[0];
}
if(endNode.nodeName === "#text") {
columnEnd = parseInt(endNode.parentNode.dataset.start) + endNodeOffset;
lineEnd = parseInt(endNode.parentNode.parentNode.dataset.line);
} else if(endNode.nodeName === "SPAN") {
columnEnd = 1;
lineEnd = parseInt(endNode.parentNode.dataset.line);
} else if(endNode.nodeName === "TD"){
if(endNodeOffset > 0) {
const child = endNode.children[endNodeOffset-1];
columnEnd = parseInt(child.dataset.start);
} else {
columnEnd = 1;
}
lineEnd = parseInt(endNode.id.slice(2));
}
const loadExprPromise = component.get('store').loadExpressions(
component.get('packageId'),
component.get('path'),
lineStart,
columnStart,
lineEnd,
columnEnd);
isLoading = true;
loadExprPromise.then((expressions) => {
Ember.run.next(() => {
if(expressions && expressions.length > 0) {
expressions.sort(function(expr1,expr2) {
if( compareLocations(expr1.srcSpan.start,expr2.srcSpan.start) <= 0
&& compareLocations(expr1.srcSpan.end,expr2.srcSpan.end) >= 0 ) {
return -1;
} else {
return 1;
}
});
const expressionsWithSourceCode = expressions.reduce((result,expression) => {
const object = Ember.copy(expression);
const srcSpan = buildSrcSpan(sourceCodeLines,
expression.srcSpan.start,
expression.srcSpan.end);
if(srcSpan) {
object.sourceCode = srcSpan;
return result.concat(object);
} else {
return result;
}
},[]);
if(expressionsWithSourceCode.length > 0) {
component.set('selectedIdentifier',infoWindowTargetElement);
component.set('expressions',expressionsWithSourceCode);
component.set('currentLineNumber',parseInt(infoWindowTargetElement.parentNode.dataset.line) || 1);
component.set('hasSelectedExpression',true);
}
}
isLoading = false;
});
});
});
};
sourceCodeContainerElement.addEventListener('mouseup',onmouseup);
component._onmouseup = onmouseup;
}
}
export default Ember.Component.extend({
store : Ember.inject.service('store'),
selectedIdentifier : null,
isHoveredOverIdentifier : false,
hasSelectedExpression : false,
showDeclarations : true,
showDeclarationsLabel : Ember.computed('showDeclarations',function () {
return this.get('showDeclarations') ? "Hide" : "Show";
}),
queryObserver : Ember.observer("query",function() {
Ember.run.debounce(this, () => {
const regExp = new RegExp(this.get('query'),"i");
const filteredDeclarations = this.get('declarations').filter((d) => d.name.search(regExp) != -1);
Ember.run.next(() => {
this.set('filteredDeclarations',filteredDeclarations);
});
}, 300);
}),
identifierLocationInfo : Ember.computed('identifierInfo','identifierOccurrence',function() {
const idOcc = this.get('identifierOccurrence');
const idInfo = this.get('identifierInfo');
if(idOcc) {
if(idOcc.sort.tag === "ModuleId") {
return idOcc.sort.contents;
} else {
if(idInfo) {
return idInfo.locationInfo;
} else {
return null;
}
}
}
}),
themeObserver : Ember.observer('colorTheme',function() {
Ember.run.next(this,() => {
this.cleanup();
this.didInsertElement();
});
}),
fileObserver : Ember.observer('path',function() {
Ember.run.next(this,() => {
this.cleanup();
this.didInsertElement();
});
}),
cleanup() {
if(this.timer) {
clearTimeout(this.timer);
}
if(this._onhashchange) {
window.removeEventListener('hashchange',this._onhashchange);
}
if(this._onkeydown) {
document.removeEventListener('keydown',this._onkeydown);
}
if(this._onkeyup) {
document.removeEventListener('keyup',this._onkeyup);
}
if(this._onmouseup) {
this.sourceCodeContainerElement.removeEventListener('mouseup',this._onmouseup);
}
this.set('selectedIdentifier',null);
this.set('isHoveredOverIdentifier',false);
this.set('hasSelectedExpression',false);
this.set('showDeclarations',true);
},
didReceiveAttrs() {
this.set('filteredDeclarations',this.get('declarations'));
},
didInsertElement() {
this._super(...arguments);
const sourceCodeContainerElement = this.element.querySelector('.source-code-container');
sourceCodeContainerElement.innerHTML = this.get('html');
this.sourceCodeContainerElement = sourceCodeContainerElement;
this.element.parentNode.scrollTop = 0;
const declarations = this.element.querySelector('.declarations-content');
this.set('query','');
if(declarations) {
declarations.scrollTop = 0;
}
Ember.run.next(this,() => {
initializeIdentifiers(sourceCodeContainerElement,this);
initializeLineSelection(sourceCodeContainerElement,this);
initializeExpressionInfo(sourceCodeContainerElement,this);
});
},
willDestroyElement() {
this.cleanup();
},
actions : {
goToLine(lineNumber) {
window.location.hash = "L"+lineNumber;
},
toggleShowDeclarations() {
this.toggleProperty('showDeclarations');
}
}
});

View File

@ -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);
}});
});
}
}
})
});

View File

@ -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);
}
}
});

View File

@ -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;
});
}
}
}
}
});

View File

@ -0,0 +1,144 @@
import Ember from 'ember';
let resizing = false;
let dragging = false;
function updatePosition(component) {
const targetElement = component.get('targetElement');
if(targetElement) {
const infoWindowHeight = component.element.offsetHeight;
const targetElementHeight = targetElement.offsetHeight;
const parent = targetElement.parentNode;//<td> element
const containerElement = document.querySelector("#" + component.get('containerElementId'));
//getBoundingClientRect() returns the smallest rectangle which contains
//the entire element, with read-only left, top, right, bottom, x, y, width,
//and height properties describing the overall border-box in pixels. Properties
//other than width and height are relative to the top-left of the *viewport*.
const targetTopViewport = targetElement.getBoundingClientRect().top;
let containerTopViewport;
if (containerElement) {
containerTopViewport = containerElement.getBoundingClientRect().top;
} else {
containerTopViewport = 0;
}
let infoWindowTop;
if(targetTopViewport < infoWindowHeight + containerTopViewport) {
//offsetTop is the number of pixels from the top of the closest relatively
//positioned parent element.
infoWindowTop = targetElement.offsetTop + parent.offsetTop
+ targetElementHeight + 10 + "px";
} else {
infoWindowTop = targetElement.offsetTop + parent.offsetTop
- infoWindowHeight + "px";
}
const infoWindowLeft = targetElement.offsetLeft + parent.offsetLeft + "px";
component.$().css({
top:infoWindowTop,
left:infoWindowLeft
});
} else {
component.set('isPinned',false);
}
}
export default Ember.Component.extend({
classNames : ["info-window-container"],
attributeBindings: ['hidden'],
isPinned : false,
isFocused: false,
didInsertElement () {
const component = this;
const $headerElement = Ember.$(component.element.querySelector(".info-window-header"));
const $contentElement = Ember.$(component.element.querySelector(".info-window-content"));
const $infoWindowElement = Ember.$(component.element.querySelector(".info-window"));
const $infoWindowContainerElement = Ember.$(component.element);
this.$headerElement = $headerElement;
this.$contentElement = $contentElement;
this.$().resizable({
handles: "n,w",
minHeight: 80,
minWidth: 400,
start: function() {
resizing = true;
},
stop: function() {
resizing = false;
},
resize : function() {
const containerHeight = $infoWindowContainerElement.height();
$infoWindowElement.css({
"height": containerHeight + 2 + "px"
});
$contentElement.css({
"max-height":(containerHeight - $headerElement.outerHeight(true)) + "px"
});
}
});
this.$().draggable({
containment:"#" + this.get('containerElementId'),
handle: $headerElement,
start: function() {
dragging = true;
},
stop: function() {
dragging = false;
}
});
},
mouseEnter () {
if(!this.get('hasSelectedExpression')) {
this.set('isFocused',true);
}
},
mouseLeave (event) {
//Workaround for a bug in Chrome
const element = document.elementFromPoint(event.clientX,event.clientY);
if(element && element.classList.contains('link')) {
return;
}
if(!resizing
&& !dragging
&& !this.get('isPinned')
&& !this.get('hasSelectedExpression')) {
this.set('isFocused',false);
}
},
hidden : Ember.computed('isHoveredOverIdentifier',
'isFocused',
'hasSelectedExpression',
'isPinned', function() {
if (this.$contentElement) {
this.$contentElement.scrollTop(0);
}
if (this.get('isPinned')
|| this.get('isFocused')
|| this.get('isHoveredOverIdentifier')
|| this.get('hasSelectedExpression')) {
return false;
} else {
return true;
}
}),
didUpdate() {
updatePosition(this);
},
actions : {
close() {
this.set('isPinned',false);
this.set('isFocused',false);
this.set('hasSelectedExpression',false);
},
pin() {
this.toggleProperty('isPinned');
}
}
});

View File

@ -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);
}
}
});

View File

@ -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;
}
}
});

View File

@ -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);
}
}
});

View File

@ -0,0 +1,72 @@
import Ember from 'ember';
function hide (component,byUser) {
component.$alsoResizeElement.css({left: 0});
component.$().css({width:0});
component.set('hidden',true);
component.$(".show-left-panel-button").show();
if(byUser) {
component.set('hiddenByUser',true);
}
}
function show (component,byUser) {
component.$alsoResizeElement.css({left: 300});
component.$().css({width:300});
component.set('hidden',false);
component.$(".show-left-panel-button").hide();
if(byUser) {
component.set('hiddenByUser',false);
}
}
export default Ember.Component.extend({
hidden:false,
hiddenByUser:false,
didInsertElement : function () {
this._super(...arguments);
Ember.run.next(this,() => {
const onresize = () => {
if(!this.get('hiddenByUser')) {
const width = window.innerWidth;
if(!this.get('hidden') && width < 700) {
hide(this,false);
} else if(this.get('hidden') && width > 700) {
show(this,false);
}
}
};
this._onresize = onresize;
window.addEventListener('resize', onresize);
const $alsoResizeElement = Ember.$(this.get('alsoResizeElementId'));
Ember.$(this.element).resizable({
maxWidth: 800,
minWidth: 200,
handles: 'e',
resize: (event,ui) => {
Ember.run.next(this,() => {
$alsoResizeElement.css({left: ui.size.width});
});
}
});
this.$alsoResizeElement = $alsoResizeElement;
});
},
hideButtonLabel : Ember.computed('hidden',function() {
return this.get('hidden') ? "&gt;" : "&lt;";
}),
willDestroyElement() {
if(this._onresize) {
window.removeEventListener('resize',this._onresize);
}
},
actions : {
hide() {
if(this.get('hidden')) {
show(this,true);
} else {
hide(this,true);
}
}
}
});

View File

@ -0,0 +1,67 @@
/* global showdown */
import Ember from 'ember';
import {initializeLineSelection} from '../utils/line-selection';
function escapeHtml(text) {
return text.replace(/[\"&<>]/g, function (a) {
return { '"': '&quot;', '&': '&amp;', '<': '&lt;', '>': '&gt;' }[a];
});
}
function addLineNumbers (text) {
const start = "<table class='source-code'><tbody>";
const end = "</tbody></table>";
let lineNumber = 0;
const lines = text.split("\n").map((line) => {
lineNumber ++;
const lineNumberHtml = "<td id='LN"+lineNumber+"' class='line-number'>"+lineNumber+"</td>";
const lineContentHtml = "<td id='LC"+lineNumber+"' class='line-content'>"+escapeHtml(line)+"</td>";
return "<tr>"+ lineNumberHtml + lineContentHtml + "</tr>";
}).join("");
return start + lines + end;
}
const markdownExtensions = ["markdown", "mdown", "mkdn", "mkd", "md"];
export default Ember.Component.extend({
isMarkdown : Ember.computed('path',function() {
const maybeExtension = this.get('path').split('.').pop();
return markdownExtensions.any((extension) => (maybeExtension === extension));
}),
html : Ember.computed('path','isMarkdown',function() {
if(this.get('isMarkdown')) {
return this.markdownConverter.makeHtml(this.get('text'));
} else {
return addLineNumbers(this.get('text'));
}
}),
init() {
this._super(...arguments);
this.markdownConverter = new showdown.Converter();
},
didInsertElement() {
const sourceCodeContainerElement = this.element.querySelector('.source-code-container');
initializeLineSelection(sourceCodeContainerElement,this);
this.element.parentNode.scrollTop = 0;
},
willDestroyElement : function () {
this.cleanup();
},
cleanup() {
if(this._onhashchange) {
window.removeEventListener('hashchange',this._onhashchange);
}
if(this._onkeydown) {
document.removeEventListener('keydown',this._onkeydown);
}
if(this._onkeyup) {
document.removeEventListener('keyup',this._onkeyup);
}
},
pathObserver : Ember.observer('path',function() {
Ember.run.next(this,() => {
this.cleanup();
this.didInsertElement();
});
})
});

View File

@ -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;
}
}
}
});

View File

@ -0,0 +1,4 @@
import Ember from 'ember';
export default Ember.Component.extend({
tagName : "span"
});

View File

@ -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');
}
}
});

View File

@ -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);
}
}
});

View File

@ -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;
}
}
});

View File

@ -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;
}
})
});

View File

@ -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;
}
}
});

View File

@ -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);
}
}
});

View File

@ -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);
})
});

View File

22
javascript/app/index.html Normal file
View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Haskell code explorer</title>
<meta name="description" content="Haskell code explorer">
<meta name="google" content="notranslate">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{content-for 'head'}}
<link rel="stylesheet" href="/assets/vendor.css">
<link rel="stylesheet" href="/assets/haskell-code-explorer.css">
{{content-for 'head-footer'}}
</head>
<body>
{{content-for 'body'}}
<script pace-src="/assets/vendor.js"></script>
<script pace-src="/assets/haskell-code-explorer.js"></script>
{{content-for 'body-footer'}}
<noscript>You need to enable JavaScript to run this app.</noscript>
</body>
</html>

20
javascript/app/router.js Normal file
View File

@ -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;

View File

@ -0,0 +1,3 @@
import Ember from 'ember';
export default Ember.Route.extend({
});

View File

@ -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");
}
}
});

View File

@ -0,0 +1,7 @@
import Ember from 'ember';
export default Ember.Route.extend({
afterModel : function (model,transition) {
transition.send("fileOpened",null);
}
});

View File

@ -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);
}
}
});

View File

@ -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);
}
}
});

View File

@ -0,0 +1,7 @@
import Ember from 'ember';
export default Ember.Route.extend({
afterModel : function (model,transition) {
transition.send("fileOpened",null);
}
});

View File

@ -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;
}
});

View File

@ -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);
}
})
});

View File

@ -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
};
});
}
});

View File

@ -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;
}

View File

@ -0,0 +1,26 @@
<div class="absolute-container">
<div class="flex-container">
<div class="header">
<img src="/assets/haskell.ico">
<div class="container">
<span class="header-item">{{#link-to 'packages'}}Haskell packages{{/link-to}}</span>
<span class="header-item"><a target="_blank" href="https://github.com/alexwl/haskell-code-explorer">About</a></span>
<span class="header-item">
{{#each themes as |theme|}}
{{#radio-button
value=theme.id
groupValue=currentTheme
changed="themeChanged"}}
<span>{{theme.name}}</span>
{{/radio-button}}
{{/each}}
</span>
</div>
</div>
<div class="content">
<div class="absolute-container">
{{outlet}}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,6 @@
<div class="container">
<div style="margin-top:10px">
<h1>Not found</h1>
<div><a href="/">Main page</a></div>
</div>
</div>

View File

@ -0,0 +1,9 @@
<div class="bottom-panel-header">
<span class="bottom-panel-header-content">{{yield "header"}}</span>
<span class="bottom-panel-header-options">
<a href="#" {{action "close"}}>Close</a>
</span>
</div>
<div class="bottom-panel-content">
{{yield "body"}}
</div>

View File

@ -0,0 +1,8 @@
<div class="expressions">
{{#each expressions as |expression|}}
<div class="expression">
<span class="source-code">{{expression.sourceCode}}</span>
<br/>:: {{type-signature type=expression.info.exprType identifiers=identifiers currentLineNumber=currentLineNumber}}
</div>
{{/each}}
</div>

View File

@ -0,0 +1,8 @@
<div class="file-tree-header">
<div class="input-group">
{{input class="form-control" value=query placeholder="Filename"}}<a class="hide-file-tree" href="#" {{action "hide"}}>Hide</a>
</div>
</div>
<div class="file-tree-content">
<div class="file-tree"></div>
</div>

View File

@ -0,0 +1,57 @@
<div class="source-code-container"></div>
{{#if declarations}}
<div class="declarations">
<div class="declarations-header">
<div class="input-group">
{{input class="form-control" value=query placeholder="Identifier"}}<a class="hide-declarations" href="#" {{action "toggleShowDeclarations"}}>{{showDeclarationsLabel}}</a>
</div>
</div>
{{#if showDeclarations}}
<div id="declarations-content" class="declarations-content">
<ul>
{{#infinite-list containerElementId="declarations-content" elements=filteredDeclarations perPage=30 as |declaration|}}
<li class="declaration">
<div class="declaration">
<a {{action "goToLine" declaration.lineNumber}} href="#"><span class="source-code-font">{{declaration.name}}{{#if declaration.declType}} :: {{type-signature-text components=declaration.declType.components}}{{/if}}</span>
</a><span>{{#unless declaration.isExported}}<i>(not exported)</i>{{/unless}}</span>
</div>
</li>
{{/infinite-list}}
</ul>
</div>
{{/if}}
</div>
{{/if}}
{{#info-window
targetElement=selectedIdentifier
isHoveredOverIdentifier=isHoveredOverIdentifier
hasSelectedExpression=hasSelectedExpression
containerElementId="right-panel" as |section|}}
{{#if (eq section "header")}}
{{#if hasSelectedExpression}}
<div>Selected expressions</div>
{{else}}
{{identifier-name
identifierInfo=identifierInfo
identifierOccurrence=identifierOccurrence
identifierElement=selectedIdentifier
currentPackageId=packageId
isBinder=identifierOccurrence.isBinder
currentLineNumber=currentLineNumber
findReferences=findReferences}}
{{/if}}
{{else}}
{{#unless hasSelectedExpression}}
{{identifier-info
identifierInfo=identifierInfo
identifierOccurrence=identifierOccurrence
identifiers=identifiers
currentLineNumber=currentLineNumber}}
{{else}}
{{expression-info
expressions=expressions
identifiers=identifiers
currentLineNumber=currentLineNumber}}
{{/unless}}
{{/if}}
{{/info-window}}

View File

@ -0,0 +1,39 @@
{{#if (or identifierInfo identifierOccurrence.idOccType)}}
<div class="identifier-info">
{{#if identifierInfo}}
{{#if isNaughtyRecSel}}
<i>This record selector can never be called because its type mentions a type variable that isn't in the result type of the constructor</i>
{{else}}
{{type-signature
type=identifierInfo.idType
identifiers=identifiers
currentLineNumber=currentLineNumber}}
{{/if}}
{{/if}}
{{#if (and identifierInfo identifierOccurrence identifierOccurrence.idOccType)}}
<div style="height:10px"></div>
{{/if}}
{{#if (and identifierOccurrence identifierOccurrence.idOccType)}}
{{type-signature
type=identifierOccurrence.idOccType
identifiers=identifiers
currentLineNumber=currentLineNumber}}
{{/if}}
<div class="documentation">
{{{identifierInfo.doc}}}
{{{downloadedDocumentation}}}
</div>
{{#if identifierOccurrence.instanceResolution}}
<div style="height:10px"></div>
{{/if}}
<div>
{{#if identifierOccurrence.instanceResolution}}
{{instance-info
instance=identifierOccurrence.instanceResolution
identifiers=identifiers
nestedLevel=0
currentLineNumber=currentLineNumber}}
{{/if}}
</div>
</div>
{{/if}}

View File

@ -0,0 +1,12 @@
<div>
<span class="source-code-font break-word identifier-menu-item" style={{style}}>{{#if identifierInfo.demangledOccName}}{{identifierInfo.demangledOccName}}{{else}}{{name}}{{/if}}</span>
{{#unless isBinder}}
{{#if location}}
<span class="identifier-menu-item">{{location}}</span>
<span class="link identifier-menu-item" onmouseup={{action "goToDefinition"}}>Go to definition</span>
{{/if}}
{{/unless}}
{{#if isExternalIdentifier}}
<span class="link identifier-menu-item" onmouseup={{action "findReferences" identifierInfo currentPackageId}}>Find references</span>
{{/if}}
</div>

View File

@ -0,0 +1,3 @@
{{#each renderedElements as |element|}}
{{yield element}}
{{/each}}

View File

@ -0,0 +1,12 @@
<div class="info-window">
<div class="info-window-header">
<div class="info-window-options">
<a href="#" {{action "pin"}}>{{#if isPinned}}Unpin{{else}}Pin{{/if}}</a>
<a href="#" {{action "close"}}>Close</a>
</div>
{{yield "header"}}
</div>
<div class="info-window-content">
{{yield "body"}}
</div>
</div>

View File

@ -0,0 +1,19 @@
<div class="input-group">
{{input class="form-control search-input" value=query placeholder=placeholder}}
<div class="autocomplete-container">
{{# if items}}
<ul class="autocomplete-items">
{{#each items as |item index|}}
<li class="{{if (eq index highlightedItemIndex) "highlighted"}}" onclick={{action "goToDefinition" item}}>
<div class="autocomplete-item">
{{yield item}}
</div>
</li>
{{/each}}
</ul>
{{/if}}
</div>
<div class="input-group-append">
<button id="search-input" class="btn btn-outline-secondary" {{action "onSubmit" query}}>Search</button>
</div>
</div>

View File

@ -0,0 +1,10 @@
{{#if instance}}
<div style={{style}}>
<span class="source-code-font">
instance {{type-signature type=instance.instanceType identifiers=identifiers currentLineNumber=currentLineNumber noExpand=true}}</span>
(<span class="link" onclick={{action "goToDefinition"}}>Go to definition</span>)
{{#each instance.instances as |inst|}}
{{instance-info instance=inst identifiers=identifiers nestedLevel=nextNestedLevel currentLineNumber=currentLineNumber}}
{{/each}}
</div>
{{/if}}

View File

@ -0,0 +1,17 @@
<div class="paginated-list-header">
<span>Found {{total}}</span>
{{#if (or next prev)}}
&nbsp;
&nbsp;
<span>
{{#if first}}<button class="btn btn-outline-secondary btn-sm" {{action "update" first.href}}>&lt;&lt;</button>{{/if}}
{{#if prev}}<button button class="btn btn-outline-secondary btn-sm" {{action "update" prev.href}}>&lt;</button>{{/if}}
{{firstItemOnPage}} - {{lastItemOnPage}}
{{#if next}}<button button class="btn btn-outline-secondary btn-sm" {{action "update" next.href}}>&gt;</button>{{/if}}
{{#if last}}<button button class="btn btn-outline-secondary btn-sm" {{action "update" last.href}}>&gt;&gt;</button>{{/if}}
</span>
{{/if}}
</div>
<div class="paginated-list-content">
{{yield items}}
</div>

View File

@ -0,0 +1,5 @@
<div class="absolute-container">{{yield (action "hide")}}
<div onclick={{action "hide"}} class="show-left-panel-button">
{{{hideButtonLabel}}}
</div>
</div>

View File

@ -0,0 +1,3 @@
<div class="source-code-container">
{{{html}}}
</div>

View File

@ -0,0 +1,3 @@
{{#if (and expanded identifierInfo)}}({{/if}}
<span class="source-code {{linkClass}}" onmouseup={{action "onmouseup"}}>{{occName}}</span>
{{#if (and expanded identifierInfo)}} :: {{type-signature type=identifierInfo.idType identifiers=identifiers noExpand=true}}){{/if}}

View File

@ -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}}

View File

@ -0,0 +1,10 @@
{{!-- No newlines to get rid of spaces between spans --}}
{{#each components as |typeComponent|}}{{#if (eq typeComponent.tag "Text")}}<span class="source-code">{{typeComponent.contents}}</span>{{else}}{{type-component occName=typeComponent.name internalId=typeComponent.internalId identifiers=identifiers currentLineNumber=currentLineNumber}}{{/if}}{{/each}}
{{#unless noExpand}}
{{#if type.componentsExpanded}}
<div style="margin-top:5px">
<button class="btn btn-outline-secondary btn-sm" {{action "toggleExpandTypeSynonyms"}}>{{expandTypeSynonymsLabel}}
</button>
</div>
{{/if}}
{{/unless}}

View File

@ -0,0 +1,57 @@
<div class="flex-container">
<div class="package-header">
<span class="package-header-package-name">{{#link-to 'package' model}}{{model.id}}{{/link-to}}</span>
<span class="package-header-input">
{{#input-with-autocomplete
onSubmit=(action 'searchIdentifier')
createSearchUrlFunction=createSearchUrlFunction
maxItems=10
selectItem=(action 'showIdentifier')
placeholder="Identifier" as |identifier|}}
<span class="source-code-font">{{identifier.demangledOccName}} :: {{type-signature-text components=identifier.idType.components}}</span>
<div class="module-name">
{{#if identifier.locationInfo.modulePath}}
{{identifier.locationInfo.modulePath}}
{{else}}
{{identifier.locationInfo.moduleName}}
{{/if}}
</div>
{{/input-with-autocomplete}}
</span>
{{#if currentFile}}
<span class="package-header-filename">
{{currentFile}}
</span>
{{/if}}
</div>
<div class="package-content">
{{#resizable-panel class="left-panel" alsoResizeElementId="#right-panel" as |hide|}}
<div class="file-tree-container">
{{file-tree directoryTree=model.directoryTree openFile="openFile" currentFile=currentFile packageId=model.id hide=hide}}
</div>
{{/resizable-panel}}
<div id="right-panel" class="right-panel">
<div id="file-container" class="file-container">
{{outlet}}
</div>
{{#bottom-panel visible=bottomPanelVisible topPanelElementId="#file-container" containerElementId="#right-panel" as |section|}}
{{#if (eq section "header")}}
References to <b><span class="source-code">{{occName}}</span></b> in <span class="source-code">{{packageId}}</span>
{{else}}
{{#paginated-list url=referencesUrl as |files|}}
<ul>
{{#each files as |file|}}
<li>
<div class="file-name"><a href="/package/{{packageId}}/show/{{file.name}}">{{file.name}}</a></div>
{{#each file.references as |reference|}}
<a class="source-code source-code-snippet" href="/package/{{packageId}}/show/{{file.name}}#L{{reference.idSrcSpan.line}}">{{{reference.sourceCodeHtml}}}</a>
{{/each}}
</li>
{{/each}}
</ul>
{{/paginated-list}}
{{/if}}
{{/bottom-panel}}
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
<div class="package-modules">
<div class="module-search-input">
{{input class="form-control" type="text" value=query placeholder="Module name"}}
</div>
<ul class="modules">
{{#each modulesFiltered as |module|}}
<li>
<a href="/package/{{model.id}}/show/{{module}}">{{module}}</a>
</li>
{{/each}}
</ul>
</div>

View File

@ -0,0 +1,21 @@
<div class="search-results-header">
Query : {{model.query}}
</div>
<div class="search-results-content">
{{#paginated-list url=model.url as |identifiers|}}
<ul>
{{#each identifiers as |identifier|}}
<li class="search-result">
<span class="source-code" >{{identifier.demangledOccName}} :: {{type-signature-text components=identifier.idType.components}}</span>
<div><a href="#" onmouseup={{action "goToDefinition" identifier.locationInfo}}>Go to definition</a></div>
<div class="identifier-module">
{{#if identifier.locationInfo.modulePath}}
Defined in <a href="/package/{{identifier.locationInfo.packageId.name}}-{{identifier.locationInfo.packageId.version}}/show/{{identifier.locationInfo.modulePath}}">{{identifier.locationInfo.modulePath}}</a>
{{/if}}
</div>
<div>{{{identifier.doc}}}</div>
</li>
{{/each}}
</ul>
{{/paginated-list}}
</div>

View File

@ -0,0 +1 @@
{{outlet}}

View File

@ -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}}

View File

@ -0,0 +1,26 @@
<div class="flex-container container">
<div>
<!-- <p class="lead"><b>Haskell code explorer</b> </p> -->
<div class="package-search-form">
{{input class="form-control" type="text" value=query placeholder="Package name"}}
<span>Number of packages : {{packages.length}}</span>
</div>
</div>
<div id="packages" class="packages">
<ul>
{{#infinite-list containerElementId="packages" elements=packages perPage=80 as |package|}}
<li>
{{#each package.versions as |version index|}}
{{#link-to 'package' (concat package.name "-" version)}}
{{#if (gt index 0)}}
{{version}}
{{else}}
{{package.name}}-{{version}}
{{/if}}
{{/link-to}}
{{/each}}
</li>
{{/infinite-list}}
</ul>
</div>
</div>

View File

@ -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');
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,111 @@
function initializeLineSelection(sourceCodeContainerElement,component) {
const lineNumbers = Array.prototype.slice.call(sourceCodeContainerElement.querySelectorAll("td.line-number"));
if(lineNumbers.length > 0) {
const onhashchange = function () {
highlightSelectedLines(sourceCodeContainerElement);
}
window.addEventListener("hashchange",onhashchange);
component._onhashchange = onhashchange;
let shiftPressed;
const onkeydown = function (event) {
if(event.keyCode === 16) { shiftPressed = true; }
};
const onkeyup = function (event) {
if(event.keyCode === 16) { shiftPressed = false; }
};
document.addEventListener('keydown',onkeydown);
document.addEventListener('keyup',onkeyup);
component._onkeydown = onkeydown;
component._onkeyup = onkeyup;
let selectedLine1,selectedLine2;
lineNumbers.forEach((lineNumberElement) => {
lineNumberElement.onclick = function() {
const number = parseInt(this.textContent);
if(shiftPressed && selectedLine1) {
if(selectedLine1 != number) {
selectedLine2 = number;
if(selectedLine1 < selectedLine2) {
highlightLines(sourceCodeContainerElement,selectedLine1,selectedLine2);
window.location.hash = "L"+selectedLine1+"-L"+selectedLine2;
} else {
highlightLines(sourceCodeContainerElement,selectedLine2,selectedLine1);
window.location.hash = "L"+selectedLine2+"-L"+selectedLine1;
}
}
} else {
selectedLine1 = number;
selectedLine2 = null;
highlightLines(sourceCodeContainerElement,selectedLine1,selectedLine1);
window.location.hash = "L"+number;
}
}
});
const lines = highlightSelectedLines(sourceCodeContainerElement);
if(lines.length) {
selectedLine1 = lines[0];
selectedLine2 = lines[1];
}
}
}
function highlightSelectedLines (sourceCodeContainerElement) {
const lineInfo = window.location.hash.slice(1);
if(lineInfo) {
if(lineInfo.includes('-')) {
const lines = lineInfo.split("-");
const lineNumber1 = parseInt(lines[0].substring(1));
const lineNumber2 = parseInt(lines[1].substring(1));
if(lineNumber1 && lineNumber2 && lineNumber1 <= lineNumber2) {
highlightLines(sourceCodeContainerElement,lineNumber1,lineNumber2);
const line = sourceCodeContainerElement.querySelector("td#LC"+lineNumber1);
if(line) {
scrollLineIntoView(line,sourceCodeContainerElement);
}
return [lineNumber1,lineNumber2];
}
} else {
const lineNumber = parseInt(lineInfo.substring(1));
if(lineNumber) {
highlightLines(sourceCodeContainerElement,lineNumber,lineNumber);
const line = sourceCodeContainerElement.querySelector("td#LC"+lineNumber);
if(line) {
scrollLineIntoView(line,sourceCodeContainerElement);
}
return [lineNumber];
}
}
} else {
highlightLines(sourceCodeContainerElement,0,0);
return [];
}
}
function scrollLineIntoView(lineElement,sourceCodeContainerElement) {
lineElement.parentNode.scrollIntoView();
const container = sourceCodeContainerElement.parentNode.parentNode;
const windowHeight = container.offsetHeight;
const fullHeight = sourceCodeContainerElement.offsetHeight;
if(fullHeight - container.scrollTop > windowHeight) {
container.scrollTop = container.scrollTop - (windowHeight/2 - 20);
}
}
function highlightLines(parentElement,startLine,endLine) {
const lineElements = Array.prototype.slice.call(parentElement.querySelectorAll("td.line-content"));
lineElements.forEach((lineElement) => {
const number = parseInt(lineElement.id.substring(2)); //<td "id"="LC10">...</td>
if(number >= startLine && number <= endLine) {
lineElement.classList.add('highlighted-line');
} else {
lineElement.classList.remove('highlighted-line');
}
});
}
export {
initializeLineSelection,highlightLines,highlightSelectedLines
}

6
javascript/bower.json Normal file
View File

@ -0,0 +1,6 @@
{
"name": "haskell-code-explorer",
"dependencies": {
"pace": "^1.0.2"
}
}

View File

@ -0,0 +1,64 @@
/* eslint-env node */
'use strict';
module.exports = function(environment) {
let ENV = {
modulePrefix: 'haskell-code-explorer',
environment,
rootURL: '/',
locationType: 'auto',
EmberENV: {
FEATURES: {
// Here you can enable experimental features on an ember canary build
// e.g. 'with-controller': true
},
EXTEND_PROTOTYPES: {
// Prevent Ember Data from overriding Date.parse.
Date: false
}
},
pace: {
theme: 'minimal',
color: 'silver',
target: 'body',
elements: {
checkInterval: 100,
selectors: ['body', '.ember-view']
},
ajax: false
},
APP: {
staticUrlPrefix: "/files",
apiUrlPrefix: "/api",
haskellCodeExplorerDirectory: ".haskell-code-explorer",
title: "Haskell code explorer"
}
};
if (environment === 'development') {
// ENV.APP.LOG_RESOLVER = true;
// ENV.APP.LOG_ACTIVE_GENERATION = true;
// ENV.APP.LOG_TRANSITIONS = true;
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
// ENV.APP.LOG_VIEW_LOOKUPS = true;
}
if (environment === 'test') {
// Testem prefers this...
ENV.locationType = 'none';
// keep test console output quieter
ENV.APP.LOG_ACTIVE_GENERATION = false;
ENV.APP.LOG_VIEW_LOOKUPS = false;
ENV.APP.autoboot = false;
ENV.APP.rootElement = '#ember-testing';
}
if (environment === 'production') {
}
return ENV;
};

View File

@ -0,0 +1,9 @@
/* eslint-env node */
module.exports = {
browsers: [
'ie 9',
'last 1 Chrome versions',
'last 1 Firefox versions',
'last 1 Safari versions'
]
};

View File

@ -0,0 +1,21 @@
/* eslint-env node */
'use strict';
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
module.exports = function(defaults) {
let app = new EmberApp(defaults, {
'ember-cli-bootstrap-4': {
js: null
},
'ember-cli-babel': {
includePolyfill: true
}
});
app.import('vendor/jquery-ui-1.12.1.custom/jquery-ui.min.js')// only draggable and resizable
app.import('vendor/jquery-ui-1.12.1.custom/jquery-ui.structure.min.css')
app.import('node_modules/jstree/dist/jstree.min.js');
app.import('node_modules/jstree/dist/themes/default/style.min.css');
app.import('node_modules/showdown/dist/showdown.min.js');
return app.toTree();
};

53
javascript/package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "haskell-code-explorer",
"version": "0.0.1",
"description": "Haskell code explorer",
"private": false,
"license": "MIT",
"author": "alexwl",
"directories": {
"doc": "doc",
"test": "tests"
},
"repository": "",
"scripts": {
"build": "ember build --environment=production",
"release": "ember build --environment=production --output-path=release",
"start": "ember server"
},
"devDependencies": {
"broccoli-asset-rev": "^2.4.5",
"ember-cli": "^3.2.0",
"ember-cli-app-version": "^3.0.0",
"ember-cli-babel": "^6.12.0",
"ember-cli-bootstrap-4": "^0.5.6",
"ember-cli-dependency-checker": "^1.3.0",
"ember-cli-eslint": "^4.2.1",
"ember-cli-htmlbars": "^2.0.1",
"ember-cli-htmlbars-inline-precompile": "^1.0.0",
"ember-cli-inject-live-reload": "^1.4.1",
"ember-cli-pace": "^0.1.0",
"ember-cli-qunit": "^4.1.1",
"ember-cli-sass": "^7.1.7",
"ember-cli-shims": "^1.1.0",
"ember-cli-sri": "^2.1.0",
"ember-cli-uglify": "^1.2.0",
"ember-export-application-global": "^2.0.0",
"ember-load-initializers": "^1.0.0",
"ember-radio-button": "^1.1.1",
"ember-resolver": "^4.0.0",
"ember-sinon": "^1.0.1",
"ember-sinon-qunit": "^2.0.0",
"ember-source": "3.2.0",
"ember-truth-helpers": "^2.0.0",
"express": "^4.16.2",
"glob": "^4.5.3",
"jstree": "3.3.5",
"loader.js": "^4.2.3",
"morgan": "^1.9.0",
"showdown": "^1.8.6"
},
"engines": {
"node": "^4.5 || 6.* || >= 7.*"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,3 @@
# http://www.robotstxt.org
User-agent: *
Disallow:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
# http://www.robotstxt.org
User-agent: *
Disallow:

Some files were not shown because too many files have changed in this diff Show More