feat: Code blocks can now optionally display line numbers (#4324)
* feat: Code blocks can now optionally display line numbers as a user preference * Touch more breathing room
This commit is contained in:
@@ -57,10 +57,12 @@ export type Props = Optional<
|
||||
|
||||
function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
|
||||
const { id, shareId, onChange, onHeadingsChange } = props;
|
||||
const { documents } = useStores();
|
||||
const { documents, auth } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const dictionary = useDictionary();
|
||||
const embeds = useEmbeds(!shareId);
|
||||
const preferences = auth.user?.preferences;
|
||||
|
||||
const [
|
||||
activeLinkEvent,
|
||||
setActiveLinkEvent,
|
||||
@@ -286,6 +288,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
|
||||
uploadFile={onUploadFile}
|
||||
onShowToast={showToast}
|
||||
embeds={embeds}
|
||||
userPreferences={preferences}
|
||||
dictionary={dictionary}
|
||||
{...props}
|
||||
onHoverLink={handleLinkActive}
|
||||
|
||||
@@ -28,9 +28,8 @@ import Node from "@shared/editor/nodes/Node";
|
||||
import ReactNode from "@shared/editor/nodes/ReactNode";
|
||||
import fullExtensionsPackage from "@shared/editor/packages/full";
|
||||
import { EventType } from "@shared/editor/types";
|
||||
import { IntegrationType } from "@shared/types";
|
||||
import { UserPreferences } from "@shared/types";
|
||||
import EventEmitter from "@shared/utils/events";
|
||||
import Integration from "~/models/Integration";
|
||||
import Flex from "~/components/Flex";
|
||||
import { Dictionary } from "~/hooks/useDictionary";
|
||||
import Logger from "~/utils/Logger";
|
||||
@@ -105,14 +104,15 @@ export type Props = {
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
/** Collection of embed types to render in the document */
|
||||
embeds: EmbedDescriptor[];
|
||||
/** Display preferences for the logged in user, if any. */
|
||||
userPreferences?: UserPreferences | null;
|
||||
/** Whether embeds should be rendered without an iframe */
|
||||
embedsDisabled?: boolean;
|
||||
/** Callback when a toast message is triggered (eg "link copied") */
|
||||
onShowToast: (message: string) => void;
|
||||
className?: string;
|
||||
/** Optional style overrides */
|
||||
style?: React.CSSProperties;
|
||||
|
||||
embedIntegrations?: Integration<IntegrationType.Embed>[];
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
@@ -98,6 +98,18 @@ function Preferences() {
|
||||
onChange={handlePreferenceChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name="codeBlockLineNumbers"
|
||||
label={t("Show line numbers")}
|
||||
description={t("Show line numbers on code blocks in documents.")}
|
||||
>
|
||||
<Switch
|
||||
id="codeBlockLineNumbers"
|
||||
name="codeBlockLineNumbers"
|
||||
checked={user.getPreference(UserPreference.CodeBlockLineNumers, true)}
|
||||
onChange={handlePreferenceChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
border={false}
|
||||
name="rememberLastPath"
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
"react-window": "^1.8.7",
|
||||
"reakit": "^1.3.11",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"refractor": "^3.5.0",
|
||||
"refractor": "^3.6.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"request-filtering-agent": "^1.1.2",
|
||||
"semver": "^7.3.7",
|
||||
|
||||
@@ -864,6 +864,21 @@ mark {
|
||||
}
|
||||
}
|
||||
|
||||
.code-block.with-line-numbers {
|
||||
pre {
|
||||
padding-left: calc(var(--line-number-gutter-width, 0) * 1em + 1.5em);
|
||||
}
|
||||
}
|
||||
|
||||
.code-block .line-numbers {
|
||||
position: absolute;
|
||||
left: 1em;
|
||||
color: ${props.theme.textTertiary};
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mermaid-diagram-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -39,6 +39,7 @@ import sql from "refractor/lang/sql";
|
||||
import swift from "refractor/lang/swift";
|
||||
import typescript from "refractor/lang/typescript";
|
||||
import yaml from "refractor/lang/yaml";
|
||||
import { UserPreferences } from "@shared/types";
|
||||
import { Dictionary } from "~/hooks/useDictionary";
|
||||
|
||||
import toggleBlockType from "../commands/toggleBlockType";
|
||||
@@ -81,8 +82,10 @@ const DEFAULT_LANGUAGE = "javascript";
|
||||
export default class CodeFence extends Node {
|
||||
constructor(options: {
|
||||
dictionary: Dictionary;
|
||||
userPreferences?: UserPreferences | null;
|
||||
onShowToast: (message: string) => void;
|
||||
}) {
|
||||
console.log({ options });
|
||||
super(options);
|
||||
}
|
||||
|
||||
@@ -174,7 +177,11 @@ export default class CodeFence extends Node {
|
||||
return [
|
||||
"div",
|
||||
{
|
||||
class: "code-block",
|
||||
class: `code-block ${
|
||||
this.options.userPreferences?.codeBlockLineNumbers
|
||||
? "with-line-numbers"
|
||||
: ""
|
||||
}`,
|
||||
"data-language": node.attrs.language,
|
||||
},
|
||||
...(actions ? [["div", { contentEditable: "false" }, actions]] : []),
|
||||
@@ -301,7 +308,10 @@ export default class CodeFence extends Node {
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
Prism({ name: this.name }),
|
||||
Prism({
|
||||
name: this.name,
|
||||
lineNumbers: this.options.userPreferences?.codeBlockLineNumbers,
|
||||
}),
|
||||
Mermaid({ name: this.name }),
|
||||
new Plugin({
|
||||
key: new PluginKey("triple-click"),
|
||||
|
||||
@@ -40,7 +40,18 @@ type ParsedNode = {
|
||||
|
||||
const cache: Record<number, { node: Node; decorations: Decoration[] }> = {};
|
||||
|
||||
function getDecorations({ doc, name }: { doc: Node; name: string }) {
|
||||
function getDecorations({
|
||||
doc,
|
||||
name,
|
||||
lineNumbers,
|
||||
}: {
|
||||
/** The prosemirror document to operate on. */
|
||||
doc: Node;
|
||||
/** The node name. */
|
||||
name: string;
|
||||
/** Whether to include decorations representing line numbers */
|
||||
lineNumbers?: boolean;
|
||||
}) {
|
||||
const decorations: Decoration[] = [];
|
||||
const blocks: { node: Node; pos: number }[] = findBlockNodes(doc).filter(
|
||||
(item) => item.node.type.name === name
|
||||
@@ -71,6 +82,27 @@ function getDecorations({ doc, name }: { doc: Node; name: string }) {
|
||||
}
|
||||
|
||||
if (!cache[block.pos] || !cache[block.pos].node.eq(block.node)) {
|
||||
if (lineNumbers) {
|
||||
const lineCount =
|
||||
(block.node.textContent.match(/\n/g) || []).length + 1;
|
||||
decorations.push(
|
||||
Decoration.widget(block.pos + 1, () => {
|
||||
const el = document.createElement("div");
|
||||
el.innerText = new Array(lineCount)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1)
|
||||
.join("\n");
|
||||
el.className = "line-numbers";
|
||||
return el;
|
||||
})
|
||||
);
|
||||
decorations.push(
|
||||
Decoration.node(block.pos, block.pos + block.node.nodeSize, {
|
||||
style: `--line-number-gutter-width: ${String(lineCount).length}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const nodes = refractor.highlight(block.node.textContent, language);
|
||||
const _decorations = flattenDeep(parseNodes(nodes))
|
||||
.map((node: ParsedNode) => {
|
||||
@@ -111,7 +143,15 @@ function getDecorations({ doc, name }: { doc: Node; name: string }) {
|
||||
return DecorationSet.create(doc, decorations);
|
||||
}
|
||||
|
||||
export default function Prism({ name }: { name: string }) {
|
||||
export default function Prism({
|
||||
name,
|
||||
lineNumbers,
|
||||
}: {
|
||||
/** The node name. */
|
||||
name: string;
|
||||
/** Whether to include decorations representing line numbers */
|
||||
lineNumbers?: boolean;
|
||||
}) {
|
||||
let highlighted = false;
|
||||
|
||||
return new Plugin({
|
||||
@@ -129,7 +169,7 @@ export default function Prism({ name }: { name: string }) {
|
||||
|
||||
if (!highlighted || codeBlockChanged || ySyncEdit) {
|
||||
highlighted = true;
|
||||
return getDecorations({ doc: transaction.doc, name });
|
||||
return getDecorations({ doc: transaction.doc, name, lineNumbers });
|
||||
}
|
||||
|
||||
return decorationSet.map(transaction.mapping, transaction.doc);
|
||||
|
||||
@@ -696,6 +696,8 @@
|
||||
"Choose the interface language. Community translations are accepted though our <2>translation portal</2>.": "Choose the interface language. Community translations are accepted though our <2>translation portal</2>.",
|
||||
"Use pointer cursor": "Use pointer cursor",
|
||||
"Show a hand cursor when hovering over interactive elements.": "Show a hand cursor when hovering over interactive elements.",
|
||||
"Show line numbers": "Show line numbers",
|
||||
"Show line numbers on code blocks in documents.": "Show line numbers on code blocks in documents.",
|
||||
"Remember previous location": "Remember previous location",
|
||||
"Automatically return to the document you were last viewing when the app is re-opened.": "Automatically return to the document you were last viewing when the app is re-opened.",
|
||||
"You may delete your account at any time, note that this is unrecoverable": "You may delete your account at any time, note that this is unrecoverable",
|
||||
|
||||
@@ -49,6 +49,7 @@ export enum UserPreference {
|
||||
RememberLastPath = "rememberLastPath",
|
||||
/** If web-style hand pointer should be used on interactive elements. */
|
||||
UseCursorPointer = "useCursorPointer",
|
||||
CodeBlockLineNumers = "codeBlockLineNumbers",
|
||||
}
|
||||
|
||||
export type UserPreferences = { [key in UserPreference]?: boolean };
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@@ -12455,10 +12455,10 @@ pretty@^2.0.0:
|
||||
extend-shallow "^2.0.1"
|
||||
js-beautify "^1.6.12"
|
||||
|
||||
prismjs@~1.25.0:
|
||||
version "1.25.0"
|
||||
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756"
|
||||
integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==
|
||||
prismjs@~1.27.0:
|
||||
version "1.27.0"
|
||||
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057"
|
||||
integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==
|
||||
|
||||
process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
|
||||
version "2.0.1"
|
||||
@@ -13272,14 +13272,14 @@ reflect.ownkeys@^0.2.0:
|
||||
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
|
||||
integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
|
||||
|
||||
refractor@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.5.0.tgz#334586f352dda4beaf354099b48c2d18e0819aec"
|
||||
integrity sha512-QwPJd3ferTZ4cSPPjdP5bsYHMytwWYnAN5EEnLtGvkqp/FCCnGsBgxrm9EuIDnjUC3Uc/kETtvVi7fSIVC74Dg==
|
||||
refractor@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a"
|
||||
integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==
|
||||
dependencies:
|
||||
hastscript "^6.0.0"
|
||||
parse-entities "^2.0.0"
|
||||
prismjs "~1.25.0"
|
||||
prismjs "~1.27.0"
|
||||
|
||||
regenerate-unicode-properties@^8.2.0:
|
||||
version "8.2.0"
|
||||
|
||||
Reference in New Issue
Block a user