Skip to content

Commit 40ca653

Browse files
committed
[Code] Add highlightLine prop to CodeBlock (#48230)
* CodeBlock accepts an array of strings, rather than a single string Rather than do the splitting up of lines (for highlighting, numbering) internally, it makes a bit more sense to have the consumer provide an array of strings to be rendered. The biggest win here is the disambiguation of our upcoming `highlightLine` prop: were we to accept an array of line indices, it's unclear whether those should correspond to the monaco index (1-based), internal index (0-based), or formatted (passed through `lineNumber)`. Better to standardize on the lineIndex argument parameter already used for lineNumber, and simply ask the consumer to return a boolean for any given line. * Add highlightLine prop to CodeBlock Allows consumers to declare which lines should be highlighted with the more subtle, full-width coloring. * Refactors decoration generation into private methods * Simplifies both lineNumber and highlightLine to be invoked with _just_ the lineIndex, as consumers will now have the array to index into themselves, if necessary. * Simplify CSS related to line highlighting Because of the way we're currently using Monaco, we need to apply all three of these options to our line decorations. However, all we really need to do is set the background-color. As such, we can remove these redundant/unused css classes and reduce the noise around this functionality. Also, BEM. * Remove errant CSS rule This selector is meant to move the folding button over to account for the extra width taken by the Blame sidebar. However, `.code-line-decoration` is the only class that is applied to the blame view, and the selector in question was in fact incorrectly moving the folding button off the screen on any foldable line that was also highlighted. The bug was fixed in the previous commit that removed this class, but this was the last mention of it. * Update mock data to depend on Typescript-Node-Starter repo So that we don't have to import a non-standard repo to view the full functionality of this page.
1 parent bf128ed commit 40ca653

8 files changed

Lines changed: 140 additions & 141 deletions

File tree

x-pack/legacy/plugins/code/public/components/code_block/code_block.tsx

Lines changed: 58 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,27 @@ export interface Position {
1717
}
1818

1919
export interface Props {
20-
content: string;
20+
lines: string[];
2121
language: string;
22+
/**
23+
* Returns whether to highlight the given line.
24+
* @param lineIndex The index of the line (0-based)
25+
*/
26+
highlightLine: (lineIndex: number) => boolean;
2227
highlightRanges: IRange[];
2328
onClick: (event: Position) => void;
2429
folding: boolean;
2530
/**
2631
* Returns the line number to display for a given line.
27-
* @param lineIndex The index of the given line (0-indexed)
32+
* @param lineIndex The index of the line (0-based)
2833
*/
2934
lineNumber: (lineIndex: number) => string;
3035
}
3136

3237
export class CodeBlock extends React.PureComponent<Props> {
3338
static defaultProps = {
3439
folding: false,
40+
highlightLine: () => {},
3541
highlightRanges: [],
3642
language: 'text',
3743
lineNumber: String,
@@ -41,13 +47,13 @@ export class CodeBlock extends React.PureComponent<Props> {
4147
private el = createRef<HTMLDivElement>();
4248
private ed?: editor.IStandaloneCodeEditor;
4349
private resizeChecker?: ResizeChecker;
44-
private currentHighlightDecorations: string[] = [];
50+
private currentDecorations: string[] = [];
4551

4652
public async componentDidMount() {
47-
const { content, highlightRanges, language, onClick } = this.props;
53+
const { language, onClick } = this.props;
4854

4955
if (this.el.current) {
50-
await this.tryLoadFile(content, language);
56+
await this.tryLoadFile(this.text, language);
5157
this.ed!.onMouseDown((e: editor.IEditorMouseEvent) => {
5258
if (
5359
onClick &&
@@ -65,17 +71,8 @@ export class CodeBlock extends React.PureComponent<Props> {
6571
});
6672
registerEditor(this.ed!);
6773

68-
if (highlightRanges.length) {
69-
const decorations = highlightRanges.map((range: IRange) => {
70-
return {
71-
range,
72-
options: {
73-
inlineClassName: 'codeSearch__highlight',
74-
},
75-
};
76-
});
77-
this.currentHighlightDecorations = this.ed!.deltaDecorations([], decorations);
78-
}
74+
this.setDecorations();
75+
7976
this.resizeChecker = new ResizeChecker(this.el.current!);
8077
this.resizeChecker.on('resize', () => {
8178
setTimeout(() => {
@@ -85,18 +82,18 @@ export class CodeBlock extends React.PureComponent<Props> {
8582
}
8683
}
8784

88-
private async tryLoadFile(code: string, language: string) {
85+
private async tryLoadFile(text: string, language: string) {
8986
try {
90-
await monaco.editor.colorize(code, language, {});
91-
this.loadFile(code, language);
87+
await monaco.editor.colorize(text, language, {});
88+
this.loadFile(text, language);
9289
} catch (e) {
93-
this.loadFile(code);
90+
this.loadFile(text);
9491
}
9592
}
9693

97-
private loadFile(code: string, language: string = 'text') {
94+
private loadFile(text: string, language: string = 'text') {
9895
this.ed = monaco.editor.create(this.el.current!, {
99-
value: code,
96+
value: text,
10097
language,
10198
lineNumbers: this.lineNumber,
10299
readOnly: true,
@@ -122,28 +119,15 @@ export class CodeBlock extends React.PureComponent<Props> {
122119
}
123120

124121
public componentDidUpdate(prevProps: Readonly<Props>) {
125-
const { content, highlightRanges } = this.props;
122+
const { highlightRanges } = this.props;
123+
const prevText = prevProps.lines.join('\n');
126124

127-
if (prevProps.content !== content || prevProps.highlightRanges !== highlightRanges) {
125+
if (prevText !== this.text || prevProps.highlightRanges !== highlightRanges) {
128126
if (this.ed) {
129127
const model = this.ed.getModel();
130128
if (model) {
131-
model.setValue(content);
132-
133-
if (highlightRanges.length) {
134-
const decorations = highlightRanges!.map((range: IRange) => {
135-
return {
136-
range,
137-
options: {
138-
inlineClassName: 'codeSearch__highlight',
139-
},
140-
};
141-
});
142-
this.currentHighlightDecorations = this.ed.deltaDecorations(
143-
this.currentHighlightDecorations,
144-
decorations
145-
);
146-
}
129+
model.setValue(this.text);
130+
this.setDecorations();
147131
}
148132
}
149133
}
@@ -156,14 +140,45 @@ export class CodeBlock extends React.PureComponent<Props> {
156140
}
157141

158142
public render() {
159-
const height = this.lines.length * 18;
143+
const height = this.props.lines.length * 18;
160144

161145
return <div ref={this.el} className="codeContainer__monaco" style={{ height }} />;
162146
}
163147

164148
private lineNumber = (lineIndex: number) => this.props.lineNumber(lineIndex - 1);
165149

166-
private get lines(): string[] {
167-
return this.props.content.split('\n');
150+
private get text(): string {
151+
return this.props.lines.join('\n');
152+
}
153+
154+
private setDecorations() {
155+
const decorations = this.decorations;
156+
if (decorations.length) {
157+
this.currentDecorations = this.ed!.deltaDecorations(this.currentDecorations, decorations);
158+
}
159+
}
160+
161+
private get decorations(): editor.IModelDeltaDecoration[] {
162+
const { lines, highlightRanges, highlightLine } = this.props;
163+
164+
const rangeHighlights = highlightRanges.map(range => ({
165+
range,
166+
options: {
167+
inlineClassName: 'codeSearch__highlight',
168+
},
169+
}));
170+
171+
const lineHighlights = lines
172+
.map((line, lineIndex) => ({
173+
range: new monaco.Range(lineIndex + 1, 0, lineIndex + 1, 0),
174+
options: {
175+
isWholeLine: true,
176+
className: 'codeBlock__line--highlighted',
177+
linesDecorationsClassName: 'codeBlock__line--highlighted',
178+
},
179+
}))
180+
.filter((decorations, lineIndex) => highlightLine(lineIndex));
181+
182+
return [...rangeHighlights, ...lineHighlights];
168183
}
169184
}

x-pack/legacy/plugins/code/public/components/editor/references_panel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class ReferencesPanel extends React.Component<Props, State> {
127127
className="referencesPanel__code-block"
128128
key={key}
129129
header={header}
130-
content={file.code}
130+
lines={file.code.split('\n')}
131131
language={file.language}
132132
lineNumber={i => file.lineNumbers[i]}
133133
highlightRanges={file.highlights}

x-pack/legacy/plugins/code/public/components/integrations/data.ts

Lines changed: 73 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -17,119 +17,112 @@ export interface Snippet {
1717
export type Results = Record<string, Snippet>;
1818

1919
export const results: Results = {
20-
'ringside.ts#L18': {
21-
uri: 'github.com/rylnd/ringside',
22-
filePath: 'src/ringside.ts',
20+
'user.ts#L15': {
21+
uri: 'github.com/Microsoft/Typescript-Node-Starter',
22+
filePath: 'src/controllers/user.ts',
2323
language: 'typescript',
2424
compositeContent: {
2525
content:
26-
"\nimport { fitsInside, fitsOutside } from './fitting';\n\nexport interface RingsideInterface {\n positions(): FittedPosition[];\n}\n\nclass Ringside implements RingsideInterface {\n readonly innerBounds: FullRect;\n readonly outerBounds: FullRect;\n\n}\n\nexport default Ringside;\n",
26+
'\nimport nodemailer from "nodemailer";\nimport passport from "passport";\nimport { User, UserDocument, AuthToken } from "../models/User";\nimport { Request, Response, NextFunction } from "express";\nimport { IVerifyOptions } from "passport-local";\n\n */\nexport const getLogin = (req: Request, res: Response) => {\n if (req.user) {\n return res.redirect("/");\n }\n\n }\n\n passport.authenticate("local", (err: Error, user: UserDocument, info: IVerifyOptions) => {\n if (err) { return next(err); }\n if (!user) {\n',
2727
lineMapping: [
2828
'..',
29-
'13',
30-
'14',
29+
'3',
30+
'4',
31+
'5',
32+
'6',
33+
'7',
34+
'..',
3135
'15',
3236
'16',
3337
'17',
3438
'18',
3539
'19',
36-
'20',
37-
'21',
3840
'..',
39-
'67',
40-
'68',
41-
'69',
42-
'70',
41+
'40',
42+
'41',
43+
'42',
44+
'43',
45+
'44',
46+
'..',
4347
],
4448
},
4549
},
46-
'ringside.story.tsx#L12': {
47-
uri: 'github.com/rylnd/ringside',
48-
filePath: 'stories/ringside.story.tsx',
50+
'User.ts#L8': {
51+
uri: 'github.com/Microsoft/Typescript-Node-Starter',
52+
filePath: 'src/models/User.ts',
4953
language: 'typescript',
5054
compositeContent: {
5155
content:
52-
"\nimport { interpolateRainbow } from 'd3-scale-chromatic';\n\nimport { Ringside } from '../src';\nimport { XAlignment, YAlignment, XBasis, YBasis } from '../src/types';\n\nlet ringside: Ringside;\n\nconst enumKeys: (e: any) => string[] = e =>\n\n\nconst color = position => {\n const combos = ringside.positions().map(p => JSON.stringify(p));\n const hash = combos.indexOf(JSON.stringify(position)) / combos.length;\n\n\n};\n\nconst Stories = storiesOf('Ringside', module).addDecorator(withKnobs);\n\nStories.add('Ringside', () => {\n",
56+
'\nimport mongoose from "mongoose";\n\nexport type UserDocument = mongoose.Document & {\n email: string;\n password: string;\n\n}\n\nconst userSchema = new mongoose.Schema({\n email: { type: String, unique: true },\n password: String,\n\n * Password hash middleware.\n */\nuserSchema.pre("save", function save(next) {\n const user = this as UserDocument;\n if (!user.isModified("password")) { return next(); }\n bcrypt.genSalt(10, (err, salt) => {\n',
5357
lineMapping: [
5458
'..',
59+
'3',
60+
'4',
5561
'5',
5662
'6',
5763
'7',
58-
'8',
59-
'9',
60-
'10',
61-
'11',
62-
'12',
6364
'..',
64-
'14',
65-
'15',
66-
'16',
67-
'17',
68-
'18',
65+
'31',
66+
'32',
67+
'33',
68+
'34',
69+
'35',
6970
'..',
70-
'20',
71-
'21',
72-
'22',
73-
'23',
74-
'24',
71+
'54',
72+
'55',
73+
'56',
74+
'57',
75+
'58',
76+
'59',
7577
'..',
7678
],
7779
},
7880
},
79-
80-
'ringside.story.tsx#L8': {
81-
uri: 'github.com/rylnd/ringside',
82-
filePath: 'stories/ringside.story.tsx',
81+
'passport.ts#L10': {
82+
uri: 'github.com/Microsoft/Typescript-Node-Starter',
83+
filePath: 'src/config/passport.ts',
8384
language: 'typescript',
8485
compositeContent: {
8586
content:
86-
"import { Ringside } from '../src';\n\ndescribe('Ringside', () => {\n let inner;\n let outer;\n let height;\n let width;\n let ringside: Ringside;\n\n beforeEach(() => {\n\n width = 50;\n\n ringside = new Ringside(inner, outer, height, width);\n });\n\n",
87-
lineMapping: [
88-
'1',
89-
'2',
90-
'3',
91-
'4',
92-
'5',
93-
'6',
94-
'7',
95-
'8',
96-
'9',
97-
'10',
98-
'..',
99-
'14',
100-
'15',
101-
'16',
102-
'17',
103-
'18',
104-
'..',
105-
],
87+
'\nimport _ from "lodash";\n\n// import { User, UserType } from \'../models/User\';\nimport { User, UserDocument } from "../models/User";\nimport { Request, Response, NextFunction } from "express";\n\n',
88+
lineMapping: ['..', '4', '5', '6', '7', '8', '9', '..'],
10689
},
10790
},
108-
109-
'ringside.story.tsx#L14': {
110-
uri: 'github.com/rylnd/ringside',
111-
filePath: 'stories/ringside.story.tsx',
91+
'user.test.ts#L3': {
92+
uri: 'github.com/Microsoft/Typescript-Node-Starter',
93+
filePath: 'test/user.test.ts',
94+
language: 'typescript',
95+
compositeContent: {
96+
content:
97+
'import request from "supertest";\nimport app from "../src/app";\nimport { expect } from "chai";\n',
98+
lineMapping: ['1', '2', '3', '..'],
99+
},
100+
},
101+
'app.ts#L60': {
102+
uri: 'github.com/Microsoft/Typescript-Node-Starter',
103+
filePath: 'src/app.ts',
112104
language: 'typescript',
113105
compositeContent: {
114106
content:
115-
"import { Ringside } from '../src';\n\ndescribe('Ringside', () => {\n let inner;\n let outer;\n let height;\n let width;\n let ringside: Ringside;\n\n beforeEach(() => {\n\n width = 50;\n\n ringside = new Ringside(inner, outer, height, width);\n });\n\n",
107+
'\n// Controllers (route handlers)\nimport * as homeController from "./controllers/home";\nimport * as userController from "./controllers/user";\nimport * as apiController from "./controllers/api";\nimport * as contactController from "./controllers/contact";\n\napp.use(lusca.xssProtection(true));\napp.use((req, res, next) => {\n res.locals.user = req.user;\n next();\n});\napp.use((req, res, next) => {\n // After successful login, redirect back to the intended page\n if (!req.user &&\n req.path !== "/login" &&\n req.path !== "/signup" &&\n',
116108
lineMapping: [
117-
'1',
118-
'2',
119-
'3',
120-
'4',
121-
'5',
122-
'6',
123-
'7',
124-
'8',
125-
'9',
126-
'10',
127109
'..',
128-
'14',
129-
'15',
130110
'16',
131111
'17',
132112
'18',
113+
'19',
114+
'20',
115+
'..',
116+
'60',
117+
'61',
118+
'62',
119+
'63',
120+
'64',
121+
'65',
122+
'66',
123+
'67',
124+
'68',
125+
'69',
133126
'..',
134127
],
135128
},
@@ -143,13 +136,14 @@ export interface Frame {
143136
}
144137

145138
export const frames: Frame[] = [
146-
{ fileName: 'ringside.ts', lineNumber: 18 },
147-
{ fileName: 'node_modules/library_code.js', lineNumber: 100 },
148-
{ fileName: 'ringside.story.tsx', lineNumber: 8 },
149-
{ fileName: 'node_modules/other_stuff.js', lineNumber: 58 },
150-
{ fileName: 'node_modules/other/other.js', lineNumber: 3 },
151-
{ fileName: 'ringside.story.tsx', lineNumber: 12 },
152-
{ fileName: 'ringside.story.tsx', lineNumber: 14 },
139+
{ fileName: 'user.ts', lineNumber: 15 },
140+
{ fileName: 'user.ts', lineNumber: 25 },
141+
{ fileName: 'User.ts', lineNumber: 8 },
142+
{ fileName: 'passport.ts', lineNumber: 10 },
143+
{ fileName: 'app.ts', lineNumber: 60 },
144+
{ fileName: 'app.ts', lineNumber: 2 },
145+
{ fileName: 'user.test.ts', lineNumber: 18 },
146+
{ fileName: 'user.test.ts', lineNumber: 3 },
153147
];
154148

155149
export const repos = [

0 commit comments

Comments
 (0)