blob: f1608075cd7fa6bf1afe1e33c1da946467becef4 [file] [log] [blame]
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {GrAnnotationActionsContext} from './gr-annotation-actions-context';
import {GrDiffLine} from '../../diff/gr-diff/gr-diff-line';
import {
CoverageRange,
DiffLayer,
DiffLayerListener,
} from '../../../types/types';
import {Side} from '../../../constants/constants';
import {PluginApi} from '../../plugins/gr-plugin-types';
import {ChangeInfo, NumericChangeId} from '../../../types/common';
import {appContext} from '../../../services/app-context';
type AddLayerFunc = (ctx: GrAnnotationActionsContext) => void;
type NotifyFunc = (
path: string,
start: number,
end: number,
side: Side
) => void;
export type CoverageProvider = (
changeNum: NumericChangeId,
path: string,
basePatchNum?: number,
patchNum?: number,
change?: ChangeInfo
) => Promise<Array<CoverageRange>>;
export class GrAnnotationActionsInterface {
// Collect all annotation layers instantiated by getLayer. Will be used when
// notifying their listeners in the notify function.
private annotationLayers: AnnotationLayer[] = [];
private coverageProvider: CoverageProvider | null = null;
// Default impl is a no-op.
private addLayerFunc: AddLayerFunc = () => {};
reporting = appContext.reportingService;
constructor(private readonly plugin: PluginApi) {
// Return this instance when there is an annotatediff event.
plugin.on('annotatediff', this);
}
/**
* Register a function to call to apply annotations. Plugins should use
* GrAnnotationActionsContext.annotateRange and
* GrAnnotationActionsContext.annotateLineNumber to apply a CSS class to the
* line content or the line number.
*
* @param addLayerFunc The function
* that will be called when the AnnotationLayer is ready to annotate.
*/
addLayer(addLayerFunc: AddLayerFunc) {
this.addLayerFunc = addLayerFunc;
return this;
}
/**
* The specified function will be called with a notify function for the plugin
* to call when it has all required data for annotation. Optional.
*
* @param notifyFunc See doc of the notify function below to see what it does.
*/
addNotifier(notifyFunc: (n: NotifyFunc) => void) {
notifyFunc(
(path: string, startRange: number, endRange: number, side: Side) =>
this.notify(path, startRange, endRange, side)
);
return this;
}
/**
* The specified function will be called when a gr-diff component is built,
* and feeds the returned coverage data into the diff. Optional.
*
* Be sure to call this only once and only from one plugin. Multiple coverage
* providers are not supported. A second call will just overwrite the
* provider of the first call.
*/
setCoverageProvider(
coverageProvider: CoverageProvider
): GrAnnotationActionsInterface {
if (this.coverageProvider) {
console.warn('Overwriting an existing coverage provider.');
}
this.coverageProvider = coverageProvider;
return this;
}
/**
* Used by Gerrit to look up the coverage provider. Not intended to be called
* by plugins.
*/
getCoverageProvider() {
return this.coverageProvider;
}
/**
* Returns a checkbox HTMLElement that can be used to toggle annotations
* on/off. The checkbox will be initially disabled. Plugins should enable it
* when data is ready and should add a click handler to toggle CSS on/off.
*
* Note1: Calling this method from multiple plugins will only work for the
* 1st call. It will print an error message for all subsequent calls
* and will not invoke their onAttached functions.
* Note2: This method will be deprecated and eventually removed when
* https://bugs.chromium.org/p/gerrit/issues/detail?id=8077 is
* implemented.
*
* @param checkboxLabel Will be used as the label for the checkbox.
* Optional. "Enable" is used if this is not specified.
* @param onAttached The function that will be called
* when the checkbox is attached to the page.
*/
enableToggleCheckbox(
checkboxLabel: string,
onAttached: (checkboxEl: Element | null) => void
) {
this.plugin.hook('annotation-toggler').onAttached(element => {
if (!element.content) {
this.reporting.error(new Error('plugin endpoint without content.'));
return;
}
if (!element.content.hidden) {
this.reporting.error(
new Error(
`${element.content.id} is already enabled. Cannot re-enable.`
)
);
return;
}
element.content.removeAttribute('hidden');
const label = element.content.querySelector('#annotation-label');
if (label) {
if (checkboxLabel) {
label.textContent = checkboxLabel;
} else {
label.textContent = 'Enable';
}
}
const checkbox = element.content.querySelector('#annotation-checkbox');
onAttached(checkbox);
});
return this;
}
/**
* The notify function will call the listeners of all required annotation
* layers. Intended to be called by the plugin when all required data for
* annotation is available.
*
* @param path The file path whose listeners should be notified.
* @param start The line where the update starts.
* @param end The line where the update ends.
* @param side The side of the update ('left' or 'right').
*/
notify(path: string, start: number, end: number, side: Side) {
for (const annotationLayer of this.annotationLayers) {
// Notify only the annotation layer that is associated with the specified
// path.
if (annotationLayer.path === path) {
annotationLayer.notifyListeners(start, end, side);
}
}
}
/**
* Should be called to register annotation layers by the framework. Not
* intended to be called by plugins.
*
* Don't forget to dispose layer.
*
* @param path The file path (eg: /COMMIT_MSG').
* @param changeNum The Gerrit change number.
*/
getLayer(path: string, changeNum: number) {
const annotationLayer = new AnnotationLayer(
path,
changeNum,
this.addLayerFunc
);
this.annotationLayers.push(annotationLayer);
return annotationLayer;
}
disposeLayer(path: string) {
this.annotationLayers = this.annotationLayers.filter(
annotationLayer => annotationLayer.path !== path
);
}
}
export class AnnotationLayer implements DiffLayer {
private listeners: DiffLayerListener[] = [];
/**
* Used to create an instance of the Annotation Layer interface.
*
* @param path The file path (eg: /COMMIT_MSG').
* @param changeNum The Gerrit change number.
* @param addLayerFunc The function
* that will be called when the AnnotationLayer is ready to annotate.
*/
constructor(
readonly path: string,
private readonly changeNum: number,
private readonly addLayerFunc: AddLayerFunc
) {
this.listeners = [];
}
/**
* Register a listener for layer updates.
* Don't forget to removeListener when you stop using layer.
*
* @param fn The update handler function.
* Should accept as arguments the line numbers for the start and end of
* the update and the side as a string.
*/
addListener(listener: DiffLayerListener) {
this.listeners.push(listener);
}
removeListener(listener: DiffLayerListener) {
this.listeners = this.listeners.filter(f => f !== listener);
}
/**
* Layer method to add annotations to a line.
*
* @param contentEl The DIV.contentText element of the line
* content to apply the annotation to using annotateRange.
* @param lineNumberEl The TD element of the line number to
* apply the annotation to using annotateLineNumber.
* @param line The line object.
*/
annotate(
contentEl: HTMLElement,
lineNumberEl: HTMLElement,
line: GrDiffLine
) {
const annotationActionsContext = new GrAnnotationActionsContext(
contentEl,
lineNumberEl,
line,
this.path,
this.changeNum
);
this.addLayerFunc(annotationActionsContext);
}
/**
* Notify Layer listeners of changes to annotations.
*
* @param start The line where the update starts.
* @param end The line where the update ends.
* @param side The side of the update. ('left' or 'right')
*/
notifyListeners(start: number, end: number, side: Side) {
for (const listener of this.listeners) {
listener(start, end, side);
}
}
}