/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { SpectronApplication } from '../../spectron/application'; import { QuickOutline } from './quickoutline'; import { References } from './peek'; const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box'; const RENAME_INPUT = `${RENAME_BOX} .rename-input`; export class Editor { private static readonly VIEW_LINES = '.monaco-editor .view-lines'; private static readonly LINE_NUMBERS = '.monaco-editor .margin .margin-view-overlays .line-numbers'; private static readonly FOLDING_EXPANDED = '.monaco-editor .margin .margin-view-overlays>:nth-child(${INDEX}) .folding'; private static readonly FOLDING_COLLAPSED = `${Editor.FOLDING_EXPANDED}.collapsed`; constructor(private spectron: SpectronApplication) { } async openOutline(): Promise { const outline = new QuickOutline(this.spectron); await outline.open(); return outline; } async findReferences(term: string, line: number): Promise { await this.clickOnTerm(term, line); await this.spectron.workbench.quickopen.runCommand('Find All References'); const references = new References(this.spectron); await references.waitUntilOpen(); return references; } async rename(filename: string, line: number, from: string, to: string): Promise { await this.clickOnTerm(from, line); await this.spectron.workbench.quickopen.runCommand('Rename Symbol'); await this.spectron.client.waitForActiveElement(RENAME_INPUT); await this.spectron.client.setValue(RENAME_INPUT, to); await this.spectron.client.keys(['Enter', 'NULL']); } async gotoDefinition(term: string, line: number): Promise { await this.clickOnTerm(term, line); await this.spectron.workbench.quickopen.runCommand('Go to Definition'); } async peekDefinition(term: string, line: number): Promise { await this.clickOnTerm(term, line); await this.spectron.workbench.quickopen.runCommand('Peek Definition'); const peek = new References(this.spectron); await peek.waitUntilOpen(); return peek; } async waitForHighlightingLine(line: number): Promise { const currentLineIndex = await this.getViewLineIndex(line); if (currentLineIndex) { await this.spectron.client.waitForElement(`.monaco-editor .view-overlays>:nth-child(${currentLineIndex}) .current-line`); return; } throw new Error('Cannot find line ' + line); } async getSelector(term: string, line: number): Promise { const lineIndex = await this.getViewLineIndex(line); const classNames = await this.spectron.client.waitFor(() => this.getClassSelectors(term, lineIndex), classNames => classNames && !!classNames.length, 'Getting class names for editor lines'); return `${Editor.VIEW_LINES}>:nth-child(${lineIndex}) span span.${classNames[0]}`; } async foldAtLine(line: number): Promise { const lineIndex = await this.getViewLineIndex(line); await this.spectron.client.waitAndClick(Editor.FOLDING_EXPANDED.replace('${INDEX}', '' + lineIndex)); await this.spectron.client.waitForElement(Editor.FOLDING_COLLAPSED.replace('${INDEX}', '' + lineIndex)); } async unfoldAtLine(line: number): Promise { const lineIndex = await this.getViewLineIndex(line); await this.spectron.client.waitAndClick(Editor.FOLDING_COLLAPSED.replace('${INDEX}', '' + lineIndex)); await this.spectron.client.waitForElement(Editor.FOLDING_EXPANDED.replace('${INDEX}', '' + lineIndex)); } async waitUntilHidden(line: number): Promise { await this.spectron.client.waitFor(() => this.getViewLineIndexWithoutWait(line), lineNumber => lineNumber === undefined, 'Waiting until line number is hidden'); } async waitUntilShown(line: number): Promise { await this.getViewLineIndex(line); } async clickOnTerm(term: string, line: number): Promise { const selector = await this.getSelector(term, line); await this.spectron.client.waitAndClick(selector); } async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise { const editor = [ selectorPrefix || '', `.monaco-editor[data-uri$="${filename}"]` ].join(' '); await this.spectron.client.element(editor); const textarea = `${editor} textarea`; await this.spectron.client.waitForActiveElement(textarea); // https://github.com/Microsoft/vscode/issues/34203#issuecomment-334441786 await this.spectron.client.spectron.client.selectorExecute(textarea, (elements, text) => { const textarea = (Array.isArray(elements) ? elements : [elements])[0] as HTMLTextAreaElement; const start = textarea.selectionStart; const newStart = start + text.length; const value = textarea.value; const newValue = value.substr(0, start) + text + value.substr(start); textarea.value = newValue; textarea.setSelectionRange(newStart, newStart); const event = new Event('input', { 'bubbles': true, 'cancelable': true }); textarea.dispatchEvent(event); }, text); await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix); } async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise { const selector = [ selectorPrefix || '', `.monaco-editor[data-uri$="${filename}"] .view-lines` ].join(' '); return this.spectron.client.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); } async waitForActiveEditor(filename: string): Promise { const selector = `.editor-container .monaco-editor[data-uri$="${filename}"] textarea`; return this.spectron.client.waitForActiveElement(selector); } // async waitForActiveEditorFirstLineText(filename: string): Promise { // const selector = `.editor-container .monaco-editor[data-uri$="${filename}"] textarea`; // const result = await this.spectron.client.waitFor( // () => this.spectron.client.spectron.client.execute(s => { // if (!document.activeElement.matches(s)) { // return undefined; // } // let element: Element | null = document.activeElement; // while (element && !/monaco-editor/.test(element.className) && element !== document.body) { // element = element.parentElement; // } // if (element && /monaco-editor/.test(element.className)) { // const firstLine = element.querySelector('.view-lines span span:nth-child(1)'); // if (firstLine) { // return (firstLine.textContent || '').replace(/\u00a0/g, ' '); // DAMN // } // } // return undefined; // }, selector), // r => typeof r.value === 'string', // `wait for active editor first line: ${selector}` // ); // return result.value; // } private async getClassSelectors(term: string, viewline: number): Promise { const result: { text: string, className: string }[] = await this.spectron.webclient.selectorExecute(`${Editor.VIEW_LINES}>:nth-child(${viewline}) span span`, elements => (Array.isArray(elements) ? elements : [elements]) .map(element => ({ text: element.textContent, className: element.className }))); return result.filter(r => r.text === term).map(({ className }) => className); } private async getViewLineIndex(line: number): Promise { return await this.spectron.client.waitFor(() => this.getViewLineIndexWithoutWait(line), void 0, 'Getting line index'); } private async getViewLineIndexWithoutWait(line: number): Promise { const lineNumbers = await this.spectron.webclient.selectorExecute(Editor.LINE_NUMBERS, elements => (Array.isArray(elements) ? elements : [elements]).map(element => element.textContent)); for (let index = 0; index < lineNumbers.length; index++) { if (lineNumbers[index] === `${line}`) { return index + 1; } } return undefined; } }