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