diff --git a/scm-ui/ui-components/.storybook/webpack.config.js b/scm-ui/ui-components/.storybook/webpack.config.js index 947515f4d3..c8459adf6b 100644 --- a/scm-ui/ui-components/.storybook/webpack.config.js +++ b/scm-ui/ui-components/.storybook/webpack.config.js @@ -20,6 +20,10 @@ module.exports = { } ] }, + { + test: /\.worker\.(j|t)s$/, + use: { loader: "worker-loader" } + }, { test: /\.(css|scss|sass)$/i, use: [ @@ -38,8 +42,6 @@ module.exports = { ] }, resolve: { - extensions: [ - ".ts", ".tsx", ".js", ".jsx", ".css", ".scss", ".json" - ] + extensions: [".ts", ".tsx", ".js", ".jsx", ".css", ".scss", ".json"] } }; diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 1e3648dbab..d8d4820174 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -26,6 +26,7 @@ "@types/enzyme": "^3.10.3", "@types/fetch-mock": "^7.3.1", "@types/jest": "^24.0.19", + "@types/lowlight": "^0.0.0", "@types/query-string": "5", "@types/react": "^16.9.9", "@types/react-dom": "^16.9.2", @@ -40,7 +41,8 @@ "raf": "^3.4.0", "react-test-renderer": "^16.10.2", "storybook-addon-i18next": "^1.2.1", - "typescript": "^3.7.2" + "typescript": "^3.7.2", + "worker-loader": "^2.0.0" }, "dependencies": { "@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT", @@ -48,6 +50,8 @@ "classnames": "^2.2.6", "date-fns": "^2.4.1", "event-source-polyfill": "^1.0.9", + "gitdiff-parser": "^0.1.2", + "lowlight": "^1.13.0", "query-string": "5", "react": "^16.8.6", "react-diff-view": "^2.4.1", @@ -56,8 +60,7 @@ "react-markdown": "^4.0.6", "react-router-dom": "^5.1.2", "react-select": "^2.1.2", - "react-syntax-highlighter": "^11.0.2", - "gitdiff-parser": "^0.1.2" + "react-syntax-highlighter": "^11.0.2" }, "babel": { "presets": [ diff --git a/scm-ui/ui-components/src/repos/Diff.stories.tsx b/scm-ui/ui-components/src/repos/Diff.stories.tsx index 1b38f362d3..149b1ecab5 100644 --- a/scm-ui/ui-components/src/repos/Diff.stories.tsx +++ b/scm-ui/ui-components/src/repos/Diff.stories.tsx @@ -7,8 +7,9 @@ import simpleDiff from "../__resources__/Diff.simple"; import hunksDiff from "../__resources__/Diff.hunks"; import binaryDiff from "../__resources__/Diff.binary"; import Button from "../buttons/Button"; -import { DiffEventContext } from "./DiffTypes"; +import { DiffEventContext, File } from "./DiffTypes"; import Toast from "../toast/Toast"; +import { getPath } from "./diffs"; const diffFiles = parser.parse(simpleDiff); @@ -57,4 +58,16 @@ storiesOf("Diff", module) .add("Binaries", () => { const binaryDiffFiles = parser.parse(binaryDiff); return ; + }) + .add("SyntaxHighlighting", () => { + const filesWithLanguage = diffFiles.map((file: File) => { + const ext = getPath(file).split(".")[1]; + if (ext === "tsx") { + file.language = "typescript"; + } else { + file.language = ext; + } + return file; + }); + return ; }); diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 49da9b2484..47b10997cc 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -3,11 +3,12 @@ import { withTranslation, WithTranslation } from "react-i18next"; import classNames from "classnames"; import styled from "styled-components"; // @ts-ignore -import { Diff as DiffComponent, getChangeKey, Hunk, Decoration } from "react-diff-view"; +import { getChangeKey, Hunk, Decoration } from "react-diff-view"; import { Button, ButtonGroup } from "../buttons"; import Tag from "../Tag"; import Icon from "../Icon"; import { ChangeEvent, Change, File, Hunk as HunkType, DiffObjectProps } from "./DiffTypes"; +import TokenizedDiffView from "./TokenizedDiffView"; const EMPTY_ANNOTATION_FACTORY = {}; @@ -57,33 +58,6 @@ const ChangeTypeTag = styled(Tag)` margin-left: 0.75rem; `; -const ModifiedDiffComponent = styled(DiffComponent)` - /* align line numbers */ - & .diff-gutter { - text-align: right; - } - /* column sizing */ - > colgroup .diff-gutter-col { - width: 3.25rem; - } - /* prevent following content from moving down */ - > .diff-gutter:empty:hover::after { - font-size: 0.7rem; - } - /* smaller font size for code */ - & .diff-line { - font-size: 0.75rem; - } - /* comment padding for sidebyside view */ - &.split .diff-widget-content .is-indented-line { - padding-left: 3.25rem; - } - /* comment padding for combined view */ - &.unified .diff-widget-content .is-indented-line { - padding-left: 6.5rem; - } -`; - class DiffFile extends React.Component { static defaultProps: Partial = { defaultCollapse: false, @@ -264,9 +238,9 @@ class DiffFile extends React.Component { body = (
{fileAnnotations} - + {(hunks: HunkType[]) => this.concat(hunks.map(this.renderHunk))} - +
); } diff --git a/scm-ui/ui-components/src/repos/DiffTypes.ts b/scm-ui/ui-components/src/repos/DiffTypes.ts index 49e9785d29..813322529e 100644 --- a/scm-ui/ui-components/src/repos/DiffTypes.ts +++ b/scm-ui/ui-components/src/repos/DiffTypes.ts @@ -18,6 +18,7 @@ export type File = { oldPath: string; oldRevision?: string; type: FileChangeType; + language?: string; // TODO does this property exists? isBinary?: boolean; }; diff --git a/scm-ui/ui-components/src/repos/Tokenize.worker.ts b/scm-ui/ui-components/src/repos/Tokenize.worker.ts new file mode 100644 index 0000000000..d64df14c11 --- /dev/null +++ b/scm-ui/ui-components/src/repos/Tokenize.worker.ts @@ -0,0 +1,29 @@ +/* eslint-disable no-restricted-globals */ +// @ts-ignore we have no types for react-diff-view +import { tokenize } from "react-diff-view"; +import refractor from "./refractorAdapter"; + +self.addEventListener("message", ({ data: { id, payload } }) => { + const { hunks, language } = payload; + const options = { + highlight: language !== "text", + language: language, + refractor + }; + try { + const tokens = tokenize(hunks, options); + const payload = { + success: true, + tokens: tokens + }; + // @ts-ignore seems to use wrong typing + self.postMessage({ id, payload }); + } catch (ex) { + const payload = { + success: false, + reason: ex.message + }; + // @ts-ignore seems to use wrong typing + self.postMessage({ id, payload }); + } +}); diff --git a/scm-ui/ui-components/src/repos/TokenizedDiffView.tsx b/scm-ui/ui-components/src/repos/TokenizedDiffView.tsx new file mode 100644 index 0000000000..fcc3397cb8 --- /dev/null +++ b/scm-ui/ui-components/src/repos/TokenizedDiffView.tsx @@ -0,0 +1,62 @@ +import React, { FC } from "react"; +import styled from "styled-components"; +// @ts-ignore we have no typings for react-diff-view +import { Diff, useTokenizeWorker } from "react-diff-view"; +// @ts-ignore we use webpack worker-loader to load the web worker +import TokenizeWorker from "./Tokenize.worker"; +import { File } from "./DiffTypes"; + +// styling for the diff tokens +// this must be aligned with th style, which is used in the SyntaxHighlighter component +import "highlight.js/styles/arduino-light.css"; + +const DiffView = styled(Diff)` + /* align line numbers */ + & .diff-gutter { + text-align: right; + } + /* column sizing */ + > colgroup .diff-gutter-col { + width: 3.25rem; + } + /* prevent following content from moving down */ + > .diff-gutter:empty:hover::after { + font-size: 0.7rem; + } + /* smaller font size for code */ + & .diff-line { + font-size: 0.75rem; + } + /* comment padding for sidebyside view */ + &.split .diff-widget-content .is-indented-line { + padding-left: 3.25rem; + } + /* comment padding for combined view */ + &.unified .diff-widget-content .is-indented-line { + padding-left: 6.5rem; + } +`; + +// WebWorker which creates tokens for syntax highlighting +const tokenize = new TokenizeWorker(); + +type Props = { + file: File; + viewType: "split" | "unified"; + className?: string; +}; + +const TokenizedDiffView: FC = ({ file, viewType, className, children }) => { + const { tokens } = useTokenizeWorker(tokenize, { + hunks: file.hunks, + language: file.language || "text" + }); + + return ( + + {children} + + ); +}; + +export default TokenizedDiffView; diff --git a/scm-ui/ui-components/src/repos/refractorAdapter.ts b/scm-ui/ui-components/src/repos/refractorAdapter.ts new file mode 100644 index 0000000000..3e0425aad8 --- /dev/null +++ b/scm-ui/ui-components/src/repos/refractorAdapter.ts @@ -0,0 +1,14 @@ +import lowlight from "lowlight"; + +// adapter to let lowlight look like refractor +// this is required because react-diff-view does only support refractor, +// but we want same highlighting as in the source code browser. + +const refractorAdapter = { + ...lowlight, + highlight: (value: string, language: string) => { + return lowlight.highlight(language, value).value; + } +}; + +export default refractorAdapter; diff --git a/scm-ui/ui-scripts/package.json b/scm-ui/ui-scripts/package.json index 4b2564b419..c53d2eff98 100644 --- a/scm-ui/ui-scripts/package.json +++ b/scm-ui/ui-scripts/package.json @@ -24,6 +24,7 @@ "script-loader": "^0.7.2", "style-loader": "^1.0.0", "thread-loader": "^2.1.3", + "worker-loader": "^2.0.0", "webpack": "^4.41.5", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.10.1" diff --git a/scm-ui/ui-scripts/src/webpack.config.js b/scm-ui/ui-scripts/src/webpack.config.js index b4c32fc680..5c8595e505 100644 --- a/scm-ui/ui-scripts/src/webpack.config.js +++ b/scm-ui/ui-scripts/src/webpack.config.js @@ -30,6 +30,10 @@ module.exports = [ systemjs: false } }, + { + test: /\.worker\.(j|t)s$/, + use: { loader: "worker-loader" } + }, { test: /\.(js|ts|jsx|tsx)$/i, exclude: /node_modules/, diff --git a/yarn.lock b/yarn.lock index ef120d73f6..1594f5a8ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2758,6 +2758,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== +"@types/lowlight@^0.0.0": + version "0.0.0" + resolved "https://registry.yarnpkg.com/@types/lowlight/-/lowlight-0.0.0.tgz#eef9ff807ff29bbf9f2fe55d0488fb9fc9d89df6" + integrity sha1-7vn/gH/ym7+fL+VdBIj7n8nYnfY= + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -7672,6 +7677,11 @@ highlight.js@~9.13.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e" integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A== +highlight.js@~9.16.0: + version "9.16.2" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.16.2.tgz#68368d039ffe1c6211bcc07e483daf95de3e403e" + integrity sha512-feMUrVLZvjy0oC7FVJQcSQRqbBq9kwqnYE4+Kj9ZjbHh3g+BisiPgF49NyQbVLNdrL/qqZr3Ca9yOKwgn2i/tw== + history@^4.10.1, history@^4.9.0: version "4.10.1" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" @@ -9435,7 +9445,7 @@ loader-runner@^2.3.1, loader-runner@^2.4.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@1.2.3, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3: +loader-utils@1.2.3, loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== @@ -9602,6 +9612,14 @@ lower-case@^1.1.1: resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= +lowlight@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.13.0.tgz#9b4fd00559985e40e11c916ccab14c7c0cf4320d" + integrity sha512-bFXLa+UO1eM3zieFAcNqf6rTQ1D5ERFv64/euQbbH/LT3U9XXwH6tOrgUAGWDsQ1QgN3ZhgOcv8p3/S+qKGdTQ== + dependencies: + fault "^1.0.2" + highlight.js "~9.16.0" + lowlight@~1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.11.0.tgz#1304d83005126d4e8b1dc0f07981e9b689ec2efc" @@ -12989,6 +13007,14 @@ scheduler@^0.18.0: loose-envify "^1.1.0" object-assign "^4.1.1" +schema-utils@^0.4.0: + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" + integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ== + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -14972,6 +14998,14 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac" + integrity sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw== + dependencies: + loader-utils "^1.0.0" + schema-utils "^0.4.0" + worker-rpc@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5"