diff --git a/src/main/webapp/app/entities/build-log.model.ts b/src/main/webapp/app/entities/build-log.model.ts index 014fedf1e664..db33d9a63412 100644 --- a/src/main/webapp/app/entities/build-log.model.ts +++ b/src/main/webapp/app/entities/build-log.model.ts @@ -1,5 +1,5 @@ import { safeUnescape } from 'app/shared/util/security.util'; -import { Annotation } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; +import { Annotation } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; import { ProgrammingLanguage, ProjectType } from 'app/entities/programming-exercise.model'; export enum BuildLogType { diff --git a/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.html b/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.html index c95a68f05f18..af7369cbfbc3 100644 --- a/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.html +++ b/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.html @@ -25,7 +25,6 @@

Online Code Editor

} @if (!loadingParticipation && !participationCouldNotBeFetched) {
diff --git a/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.html b/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.html index 3aea41958c80..fb67088fd00c 100644 --- a/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.html +++ b/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.html @@ -24,7 +24,6 @@ } @if (!loadingParticipation && !participationCouldNotBeFetched) { - -
-
- -
-
- - - @for (feedback of filterFeedbackForFile(feedbacks); track feedback.id) { - - } - - @for (line of newFeedbackLines; track line) { - - } - - @for (suggestion of filterFeedbackForFile(feedbackSuggestions); track suggestion.id) { - - } -
- - @if (!selectedFile && !isLoading) { -

- } -
-
diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.ts deleted file mode 100644 index d59c3ed6201a..000000000000 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.ts +++ /dev/null @@ -1,692 +0,0 @@ -import { UndoManager, acequire } from 'brace'; -import 'brace/ext/language_tools'; -import 'brace/ext/modelist'; -import 'brace/mode/java'; -import 'brace/mode/sh'; -import 'brace/mode/markdown'; -import 'brace/mode/haskell'; -import 'brace/mode/ocaml'; -import 'brace/mode/c_cpp'; -import 'brace/mode/python'; -import 'brace/mode/swift'; -import 'brace/mode/yaml'; -import 'brace/mode/makefile'; -import 'brace/mode/kotlin'; -import 'brace/mode/assembly_x86'; -import 'brace/mode/vhdl'; -import 'brace/theme/dreamweaver'; -import 'brace/theme/dracula'; -import { AceEditorComponent } from 'app/shared/markdown-editor/ace-editor/ace-editor.component'; -import { - AfterViewInit, - ChangeDetectorRef, - Component, - EventEmitter, - Input, - OnChanges, - OnDestroy, - Output, - QueryList, - SimpleChanges, - ViewChild, - ViewChildren, - ViewEncapsulation, -} from '@angular/core'; -import { Subscription, firstValueFrom, fromEvent } from 'rxjs'; -import { CommitState, CreateFileChange, DeleteFileChange, EditorState, FileChange, RenameFileChange } from 'app/exercises/programming/shared/code-editor/model/code-editor.model'; -import { CodeEditorFileService } from 'app/exercises/programming/shared/code-editor/service/code-editor-file.service'; -import { CodeEditorRepositoryFileService, ConnectionError } from 'app/exercises/programming/shared/code-editor/service/code-editor-repository.service'; -import { RepositoryFileService } from 'app/exercises/shared/result/repository.service'; -import { TextChange } from 'app/entities/text-change.model'; -import { LocalStorageService } from 'ngx-webstorage'; -import { fromPairs, pickBy } from 'lodash-es'; -import { FEEDBACK_SUGGESTION_ACCEPTED_IDENTIFIER, FEEDBACK_SUGGESTION_IDENTIFIER, Feedback } from 'app/entities/feedback.model'; -import { Course } from 'app/entities/course.model'; -import { faCircleNotch, faPlusSquare } from '@fortawesome/free-solid-svg-icons'; -import { CodeEditorTutorAssessmentInlineFeedbackComponent } from 'app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component'; -import { CodeEditorTutorAssessmentInlineFeedbackSuggestionComponent } from 'app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback-suggestion.component'; - -export type Annotation = { fileName: string; row: number; column: number; text: string; type: string; timestamp: number; hash?: string }; -export type FileSession = { [fileName: string]: { code: string; cursor: { column: number; row: number }; loadingError: boolean } }; - -@Component({ - selector: 'jhi-code-editor-ace', - templateUrl: './code-editor-ace.component.html', - styleUrls: ['./code-editor-ace.scss'], - encapsulation: ViewEncapsulation.None, - providers: [RepositoryFileService], -}) -export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestroy { - @ViewChild('editor', { static: true }) - editor: AceEditorComponent; - @ViewChildren(CodeEditorTutorAssessmentInlineFeedbackComponent) - inlineFeedbackComponents: QueryList; - @ViewChildren(CodeEditorTutorAssessmentInlineFeedbackSuggestionComponent) - inlineFeedbackSuggestionsComponents: QueryList; - @Input() - selectedFile: string; - @Input() - sessionId: number | string; - @Input() - readOnlyManualFeedback: boolean; - - @Input() - set annotations(annotations: Array) { - this.setAnnotations(annotations); - } - - @Input() - readonly commitState: CommitState; - @Input() - readonly editorState: EditorState; - @Input() - isTutorAssessment = false; - @Input() - feedbacks: Feedback[] = []; - @Input() - feedbackSuggestions: Feedback[] = []; - @Input() - highlightDifferences: boolean; - @Input() - course?: Course; - @Input() - disableActions = false; - - @Output() - onFileContentChange = new EventEmitter<{ file: string; fileContent: string }>(); - @Output() - onError = new EventEmitter(); - @Output() - onUpdateFeedback = new EventEmitter(); - @Output() - onFileLoad = new EventEmitter(); - @Output() - onAcceptSuggestion = new EventEmitter(); - @Output() - onDiscardSuggestion = new EventEmitter(); - - // This fetches a list of all supported editor modes and matches it afterwards against the file extension - readonly aceModeList = acequire('ace/ext/modelist'); - // Line widgets for inline feedback - readonly LineWidgets = acequire('ace/line_widgets').LineWidgets; - - readonly Range = acequire('ace/range').Range; - - /** Ace Editor Options **/ - editorMode: string; // string or mode object - isLoading = false; - annotationsArray: Array = []; - annotationChange: Subscription; - fileSession: FileSession = {}; - newFeedbackLines: number[] = []; // new feedback, which is not yet saved, but must already be displayed to be editable - editorSession: any; - markerIds: number[] = []; - gutterHighlights: Map = new Map(); - tabSize = 4; - - // Detecting editor resize events - resizeObserver: ResizeObserver; - - // Icons - readonly faPlusSquare = faPlusSquare; - readonly faCircleNotch = faCircleNotch; - - // Expose to template - protected readonly Feedback = Feedback; - - /** - * Filter feedback: Only referenced to current file - */ - filterFeedbackForFile(feedbacks: Feedback[]): Feedback[] { - if (!this.selectedFile) { - return []; - } - return feedbacks.filter((feedback) => feedback.reference && Feedback.getReferenceFilePath(feedback) === this.selectedFile); - } - - constructor( - private repositoryFileService: CodeEditorRepositoryFileService, - private fileService: CodeEditorFileService, - protected localStorageService: LocalStorageService, - private changeDetectorRef: ChangeDetectorRef, - ) {} - - /** - * Set the theme and other editor options - */ - ngAfterViewInit(): void { - this.editor.getEditor().setOptions({ - animatedScroll: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true, - }); - if (this.isTutorAssessment) { - this.editor.getEditor().setShowFoldWidgets(false); - } - if (!this.resizeObserver) { - // When the editor is resized in width, the inline feedback text could wrap. This would cause its height to change and we need to adapt it: - this.resizeObserver = new ResizeObserver(() => { - if (!this.editorSession) return; // not yet initialized, nothing to do - // Adjust height of all feedback line widgets - this.updateLineWidgets(); - }); - this.resizeObserver.observe(this.editor.elementRef.nativeElement); - } - } - - /** - * New clean state => reset the editor and file update subscriptions - * New selectedFile => load the file from the repository and open it in the editor - * @param {SimpleChanges} changes - */ - async ngOnChanges(changes: SimpleChanges): Promise { - if ( - (changes.commitState && changes.commitState.previousValue !== CommitState.UNDEFINED && this.commitState === CommitState.UNDEFINED) || - (changes.editorState && changes.editorState.previousValue === EditorState.REFRESHING && this.editorState === EditorState.CLEAN) - ) { - this.fileSession = {}; - if (this.annotationChange) { - this.annotationChange.unsubscribe(); - } - this.editor.getEditor().getSession().setValue(''); - } - if ( - (changes.selectedFile && this.selectedFile) || - (changes.editorState && changes.editorState.previousValue === EditorState.REFRESHING && this.editorState === EditorState.CLEAN) - ) { - // Current selected file has changed - if (this.selectedFile) { - await this.ensureLoadedThenInitEditorIfSelected(this.selectedFile); - } else { - await this.initEditor(); - } - } - if (changes.commitState && changes.commitState.currentValue === CommitState.CONFLICT) { - this.editor.setReadOnly(true); - } else if (changes.commitState && changes.commitState.previousValue === CommitState.CONFLICT && changes.commitState.currentValue !== CommitState.CONFLICT) { - this.editor.setReadOnly(false); - } - if ((changes.feedbacks || changes.feedbackSuggestions) && this.editorSession) { - // when feedback/suggestions change and initEditor was already called - await this.updateLineWidgets(); - } - } - - /** - * Set up the ace editor after a file change occurred or the file finished loading. - * Makes sure potential previous settings are restored and the correct language service is used. - **/ - async initEditor() { - // Set up editorSession for inline feedback using lineWidgets - this.editorSession = this.editor.getEditor().getSession(); - - if (!this.editorSession.widgetManager) { - this.editorSession.widgetManager = new this.LineWidgets(this.editorSession); - this.editorSession.widgetManager.attach(this.editor.getEditor()); - } - // We first remove the annotationChange subscription so the initial setValue doesn't count as an insert - if (this.annotationChange) { - this.annotationChange.unsubscribe(); - } - if (this.selectedFile && this.fileSession[this.selectedFile]) { - this.editor.getEditor().getSession().setValue(this.fileSession[this.selectedFile].code); - this.annotationChange = fromEvent(this.editor.getEditor().getSession(), 'change').subscribe(([change]) => { - this.updateAnnotations(change); - }); - - // Restore the previous cursor position - this.editor.getEditor().moveCursorToPosition(this.fileSession[this.selectedFile].cursor); - this.editorMode = this.aceModeList.getModeForPath(this.selectedFile).name; - this.editor.setMode(this.editorMode); - this.editor.getEditor().resize(); - this.editor.getEditor().focus(); - // always scroll to the top, otherwise inline annotations might be placed incorrectly - this.editor.getEditor().getSession().setScrollTop(0); - // Reset the undo stack after file change, otherwise the user can undo back to the old file - this.editor.getEditor().getSession().setUndoManager(new UndoManager()); - this.displayAnnotations(); - - if (this.markerIds.length > 0) { - this.markerIds.forEach((markerId) => this.editorSession.removeMarker(markerId)); - this.markerIds = []; - } - if (this.gutterHighlights.size > 0) { - this.gutterHighlights.forEach((classes, row) => classes.forEach((className) => this.editorSession.removeGutterDecoration(row, className))); - this.gutterHighlights.clear(); - } - this.onFileLoad.emit(this.selectedFile); - } - // Remove open inline feedback widgets for new feedback - this.newFeedbackLines = []; - // Remove previous lineWidgets and create new ones - await this.updateLineWidgets(); - } - - /** - * Fetches the content of the file with the specified filename from the server. - * Errors must be handled by the caller with a catch block. - * @param fileName Name of the file to be fetched - */ - async fetchFileContent(fileName: string): Promise { - return firstValueFrom(this.repositoryFileService.getFile(fileName)).then((fileObj) => fileObj.fileContent); - } - - /** - * Reloads the file with the specified filename from the server and overwrites the file session. - * If there is an error loading the file, the file session is still updated, but the loadingError flag is set to true. - * @param fileName Name of the file to be reloaded - */ - async forceReloadFileContents(fileName: string) { - return this.fetchFileContent(fileName) - .then((code) => { - this.fileSession[fileName] = { code, cursor: { column: 0, row: 0 }, loadingError: false }; - }) - .catch((error) => { - this.fileSession[fileName] = { code: '', cursor: { column: 0, row: 0 }, loadingError: true }; - if (error.message === ConnectionError.message) { - this.onError.emit('loadingFailed' + error.message); - } else { - this.onError.emit('loadingFailed'); - } - }); - } - - /** - * Ensures that the file with the specified filename exists in the file session. - * If it doesn't, it is fetched from the server and added to the file session. - * If there is an error loading the file, the file session is still updated, but the loadingError flag is set to true. - * @param fileName Name of the file to be loaded - */ - async ensureLoadedThenInitEditorIfSelected(fileName: string): Promise { - // If the file is not yet loaded or loading failed, fetch it from the server - // If the file is already loaded, we don't need to load it again - if (!this.fileSession[fileName] || this.fileSession[fileName].loadingError) { - this.isLoading = true; - await this.forceReloadFileContents(fileName); - this.isLoading = false; - } - - // Only initialize the editor if the selected file has not changed in between - // - prevents console errors (see https://github.com/ls1intum/Artemis/pull/603) - if (this.selectedFile === fileName) { - await this.initEditor(); - } - } - - async forceReloadAll(fileNames: string[]) { - this.isLoading = true; - await Promise.all( - fileNames.map(async (fileName) => { - await this.forceReloadFileContents(fileName); - if (this.selectedFile === fileName) { - await this.initEditor(); - } - }), - ); - this.isLoading = false; - } - - /** - * Callback function for text changes in the Ace Editor. - * Is used for updating the error annotations in the editor and giving the touched file the unsaved flag. - * @param event {string} Current editor code - */ - async onFileTextChanged(event: any) { - const code = event as string; - if (this.isTutorAssessment) { - this.editor.setReadOnly(true); - if (!this.readOnlyManualFeedback) { - this.setupLineIcons(); - } - await this.updateLineWidgets(); - } - /** Is the code different to what we have on our session? This prevents us from saving when a file is loaded **/ - if (this.selectedFile && this.fileSession[this.selectedFile]) { - if (this.fileSession[this.selectedFile].code !== code) { - const cursor = this.editor.getEditor().getCursorPosition(); - this.fileSession[this.selectedFile] = { code, cursor, loadingError: false }; - this.onFileContentChange.emit({ file: this.selectedFile, fileContent: code }); - } - } - } - - ngOnDestroy() { - if (this.annotationChange) { - this.annotationChange.unsubscribe(); - } - // Remove all line widgets so that no old ones are displayed when the editor is opened again - for (const widget of this.editorSession?.lineWidgets ?? []) { - if (widget) { - this.editorSession.widgetManager.removeLineWidget(widget); - } - } - this.resizeObserver?.disconnect(); - } - - /** - * Recalculate the position of annotations according to changes in the editor. - * Annotations are affected by changes in previous rows for row updates, - * in the same row and previous columns for column updates. - * @param change - */ - updateAnnotations(change: TextChange) { - const { - start: { row: rowStart, column: columnStart }, - end: { row: rowEnd, column: columnEnd }, - action, - } = change; - if (action === 'remove' || action === 'insert') { - const sign = action === 'remove' ? -1 : 1; - const updateRowDiff = sign * (rowEnd - rowStart); - const updateColDiff = sign * (columnEnd - columnStart); - - this.annotationsArray = this.annotationsArray.map((a) => { - return this.selectedFile !== a.fileName - ? a - : { - ...a, - row: a.row > rowStart ? a.row + updateRowDiff : a.row, - column: a.column > columnStart && a.row === rowStart && a.row === rowEnd ? a.column + updateColDiff : a.column, - }; - }); - this.displayAnnotations(); - } - } - - /** - * Set the annotations for the editor. - * Checks for each annotation whether an updated version exists in local storage. - * @param annotations The new annotations array - */ - setAnnotations(annotations: Array = []) { - if (annotations.length > 0) { - const sessionAnnotations = this.loadAnnotations(); - this.annotationsArray = annotations.map((a) => { - const hash = a.fileName + a.row + a.column + a.text; - if (sessionAnnotations[hash] == undefined || sessionAnnotations[hash].timestamp < a.timestamp) { - return { ...a, hash }; - } else { - return sessionAnnotations[hash]; - } - }); - } else { - this.annotationsArray = annotations; - } - - this.displayAnnotations(); - } - - /** - * Highlights lines using a marker and / or a gutter decorator. - * @param firstLine the first line to highlight - * @param lastLine the last line to highlight - * @param lineHightlightClassName the classname to use for the highlight of the line in the editor content area, or undefined if it should not be highlighted - * @param gutterHightlightClassName the classname to use for the highlight of the line number gutter, or undefined if the gutter should not be highlighted - */ - highlightLines(firstLine: number, lastLine: number, lineHightlightClassName: string | undefined, gutterHightlightClassName: string | undefined) { - if (lineHightlightClassName) { - this.markerIds.push(this.editorSession.addMarker(new this.Range(firstLine, 0, lastLine, 1), lineHightlightClassName, 'fullLine')); - } - if (gutterHightlightClassName) { - for (let i = firstLine; i <= lastLine; ++i) { - this.editorSession.addGutterDecoration(i, gutterHightlightClassName); - this.gutterHighlights.computeIfAbsent(i, () => []).push(gutterHightlightClassName); - } - } - } - - /** - * Updates the fileSession and annotations objects for a file change. This function is called - * by the parent container. - * @param fileChange - */ - async onFileChange(fileChange: FileChange) { - if (fileChange instanceof RenameFileChange) { - this.fileSession = this.fileService.updateFileReferences(this.fileSession, fileChange); - for (const a of this.annotationsArray) { - if (a.fileName === fileChange.oldFileName) { - a.fileName = fileChange.newFileName; - } - } - this.storeAnnotations([fileChange.newFileName]); - } else if (fileChange instanceof DeleteFileChange) { - this.fileSession = this.fileService.updateFileReferences(this.fileSession, fileChange); - this.annotationsArray = this.annotationsArray.filter((a) => a.fileName === fileChange.fileName); - this.storeAnnotations([fileChange.fileName]); - } else if (fileChange instanceof CreateFileChange && this.selectedFile === fileChange.fileName) { - this.fileSession = { ...this.fileSession, [fileChange.fileName]: { code: '', cursor: { row: 0, column: 0 }, loadingError: false } }; - await this.initEditor(); - } - this.displayAnnotations(); - } - - /** - * Saves the updated annotations to local storage - * @param savedFiles - */ - storeAnnotations(savedFiles: Array) { - const toUpdate = fromPairs(this.annotationsArray.filter((a) => savedFiles.includes(a.fileName)).map((a) => [a.hash, a])); - const toKeep = pickBy(this.loadAnnotations(), (a) => !savedFiles.includes(a.fileName)); - - this.localStorageService.store( - 'annotations-' + this.sessionId, - JSON.stringify({ - ...toKeep, - ...toUpdate, - }), - ); - } - - /** - * Loads annotations from local storage - */ - loadAnnotations() { - return JSON.parse(this.localStorageService.retrieve('annotations-' + this.sessionId) || '{}'); - } - - /** - * Updates the annotations in the editor - */ - displayAnnotations() { - this.editor - .getEditor() - .getSession() - .setAnnotations(this.annotationsArray.filter((a) => a.fileName === this.selectedFile)); - } - - /** - * Add the inline comment button to all visible gutters in the ace editor. - * We use a MutualObserver to check if children of the gutter layer changes - * in order to add the button to all gutters. - */ - setupLineIcons() { - const gutterContainer = document.querySelector('.ace_gutter-layer'); - const container = this.editor.getEditor().container; - this.observerDom(gutterContainer!, () => { - const gutters = container.querySelectorAll('.ace_gutter-cell'); - const buttonInlineComment = document.querySelector('.btn-inline-comment'); - gutters.forEach((gutter: HTMLElement) => { - const clone = buttonInlineComment!.cloneNode(true); - clone.addEventListener('click', async () => { - const lineToAddFeedbackIn = parseInt(gutter.innerText) - 1; - this.newFeedbackLines.push(lineToAddFeedbackIn); - await this.updateLineWidgets(); - }); - // TODO: Check whether this causes an issue when having annotations - if (gutter.childElementCount < 1) { - gutter.appendChild(clone); - } - }); - }); - } - - /** - * Get the component of an inline feedback DOM node. - * @param line line of code where the feedback inline component is located - */ - getInlineFeedbackNode(line: number): Element | undefined { - return [...this.inlineFeedbackComponents, ...this.inlineFeedbackSuggestionsComponents].find((c) => c.codeLine === line)?.elementRef?.nativeElement; - } - - /** - * Add lineWidget for specific line of code. - * (ACE line widget, needs to be manually handled because ACE is not well integrated into Angular) - * @param line line of code where the feedback inline component will be added to. - */ - addLineWidgetWithFeedback(line: number) { - // Get the feedback DOM node - const inlineFeedbackNode = this.getInlineFeedbackNode(line); - if (!inlineFeedbackNode) { - throw new Error("Couldn't find inline feedback node for line " + line + '. Make sure that it is rendered by Angular.'); - } - const lineWidget = { - row: line, - fixedWidth: true, - coverGutter: true, - el: inlineFeedbackNode, - }; - const displayedWidget = (this.editorSession.lineWidgets ?? [])[line]; - if (!displayedWidget) { - this.editorSession.widgetManager.addLineWidget(lineWidget); - lineWidget.el.querySelector('textarea')?.focus(); - } - } - - /** - * Add lineWidget for specific line of code. - * (ACE line widget, needs to be manually handled because ACE is not well integrated into Angular) - * @param line line of code where the feedback inline component will be added to. - */ - removeLineWidget(line: number) { - const widget = (this.editorSession.lineWidgets ?? [])[line]; - if (widget) { - this.editorSession.widgetManager.removeLineWidget(widget); - } - } - - /** - * Sync the line widgets of the ACE editor to the current feedbacks. - */ - async updateLineWidgets() { - // Make Angular update the DOM - this.changeDetectorRef.detectChanges(); - // Wait for one render cycle for all DOM updates to be done - await new Promise((resolve) => setTimeout(resolve, 0)); - // Remove all existing line widgets - const linesWithWidgets = this.editorSession.lineWidgets?.map((l: any) => l?.row).filter((row: number | undefined) => row != undefined) ?? []; - for (const line of linesWithWidgets) { - this.removeLineWidget(line); - } - // Add line widgets for all feedbacks - for (const feedback of this.filterFeedbackForFile([...this.feedbacks, ...this.feedbackSuggestions])) { - const line = Feedback.getReferenceLine(feedback)!; - this.addLineWidgetWithFeedback(line); - } - // Add line widgets for new feedback that is not yet saved - for (const line of this.newFeedbackLines) { - this.addLineWidgetWithFeedback(line); - } - } - - /** - * Adjusts the height of the ace editor after the lineWidget (inline feedback component) changed size - * @param line Line of code which has inline feedback (lineWidget) - */ - adjustLineWidgetHeight(line: number) { - const widget = this.editorSession.lineWidgets[line]; - this.editorSession.widgetManager.removeLineWidget(widget); - this.editorSession.widgetManager.addLineWidget(widget); - } - - /** - * Called whenever an inline feedback element is emitted. Updates existing feedbacks or adds onto it - * @param feedback Newly created inline feedback. - */ - async updateFeedback(feedback: Feedback) { - const line = Feedback.getReferenceLine(feedback)!; - // Check if feedback already exists and update it, else append it to feedbacks of the file - if (this.feedbacks.some((f) => f.reference === feedback.reference)) { - const index = this.feedbacks.findIndex((f) => f.reference === feedback.reference); - this.feedbacks[index] = feedback; - } else { - this.feedbacks.push(feedback); - // the feedback isn't new anymore - this.newFeedbackLines = this.newFeedbackLines.filter((l) => l !== line); - } - await this.updateLineWidgets(); - this.onUpdateFeedback.emit(this.feedbacks); - } - - /** - * Called whenever an inline feedback is cancelled. Removes it from ace editor or just aligns height. - * @param line - */ - async cancelFeedback(line: number) { - if (this.newFeedbackLines.includes(line)) { - // Just remove the new feedback - this.newFeedbackLines = this.newFeedbackLines.filter((l) => l !== line); - await this.updateLineWidgets(); - } else { - // Feedback exists (not new) and we keep it, just re-align height - this.adjustLineWidgetHeight(line); - } - } - - /** - * Deletes a feedback from the feedbacks - * @param feedback Feedback to be removed - */ - deleteFeedback(feedback: Feedback) { - this.feedbacks = this.feedbacks.filter((f) => !Feedback.areIdentical(f, feedback)); - this.removeLineWidget(Feedback.getReferenceLine(feedback)!); - this.onUpdateFeedback.emit(this.feedbacks); - } - - /** - * Accept a feedback suggestion: Make it "real" feedback and remove the suggestion card - */ - async acceptSuggestion(feedback: Feedback) { - this.feedbackSuggestions = this.feedbackSuggestions.filter((f) => f !== feedback); // Remove the suggestion card - this.removeLineWidget(Feedback.getReferenceLine(feedback)!); - // Change the prefix "FeedbackSuggestion:" to "FeedbackSuggestion:accepted:" - feedback.text = (feedback.text ?? FEEDBACK_SUGGESTION_IDENTIFIER).replace(FEEDBACK_SUGGESTION_IDENTIFIER, FEEDBACK_SUGGESTION_ACCEPTED_IDENTIFIER); - await this.updateFeedback(feedback); // Make it "real" feedback - this.onAcceptSuggestion.emit(feedback); - } - - /** - * Discard a feedback suggestion: Remove the suggestion card and emit the event - */ - discardSuggestion(feedback: Feedback) { - this.feedbackSuggestions = this.feedbackSuggestions.filter((f) => f !== feedback); // Remove the suggestion card - this.removeLineWidget(Feedback.getReferenceLine(feedback)!); - this.onDiscardSuggestion.emit(feedback); - } - - /** - * Observes whether the children of the DOM element change. - * We need this to detect changes in the gutter layer, to add the inline button to each gutter. - * @param obj The DOM element on which we check if children change. - * @param callback The method to be called when a change is detected. - */ - observerDom(obj: Element, callback: () => void) { - if (!obj || obj.nodeType !== 1) { - return; - } - if (MutationObserver) { - // define a new observer - const mutationObserver = new MutationObserver(callback); - - // have the observer observe the gutter layer for changes in children - mutationObserver.observe(obj, { childList: true, subtree: true }); - } else { - obj.addEventListener('DOMNodeInserted', callback, false); - obj.addEventListener('DOMNodeRemoved', callback, false); - } - callback(); - } - - updateTabSize(event: number) { - this.tabSize = event; - } -} diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.scss b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.scss deleted file mode 100644 index 2f196de01083..000000000000 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.scss +++ /dev/null @@ -1,80 +0,0 @@ -/*!* ========================================================================== -Ace Editor -========================================================================== *!*/ - -.code-editor-ace { - flex: 1 1 auto; - display: flex; - &__content { - padding: 0; - display: flex; - flex: 1 1 auto; - &__editor { - flex: 1 1 auto; - } - - .inline-feedback { - display: block; - } - - .btn-inline-comment { - display: none; - color: var(--primary); - height: 16px; - } - - .ace_gutter-cell { - position: relative; - } - - .ace_gutter-cell:hover .btn-inline-comment { - display: inline-flex; - position: absolute; - top: 8px; // standard gutter height is 16px - left: 25%; - transform: translate(-50%, -50%); - cursor: pointer; - - fa-icon { - position: relative; - - &::after { - content: ''; - position: absolute; - width: calc(100% - 6px); - height: calc(100% - 6px); - top: 3px; - left: 3px; - background: white; - z-index: -1; - } - } - } - - .ace_gutter-cell.ace_warning, - .ace_gutter-cell.ace_error { - background-position: unset; - } - - &__no-selected { - justify-self: center; - width: 100%; - } - } - - .card-body > .inline-feedback { - /* The ACE Line Widget will copy this node to another place where it will be visible */ - display: none; - } - - .diff-newLine { - position: absolute; - background: var(--code-editor-diff-newline-background); - z-index: 20; - } - - .ace_gutter-cell.gutter-diff-newLine { - transition: background-color 0s; - background-color: var(--code-editor-gutter-newline-background); - } -} diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component.ts index 264fb2bf79f3..d4506dbc256b 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component.ts @@ -11,7 +11,6 @@ import { ResultService } from 'app/exercises/shared/result/result.service'; import { Result } from 'app/entities/result.model'; import { Interactable } from '@interactjs/core/Interactable'; import interact from 'interactjs'; -import { Annotation } from '../ace/code-editor-ace.component'; import { ProgrammingSubmission } from 'app/entities/programming-submission.model'; import { findLatestResult } from 'app/shared/util/utils'; import { StaticCodeAnalysisIssue } from 'app/entities/static-code-analysis-issue.model'; @@ -19,6 +18,7 @@ import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; import { faChevronDown, faCircleNotch, faTerminal } from '@fortawesome/free-solid-svg-icons'; import { hasParticipationChanged } from 'app/exercises/shared/participation/participation.utils'; import { AssessmentType } from 'app/entities/assessment-type.model'; +import { Annotation } from '../monaco/code-editor-monaco.component'; @Component({ selector: 'jhi-code-editor-build-output', diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/code-editor.module.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/code-editor.module.ts index 4bba62661e4a..d403afbfa572 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/code-editor.module.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/code-editor.module.ts @@ -4,7 +4,6 @@ import { CodeEditorBuildOutputComponent } from 'app/exercises/programming/shared import { CodeEditorGridComponent } from 'app/exercises/programming/shared/code-editor/layout/code-editor-grid.component'; import { CodeEditorActionsComponent } from 'app/exercises/programming/shared/code-editor/actions/code-editor-actions.component'; import { CodeEditorFileBrowserFolderComponent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser-folder.component'; -import { CodeEditorAceComponent } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; import { CodeEditorFileBrowserDeleteComponent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser-delete'; import { CodeEditorInstructionsComponent } from 'app/exercises/programming/shared/code-editor/instructions/code-editor-instructions.component'; import { CodeEditorResolveConflictModalComponent } from 'app/exercises/programming/shared/code-editor/actions/code-editor-resolve-conflict-modal.component'; @@ -48,7 +47,6 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo CodeEditorFileBrowserFolderComponent, CodeEditorFileBrowserBadgeComponent, CodeEditorFileBrowserCreateNodeComponent, - CodeEditorAceComponent, CodeEditorBuildOutputComponent, CodeEditorInstructionsComponent, CodeEditorStatusComponent, @@ -62,7 +60,6 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo exports: [ CodeEditorGridComponent, CodeEditorRepositoryIsLockedComponent, - CodeEditorAceComponent, CodeEditorFileBrowserComponent, CodeEditorActionsComponent, CodeEditorInstructionsComponent, diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.html b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.html index d7b96f5511c8..f95022f3c03a 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.html +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.html @@ -42,49 +42,26 @@

(onToggleCollapse)="onToggleCollapse($event, CollapsableCodeEditorElement.FileBrowser)" /> - @if (!useMonacoEditor) { - - } @else { - - } + @if (showEditorInstructions) { diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts index 731c4e7a2cc8..cb89fe997152 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts @@ -20,13 +20,12 @@ import { AlertService } from 'app/core/util/alert.service'; import { CodeEditorFileBrowserComponent, InteractableEvent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component'; import { CodeEditorActionsComponent } from 'app/exercises/programming/shared/code-editor/actions/code-editor-actions.component'; import { CodeEditorBuildOutputComponent } from 'app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component'; -import { Annotation, CodeEditorAceComponent } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; import { Participation } from 'app/entities/participation/participation.model'; import { CodeEditorInstructionsComponent } from 'app/exercises/programming/shared/code-editor/instructions/code-editor-instructions.component'; import { Feedback } from 'app/entities/feedback.model'; import { Course } from 'app/entities/course.model'; import { ConnectionError } from 'app/exercises/programming/shared/code-editor/service/code-editor-repository.service'; -import { CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; +import { Annotation, CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; export enum CollapsableCodeEditorElement { FileBrowser, @@ -48,8 +47,7 @@ export class CodeEditorContainerComponent implements OnChanges, ComponentCanDeac @ViewChild(CodeEditorFileBrowserComponent, { static: false }) fileBrowser: CodeEditorFileBrowserComponent; @ViewChild(CodeEditorActionsComponent, { static: false }) actions: CodeEditorActionsComponent; @ViewChild(CodeEditorBuildOutputComponent, { static: false }) buildOutput: CodeEditorBuildOutputComponent; - @ViewChild(CodeEditorAceComponent, { static: false }) aceEditor?: CodeEditorAceComponent; - @ViewChild(CodeEditorMonacoComponent, { static: false }) monacoEditor?: CodeEditorMonacoComponent; + @ViewChild(CodeEditorMonacoComponent, { static: false }) monacoEditor: CodeEditorMonacoComponent; @ViewChild(CodeEditorInstructionsComponent, { static: false }) instructions: CodeEditorInstructionsComponent; @Input() @@ -74,8 +72,6 @@ export class CodeEditorContainerComponent implements OnChanges, ComponentCanDeac highlightDifferences: boolean; @Input() disableAutoSave = false; - @Input() - useMonacoEditor = false; @Output() onResizeEditorInstructions = new EventEmitter(); @@ -205,8 +201,7 @@ export class CodeEditorContainerComponent implements OnChanges, ComponentCanDeac if (_isEmpty(this.unsavedFiles) && this.editorState === EditorState.UNSAVED_CHANGES) { this.editorState = EditorState.CLEAN; } - this.monacoEditor?.onFileChange(fileChange); - this.aceEditor?.onFileChange(fileChange); + this.monacoEditor.onFileChange(fileChange); this.onFileChanged.emit(); } @@ -229,8 +224,7 @@ export class CodeEditorContainerComponent implements OnChanges, ComponentCanDeac if (errorFiles.length) { this.onError('saveFailed'); } - this.aceEditor?.storeAnnotations(savedFiles); - this.monacoEditor?.storeAnnotations(savedFiles); + this.monacoEditor.storeAnnotations(savedFiles); } /** @@ -277,21 +271,20 @@ export class CodeEditorContainerComponent implements OnChanges, ComponentCanDeac } getText(): string { - return this.monacoEditor?.getText() ?? ''; + return this.monacoEditor.getText() ?? ''; } getNumberOfLines(): number { - if (this.aceEditor) { - return this.aceEditor.editorSession.getLength(); - } - return this.monacoEditor?.getNumberOfLines() ?? 0; + return this.monacoEditor.getNumberOfLines() ?? 0; } + /** + * Highlights the line range in the Monaco editor. + * @param startLine The first line to highlight. Line numbers start at 1. + * @param endLine The last line to highlight. + */ highlightLines(startLine: number, endLine: number): void { - // Workaround: increase line number by 1 for monaco - // Will be removed once ace is gone from every instance of this component - this.monacoEditor?.highlightLines(startLine + 1, endLine + 1); - this.aceEditor?.highlightLines(startLine, endLine, 'diff-newLine', 'gutter-diff-newLine'); + this.monacoEditor.highlightLines(startLine, endLine); } // displays the alert for confirming refreshing or closing the page if there are unsaved changes @@ -310,9 +303,6 @@ export class CodeEditorContainerComponent implements OnChanges, ComponentCanDeac if (type === ResizeType.SIDEBAR_RIGHT || type === ResizeType.MAIN_BOTTOM) { this.onResizeEditorInstructions.emit(); } - if (this.aceEditor && (type === ResizeType.SIDEBAR_LEFT || type === ResizeType.SIDEBAR_RIGHT || type === ResizeType.MAIN_BOTTOM)) { - this.aceEditor.editor.getEditor().resize(); - } } /** diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.html b/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.html index bf9bec10bdf5..51aa9a9c8389 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.html +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.html @@ -57,7 +57,6 @@ [textChangedEmitDelay]="200" #editor [id]="'monaco-editor-' + sessionId" - class="code-editor-ace__content__editor" /> @if (!selectedFile && !isLoading) { diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.ts index 318d2b1306fa..e089c547930c 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component.ts @@ -18,7 +18,6 @@ import { CodeEditorFileService } from 'app/exercises/programming/shared/code-edi import { LocalStorageService } from 'ngx-webstorage'; import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.component'; import { firstValueFrom } from 'rxjs'; -import { Annotation, FileSession } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; import { FEEDBACK_SUGGESTION_ACCEPTED_IDENTIFIER, FEEDBACK_SUGGESTION_IDENTIFIER, Feedback } from 'app/entities/feedback.model'; import { Course } from 'app/entities/course.model'; import { CodeEditorTutorAssessmentInlineFeedbackComponent } from 'app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component'; @@ -37,6 +36,8 @@ import { CodeEditorTutorAssessmentInlineFeedbackSuggestionComponent } from 'app/ import { MonacoEditorLineHighlight } from 'app/shared/monaco-editor/model/monaco-editor-line-highlight.model'; import { FileTypeService } from 'app/exercises/programming/shared/service/file-type.service'; +type FileSession = { [fileName: string]: { code: string; cursor: { column: number; row: number }; loadingError: boolean } }; +export type Annotation = { fileName: string; row: number; column: number; text: string; type: string; timestamp: number; hash?: string }; @Component({ selector: 'jhi-code-editor-monaco', templateUrl: './code-editor-monaco.component.html', @@ -380,9 +381,6 @@ export class CodeEditorMonacoComponent implements OnChanges { this.setBuildAnnotations(this.annotationsArray); } - /* - * Taken from code-editor-ace.component.ts - */ /** * Saves the updated annotations to local storage * @param savedFiles diff --git a/src/main/webapp/app/localvc/repository-view/repository-view.component.html b/src/main/webapp/app/localvc/repository-view/repository-view.component.html index 876027ad3372..57dd26df8d29 100644 --- a/src/main/webapp/app/localvc/repository-view/repository-view.component.html +++ b/src/main/webapp/app/localvc/repository-view/repository-view.component.html @@ -26,7 +26,6 @@

Source

{ - let comp: CodeEditorAceComponent; - let fixture: ComponentFixture; - let debugElement: DebugElement; - let codeEditorRepositoryFileService: CodeEditorRepositoryFileService; - let loadRepositoryFileStub: jest.SpyInstance; - let getInlineFeedbackNodeStub: jest.SpyInstance; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ArtemisTestModule, AceEditorModule], - declarations: [ - CodeEditorAceComponent, - TranslatePipeMock, - MockComponent(CodeEditorTutorAssessmentInlineFeedbackComponent), - MockComponent(CodeEditorTutorAssessmentInlineFeedbackSuggestionComponent), - MockComponent(CodeEditorHeaderComponent), - MockDirective(NgModel), - ], - providers: [ - CodeEditorFileService, - { provide: CodeEditorRepositoryFileService, useClass: MockCodeEditorRepositoryFileService }, - { provide: LocalStorageService, useClass: MockLocalStorageService }, - ], - }) - .compileComponents() - .then(() => { - fixture = TestBed.createComponent(CodeEditorAceComponent); - comp = fixture.componentInstance; - debugElement = fixture.debugElement; - codeEditorRepositoryFileService = debugElement.injector.get(CodeEditorRepositoryFileService); - loadRepositoryFileStub = jest.spyOn(codeEditorRepositoryFileService, 'getFile'); - getInlineFeedbackNodeStub = jest.spyOn(comp, 'getInlineFeedbackNode'); - getInlineFeedbackNodeStub.mockReturnValue(document.createElement('div')); - // Mock the ResizeObserver, which is not available in the test environment - global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => { - return new MockResizeObserver(callback); - }); - }); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('without any inputs, should still render correctly without ace, showing a placeholder', () => { - fixture.detectChanges(); - const placeholder = debugElement.query(By.css('#no-file-selected')); - expect(placeholder).not.toBeNull(); - const aceEditor = debugElement.query(By.css('#ace-code-editor')); - expect(aceEditor.nativeElement.hasAttribute('hidden')).toBeTrue(); - }); - - it('if the component is loading a file from server, it should show the editor in a readonly state', () => { - comp.selectedFile = 'dummy'; - comp.isLoading = true; - fixture.detectChanges(); - const placeholder = debugElement.query(By.css('#no-file-selected')); - expect(placeholder).toBeNull(); - const aceEditor = debugElement.query(By.css('#ace-code-editor')); - expect(aceEditor.nativeElement.hasAttribute('hidden')).toBeTrue(); - expect(comp.editor.getEditor().getReadOnly()).toBeTrue(); - - comp.isLoading = false; - fixture.detectChanges(); - expect(aceEditor.nativeElement.hasAttribute('hidden')).toBeFalse(); - expect(comp.editor.getEditor().getReadOnly()).toBeFalse(); - }); - - it('if actions are disabled, it should show the editor in a readonly state', () => { - comp.selectedFile = 'dummy'; - comp.disableActions = true; - fixture.detectChanges(); - const aceEditor = debugElement.query(By.css('#ace-code-editor')); - expect(aceEditor.nativeElement.hasAttribute('hidden')).toBeFalse(); - expect(comp.editor.getEditor().getReadOnly()).toBeTrue(); - }); - - it('if a file is selected and the component is not loading a file from server, the editor should be usable', () => { - comp.selectedFile = 'dummy'; - comp.isLoading = false; - fixture.detectChanges(); - const placeholder = debugElement.query(By.css('#no-file-selected')); - expect(placeholder).toBeNull(); - const aceEditor = debugElement.query(By.css('#ace-code-editor')); - expect(aceEditor.nativeElement.hasAttribute('hidden')).toBeFalse(); - expect(comp.editor.getEditor().getReadOnly()).toBeFalse(); - }); - - it('should correctly init editor after file change', waitForAsync(async () => { - const selectedFile = 'dummy'; - const fileSession = {}; - const loadFileSubject = new Subject(); - const initEditorSpy = jest.spyOn(comp, 'initEditor'); - loadRepositoryFileStub.mockReturnValue(loadFileSubject); - fixture.detectChanges(); - comp.selectedFile = selectedFile; - comp.fileSession = fileSession; - await comp.ngOnChanges({ selectedFile: { firstChange: false, previousValue: undefined, currentValue: selectedFile, isFirstChange: () => false } }); - - expect(comp.isLoading).toBeTrue(); - expect(loadRepositoryFileStub).toHaveBeenCalledWith(selectedFile); - expect(initEditorSpy).not.toHaveBeenCalled(); - loadFileSubject.next({ fileName: selectedFile, fileContent: 'lorem ipsum' }); - await comp.ngOnChanges({ selectedFile: { firstChange: false, previousValue: selectedFile, currentValue: selectedFile, isFirstChange: () => false } }); - - expect(comp.isLoading).toBeFalse(); - expect(comp.fileSession).toEqual({ - dummy: { - code: 'lorem ipsum', - cursor: { column: 0, row: 0 }, - loadingError: false, - }, - }); - expect(initEditorSpy).toHaveBeenCalledWith(); - })); - - it.each([ - [new ConnectionError(), 'loadingFailedInternetDisconnected'], - [new Error(), 'loadingFailed'], - ])('should correctly init editor after file change in case of error', async (error: Error, errorCode: string) => { - const selectedFile = 'dummy'; - const fileSession = {}; - const loadFileSubject = new Subject(); - const initEditorSpy = jest.spyOn(comp, 'initEditor'); - const onErrorSpy = jest.spyOn(comp.onError, 'emit'); - loadRepositoryFileStub.mockReturnValue(loadFileSubject); - comp.selectedFile = selectedFile; - comp.fileSession = fileSession; - - triggerChanges(comp, { property: 'selectedFile', currentValue: selectedFile }); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(comp.isLoading).toBeTrue(); - expect(loadRepositoryFileStub).toHaveBeenCalledWith(selectedFile); - expect(initEditorSpy).not.toHaveBeenCalled(); - loadFileSubject.error(error); - fixture.detectChanges(); - - await firstValueFrom(comp.onFileLoad); - expect(onErrorSpy).toHaveBeenCalledWith(errorCode); - expect(comp.isLoading).toBeFalse(); - expect(comp.fileSession).toEqual({ dummy: { code: '', cursor: { column: 0, row: 0 }, loadingError: true } }); - expect(initEditorSpy).toHaveBeenCalledOnce(); - }); - - it('should discard all new feedback after a re-init because of a file change', async () => { - comp.newFeedbackLines = [1, 2, 3]; - await comp.initEditor(); - expect(comp.newFeedbackLines).toEqual([]); - }); - - it('should not load the file from server on selected file change if the file is already in session', () => { - const selectedFile = 'dummy'; - const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 }, loadingError: false } }; - const initEditorSpy = jest.spyOn(comp, 'initEditor'); - const loadFileSpy = jest.spyOn(comp, 'fetchFileContent'); - comp.selectedFile = selectedFile; - comp.fileSession = fileSession; - - triggerChanges(comp, { property: 'selectedFile', currentValue: selectedFile }); - fixture.detectChanges(); - - expect(initEditorSpy).toHaveBeenCalledWith(); - expect(loadFileSpy).not.toHaveBeenCalled(); - }); - - it('should load the file from server on selected file change if the file is already in session but there was a loading error', waitForAsync(async () => { - const selectedFile = 'dummy'; - const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 }, loadingError: true } }; - const initEditorSpy = jest.spyOn(comp, 'initEditor'); - const loadFileSpy = jest.spyOn(comp, 'fetchFileContent'); - const loadFileSubject = new Subject<{ fileName: 'dummy'; fileContent: 'lorem ipsum' }>(); - loadRepositoryFileStub.mockReturnValue(loadFileSubject); - comp.selectedFile = selectedFile; - comp.fileSession = fileSession; - - triggerChanges(comp, { property: 'selectedFile', currentValue: selectedFile }); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(initEditorSpy).not.toHaveBeenCalled(); - expect(loadFileSpy).toHaveBeenCalledOnce(); - })); - - it('should update file session references on file rename', async () => { - const selectedFile = 'file'; - const newFileName = 'newFilename'; - const fileChange = new RenameFileChange(FileType.FILE, selectedFile, newFileName); - const fileSession = { - [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 }, loadingError: false }, - anotherFile: { code: 'lorem ipsum 2', cursor: { column: 0, row: 0 }, loadingError: false }, - }; - comp.selectedFile = newFileName; - comp.fileSession = fileSession; - - await comp.onFileChange(fileChange); - - expect(comp.fileSession).toEqual({ - anotherFile: fileSession.anotherFile, - [fileChange.newFileName]: fileSession[selectedFile], - }); - }); - - it('should init editor on newly created file if selected', async () => { - const selectedFile = 'file'; - const fileChange = new CreateFileChange(FileType.FILE, selectedFile); - const fileSession = { anotherFile: { code: 'lorem ipsum 2', cursor: { column: 0, row: 0 }, loadingError: false } }; - const initEditorSpy = jest.spyOn(comp, 'initEditor'); - comp.selectedFile = selectedFile; - comp.fileSession = fileSession; - - await comp.onFileChange(fileChange); - - expect(initEditorSpy).toHaveBeenCalledWith(); - expect(comp.fileSession).toEqual({ - anotherFile: fileSession.anotherFile, - [fileChange.fileName]: { code: '', cursor: { row: 0, column: 0 }, loadingError: false }, - }); - }); - - it('should not do anything on file content change if the code has not changed', () => { - const textChangeEmitter = new EventEmitter(); - comp.editor.textChanged = textChangeEmitter; - const onFileContentChangeSpy = jest.spyOn(comp.onFileContentChange, 'emit'); - - const selectedFile = 'file'; - const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 }, loadingError: false } }; - comp.selectedFile = selectedFile; - comp.fileSession = fileSession; - - textChangeEmitter.emit('lorem ipsum'); - - expect(onFileContentChangeSpy).not.toHaveBeenCalled(); - }); - - it('should update build log errors and emit a message on file text change', async () => { - const onFileContentChangeSpy = jest.spyOn(comp.onFileContentChange, 'emit'); - - const selectedFile = 'file'; - const newFileContent = 'lorem ipsum new'; - const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 }, loadingError: false } }; - const annotations = [{ fileName: selectedFile, row: 5, column: 4, text: 'error', type: 'error', timestamp: 0 }]; - const editorChange = { start: { row: 1, column: 1 }, end: { row: 2, column: 1 }, action: 'remove' }; - - comp.selectedFile = selectedFile; - comp.fileSession = fileSession; - comp.annotationsArray = annotations; - - comp.updateAnnotations(editorChange); - await comp.onFileTextChanged(newFileContent); - - expect(onFileContentChangeSpy).toHaveBeenCalledWith({ file: selectedFile, fileContent: newFileContent }); - const newAnnotations = [ - { - fileName: selectedFile, - text: 'error', - type: 'error', - timestamp: 0, - row: 4, - column: 4, - }, - ]; - expect(comp.annotationsArray).toEqual(newAnnotations); - }); - - it('should be in readonly mode and display feedback when in tutor assessment', async () => { - await comp.initEditor(); - comp.isTutorAssessment = true; - const updateLineWidgetsSpy = jest.spyOn(comp, 'updateLineWidgets'); - await comp.onFileTextChanged('newFileContent'); - - expect(comp.editor.getEditor().getReadOnly()).toBeTrue(); - expect(updateLineWidgetsSpy).toHaveBeenCalledOnce(); - }); - - it('should be in readonly mode when the file could not be loaded', () => { - comp.selectedFile = 'asdf'; - comp.fileSession = { asdf: { code: '', cursor: { column: 0, row: 0 }, loadingError: true } }; - - fixture.detectChanges(); - - expect(comp.editor.getEditor().getReadOnly()).toBeTrue(); - }); - - it('should setup inline comment buttons in gutter', async () => { - await comp.initEditor(); - comp.isTutorAssessment = true; - comp.readOnlyManualFeedback = false; - const setupLineIconsSpy = jest.spyOn(comp, 'setupLineIcons'); - const observerDomSpy = jest.spyOn(comp, 'observerDom'); - await comp.onFileTextChanged('newFileContent'); - - expect(setupLineIconsSpy).toHaveBeenCalledOnce(); - expect(observerDomSpy).toHaveBeenCalledOnce(); - }); - - it('should change the displayed tab width', () => { - const editorTabSize = () => comp.editor.getEditor().getSession().getTabSize(); - - comp.updateTabSize(5); - fixture.detectChanges(); - expect(editorTabSize()).toBe(5); - - comp.tabSize = 4; - fixture.detectChanges(); - expect(editorTabSize()).toBe(4); - }); - - it('should temporarily show new feedbacks which have not been updated yet', async () => { - await comp.initEditor(); - comp.newFeedbackLines = [16]; - await comp.updateLineWidgets(); - expect(comp.editorSession.lineWidgets[16]).toBeDefined(); - }); - - it('should not show an updated feedback as new', async () => { - await comp.initEditor(); - comp.feedbacks = []; - const adjustLineWidgetHeightStub = jest.spyOn(comp, 'adjustLineWidgetHeight'); - adjustLineWidgetHeightStub.mockImplementation(() => {}); - comp.newFeedbackLines = [16, 17]; - await comp.updateLineWidgets(); - await comp.updateFeedback({ reference: 'file:src/Test.java_line:16' }); - expect(comp.newFeedbackLines).toEqual([17]); - }); - - it('should not show new feedback that is cancelled anymore', async () => { - await comp.initEditor(); - const removeLineWidget = jest.spyOn(comp.editorSession.widgetManager, 'removeLineWidget'); - comp.newFeedbackLines = [1, 16]; - await comp.updateLineWidgets(); - await comp.cancelFeedback(16); - expect(comp.newFeedbackLines).toEqual([1]); - expect(removeLineWidget).toHaveBeenCalled(); - }); - - it('should explicitly remove all ACE line widgets when being destroyed', async () => { - await comp.initEditor(); - comp.newFeedbackLines = [1, 16]; - await comp.updateLineWidgets(); - const removeLineWidget = jest.spyOn(comp.editorSession.widgetManager, 'removeLineWidget'); - comp.ngOnDestroy(); - expect(removeLineWidget).toHaveBeenCalledTimes(2); - }); - - it('should only show referenced feedback that is for the current file', () => { - comp.selectedFile = 'src/Test.java'; - const feedbacks = [{ reference: 'file:src/Test.java_line:16' }, { reference: 'file:src/Another.java_line:16' }, { reference: undefined }]; - expect(comp.filterFeedbackForFile(feedbacks)).toEqual([{ reference: 'file:src/Test.java_line:16' }]); - }); - - it('should convert an accepted feedback suggestion to a marked manual feedback', async () => { - await comp.initEditor(); - const suggestion = { - text: 'FeedbackSuggestion:', - detailText: 'test', - reference: 'file:src/Test.java_line:16', - type: FeedbackType.MANUAL, - }; - comp.feedbackSuggestions = [suggestion]; - await comp.acceptSuggestion(suggestion); - expect(comp.feedbackSuggestions).toBeEmpty(); - expect(comp.feedbacks).toEqual([ - { - text: 'FeedbackSuggestion:accepted:', - detailText: 'test', - reference: 'file:src/Test.java_line:16', - type: FeedbackType.MANUAL, - }, - ]); - }); - - it('should remove discarded suggestions', async () => { - await comp.initEditor(); - const suggestion = { - text: 'FeedbackSuggestion:', - detailText: 'test', - reference: 'file:src/Test.java_line:16', - type: FeedbackType.MANUAL, - }; - comp.feedbackSuggestions = [suggestion]; - comp.discardSuggestion(suggestion); - expect(comp.feedbackSuggestions).toBeEmpty(); - }); - - it('should update the line widget heights when feedbacks or suggestions change', async () => { - const updateLineWidgetHeightSpy = jest.spyOn(comp, 'updateLineWidgets'); - - await comp.initEditor(); - - // Change of feedbacks from the outside - await comp.ngOnChanges({ - feedbacks: { - previousValue: [], - currentValue: [new Feedback()], - firstChange: true, - isFirstChange: () => true, - }, - }); - expect(updateLineWidgetHeightSpy).toHaveBeenCalled(); - - // Change of feedback suggestions from the outside - await comp.ngOnChanges({ - feedbackSuggestions: { - previousValue: [], - currentValue: [new Feedback()], - firstChange: true, - isFirstChange: () => true, - }, - }); - expect(updateLineWidgetHeightSpy).toHaveBeenCalled(); - }); - - it('renders line widgets for feedback suggestions', async () => { - await comp.initEditor(); - const addLineWidgetWithFeedbackSpy = jest.spyOn(comp, 'addLineWidgetWithFeedback'); - - comp.feedbackSuggestions = [ - { - text: 'FeedbackSuggestion:', - detailText: 'test', - reference: 'file:src/Test.java_line:16', - type: FeedbackType.MANUAL, - }, - ]; - comp.selectedFile = 'src/Test.java'; - await comp.updateLineWidgets(); - - expect(addLineWidgetWithFeedbackSpy).toHaveBeenCalled(); - }); - - describe('feedback deletion', () => { - let f1: Feedback; - let f2: Feedback; - beforeEach(() => { - f1 = new Feedback(); - f1.text = 'File src/abc/BubbleSort.java at line 6'; - f1.detailText = 'a'; - f1.reference = 'file:src/abc/BubbleSort.java_line:5'; - - f2 = new Feedback(); - f2.text = 'File src/abc/BubbleSort.java at line 7'; - f2.detailText = 'b'; - f2.reference = 'file:src/abc/BubbleSort.java_line:6'; - }); - - it('should delete correct inline feedback before saving for the first time', async () => { - await comp.initEditor(); - await comp.updateFeedback(f1); - await comp.updateFeedback(f2); - - comp.deleteFeedback(f1); - - expect(comp.feedbacks).not.toContain(f1); - expect(comp.feedbacks).toContain(f2); - }); - - it('should delete correct inline feedback after saving', async () => { - await comp.initEditor(); - await comp.updateFeedback(f1); - await comp.updateFeedback(f2); - - // saving is simulated here by giving the feedbacks an id, which is the only remaining untested factor for - // feedback deletion - f1.id = 1; - f2.id = 2; - - comp.deleteFeedback(f1); - - expect(comp.feedbacks).not.toContain(f1); - expect(comp.feedbacks).toContain(f2); - }); - }); -}); diff --git a/src/test/javascript/spec/component/code-editor/code-editor-build-output.component.spec.ts b/src/test/javascript/spec/component/code-editor/code-editor-build-output.component.spec.ts index 05ede2cf9af4..3092d1c466ac 100644 --- a/src/test/javascript/spec/component/code-editor/code-editor-build-output.component.spec.ts +++ b/src/test/javascript/spec/component/code-editor/code-editor-build-output.component.spec.ts @@ -18,11 +18,11 @@ import { ProgrammingSubmission } from 'app/entities/programming-submission.model import { Result } from 'app/entities/result.model'; import { StaticCodeAnalysisIssue } from 'app/entities/static-code-analysis-issue.model'; import { Feedback, FeedbackType, STATIC_CODE_ANALYSIS_FEEDBACK_IDENTIFIER } from 'app/entities/feedback.model'; -import { Annotation } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; import { ProgrammingLanguage, ProjectType } from 'app/entities/programming-exercise.model'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { MockPipe, MockProvider } from 'ng-mocks'; import { CodeEditorSubmissionService } from 'app/exercises/programming/shared/code-editor/service/code-editor-submission.service'; +import { Annotation } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; describe('CodeEditorBuildOutputComponent', () => { let comp: CodeEditorBuildOutputComponent; diff --git a/src/test/javascript/spec/component/code-editor/code-editor-monaco.component.spec.ts b/src/test/javascript/spec/component/code-editor/code-editor-monaco.component.spec.ts index 1c73ef866877..ef9304c1a774 100644 --- a/src/test/javascript/spec/component/code-editor/code-editor-monaco.component.spec.ts +++ b/src/test/javascript/spec/component/code-editor/code-editor-monaco.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { ArtemisTestModule } from '../../test.module'; -import { CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; +import { Annotation, CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; import { MockComponent } from 'ng-mocks'; import { CodeEditorTutorAssessmentInlineFeedbackComponent } from 'app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component'; import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; @@ -12,7 +12,6 @@ import { CodeEditorRepositoryFileService, ConnectionError } from 'app/exercises/ import { MockCodeEditorRepositoryFileService } from '../../helpers/mocks/service/mock-code-editor-repository-file.service'; import { MockLocalStorageService } from '../../helpers/mocks/service/mock-local-storage.service'; import { LocalStorageService } from 'ngx-webstorage'; -import { Annotation } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; import { SimpleChange } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { CodeEditorHeaderComponent } from 'app/exercises/programming/shared/code-editor/header/code-editor-header.component'; diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts index 98b0afa40ea4..4a0123edb07a 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts @@ -5,10 +5,10 @@ import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.co import { MockResizeObserver } from '../../../helpers/mocks/service/mock-resize-observer'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; import { BehaviorSubject } from 'rxjs'; -import { Annotation } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; import { MonacoEditorBuildAnnotationType } from 'app/shared/monaco-editor/model/monaco-editor-build-annotation.model'; import { MonacoCodeEditorElement } from 'app/shared/monaco-editor/model/monaco-code-editor-element.model'; import { MonacoEditorGlyphMarginHoverButton } from 'app/shared/monaco-editor/model/monaco-editor-glyph-margin-hover-button.model'; +import { Annotation } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; describe('MonacoEditorComponent', () => { let fixture: ComponentFixture; diff --git a/src/test/javascript/spec/helpers/sample/build-logs.ts b/src/test/javascript/spec/helpers/sample/build-logs.ts index 4a8d862f6276..5472884b6651 100644 --- a/src/test/javascript/spec/helpers/sample/build-logs.ts +++ b/src/test/javascript/spec/helpers/sample/build-logs.ts @@ -1,4 +1,4 @@ -import { Annotation } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; +import { Annotation } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; export const buildLogs = [ { diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts index ea9003cf8840..28107b0507b3 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts @@ -6,7 +6,6 @@ import { ActivatedRoute } from '@angular/router'; import { NgModel } from '@angular/forms'; import { NgbDropdown, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { BehaviorSubject, Subject, of } from 'rxjs'; -import * as ace from 'brace'; import { ArtemisTestModule } from '../../test.module'; import { ParticipationWebsocketService } from 'app/overview/participation-websocket.service'; import { ProgrammingExerciseParticipationService } from 'app/exercises/programming/manage/services/programming-exercise-participation.service'; @@ -57,7 +56,6 @@ import { CodeEditorGridComponent } from 'app/exercises/programming/shared/code-e import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { CodeEditorActionsComponent } from 'app/exercises/programming/shared/code-editor/actions/code-editor-actions.component'; import { CodeEditorFileBrowserComponent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component'; -import { CodeEditorAceComponent } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; import { CodeEditorInstructionsComponent } from 'app/exercises/programming/shared/code-editor/instructions/code-editor-instructions.component'; import { CodeEditorBuildOutputComponent } from 'app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component'; import { KeysPipe } from 'app/shared/pipes/keys.pipe'; @@ -69,17 +67,16 @@ import { CodeEditorStatusComponent } from 'app/exercises/programming/shared/code import { CodeEditorFileBrowserFolderComponent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser-folder.component'; import { CodeEditorFileBrowserFileComponent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser-file.component'; import { CodeEditorTutorAssessmentInlineFeedbackComponent } from 'app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component'; -import { AceEditorModule } from 'app/shared/markdown-editor/ace-editor/ace-editor.module'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { TreeviewComponent } from 'app/exercises/programming/shared/code-editor/treeview/components/treeview/treeview.component'; import { TreeviewItemComponent } from 'app/exercises/programming/shared/code-editor/treeview/components/treeview-item/treeview-item.component'; import { CodeEditorHeaderComponent } from 'app/exercises/programming/shared/code-editor/header/code-editor-header.component'; import { AlertService } from 'app/core/util/alert.service'; import { MockResizeObserver } from '../../helpers/mocks/service/mock-resize-observer'; +import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; +import { CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; describe('CodeEditorContainerIntegration', () => { - // needed to make sure ace is defined - ace.acequire('ace/ext/modelist'); let container: CodeEditorContainerComponent; let containerFixture: ComponentFixture; let containerDebugElement: DebugElement; @@ -103,7 +100,7 @@ describe('CodeEditorContainerIntegration', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ArtemisTestModule, AceEditorModule, MockDirective(NgbDropdown), MockModule(NgbTooltipModule)], + imports: [ArtemisTestModule, MonacoEditorModule, MockDirective(NgbDropdown), MockModule(NgbTooltipModule)], declarations: [ CodeEditorContainerComponent, MockComponent(CodeEditorGridComponent), @@ -117,7 +114,7 @@ describe('CodeEditorContainerIntegration', () => { CodeEditorActionsComponent, CodeEditorFileBrowserComponent, CodeEditorBuildOutputComponent, - CodeEditorAceComponent, + CodeEditorMonacoComponent, MockComponent(CodeEditorFileBrowserCreateNodeComponent), MockComponent(CodeEditorFileBrowserFolderComponent), MockComponent(CodeEditorFileBrowserFileComponent), @@ -233,9 +230,9 @@ describe('CodeEditorContainerIntegration', () => { expect(container.fileBrowser.errorFiles).toEqual(extractedErrorFiles); expect(container.fileBrowser.unsavedFiles).toHaveLength(0); - // ace editor - expect(container.aceEditor?.isLoading).toBeFalse(); - expect(container.aceEditor?.commitState).toBe(CommitState.CLEAN); + // monaco editor + expect(container.monacoEditor.isLoading).toBeFalse(); + expect(container.monacoEditor.commitState).toBe(CommitState.CLEAN); // actions expect(container.actions.commitState).toBe(CommitState.CLEAN); @@ -262,7 +259,7 @@ describe('CodeEditorContainerIntegration', () => { const loadFile = async (fileName: string, fileContent: string) => { getFileStub.mockReturnValue(of({ fileContent })); container.fileBrowser.selectedFile = fileName; - await container.aceEditor?.ensureLoadedThenInitEditorIfSelected(fileName); + await container.monacoEditor.selectFileInEditor(fileName); }; it('should initialize all components correctly if all server calls are successful', fakeAsync(() => { @@ -308,10 +305,10 @@ describe('CodeEditorContainerIntegration', () => { expect(container.fileBrowser.errorFiles).toEqual(extractedErrorFiles); expect(container.fileBrowser.unsavedFiles).toHaveLength(0); - // ace editor - expect(container.aceEditor?.isLoading).toBeFalse(); - expect(container.aceEditor?.annotationsArray?.map((a) => omit(a, 'hash'))).toEqual(extractedBuildLogErrors); - expect(container.aceEditor?.commitState).toBe(CommitState.COULD_NOT_BE_RETRIEVED); + // monaco editor + expect(container.monacoEditor.isLoading).toBeFalse(); + expect(container.monacoEditor.annotationsArray?.map((a) => omit(a, 'hash'))).toEqual(extractedBuildLogErrors); + expect(container.monacoEditor.commitState).toBe(CommitState.COULD_NOT_BE_RETRIEVED); // actions expect(container.actions.commitState).toBe(CommitState.COULD_NOT_BE_RETRIEVED); @@ -339,7 +336,7 @@ describe('CodeEditorContainerIntegration', () => { expect(subscribeForLatestResultOfParticipationStub).toHaveBeenCalledOnce(); })); - it('should update the file browser and ace editor on file selection', async () => { + it('should update the file browser and monaco editor on file selection', async () => { cleanInitialize(); const selectedFile = Object.keys(container.fileBrowser.repositoryFiles)[0]; const fileContent = 'lorem ipsum'; @@ -347,14 +344,14 @@ describe('CodeEditorContainerIntegration', () => { containerFixture.detectChanges(); expect(container.selectedFile).toBe(selectedFile); - expect(container.aceEditor?.selectedFile).toBe(selectedFile); - expect(container.aceEditor?.isLoading).toBeFalse(); - expect(container.aceEditor?.fileSession).toContainKey(selectedFile); + expect(container.monacoEditor.selectedFile).toBe(selectedFile); + expect(container.monacoEditor.isLoading).toBeFalse(); + expect(container.monacoEditor.fileSession).toContainKey(selectedFile); expect(getFileStub).toHaveBeenCalledOnce(); expect(getFileStub).toHaveBeenCalledWith(selectedFile); containerFixture.detectChanges(); - expect(container.aceEditor?.editor?.getEditor()?.getSession()?.getValue()).toBe(fileContent); + expect(container.getText()).toBe(fileContent); }); it('should mark file to have unsaved changes in file tree if the file was changed in editor', async () => { @@ -365,7 +362,7 @@ describe('CodeEditorContainerIntegration', () => { await loadFile(selectedFile, fileContent); containerFixture.detectChanges(); - container.aceEditor?.onFileTextChanged(newFileContent); + container.monacoEditor.onFileTextChanged(newFileContent); containerFixture.detectChanges(); expect(getFileStub).toHaveBeenCalledOnce(); diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts index c938fcca8f47..1304b9a13b31 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts @@ -6,7 +6,6 @@ import { AccountService } from 'app/core/auth/account.service'; import { ChangeDetectorRef, DebugElement } from '@angular/core'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { BehaviorSubject, Subject, of, throwError } from 'rxjs'; -import * as ace from 'brace'; import { ArtemisTestModule } from '../../test.module'; import { ProgrammingExerciseParticipationService } from 'app/exercises/programming/manage/services/programming-exercise-participation.service'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; @@ -53,7 +52,6 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { CodeEditorGridComponent } from 'app/exercises/programming/shared/code-editor/layout/code-editor-grid.component'; import { CodeEditorActionsComponent } from 'app/exercises/programming/shared/code-editor/actions/code-editor-actions.component'; import { CodeEditorFileBrowserComponent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component'; -import { CodeEditorAceComponent } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; import { CodeEditorBuildOutputComponent } from 'app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component'; import { KeysPipe } from 'app/shared/pipes/keys.pipe'; import { CodeEditorInstructionsComponent } from 'app/exercises/programming/shared/code-editor/instructions/code-editor-instructions.component'; @@ -69,8 +67,6 @@ import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; describe('CodeEditorInstructorIntegration', () => { - // needed to make sure ace is defined - ace.acequire('ace/ext/modelist'); let container: CodeEditorInstructorAndEditorContainerComponent; let containerFixture: ComponentFixture; let containerDebugElement: DebugElement; @@ -102,7 +98,6 @@ describe('CodeEditorInstructorIntegration', () => { MockComponent(CodeEditorGridComponent), MockComponent(CodeEditorActionsComponent), MockComponent(CodeEditorFileBrowserComponent), - MockComponent(CodeEditorAceComponent), MockComponent(CodeEditorMonacoComponent), CodeEditorBuildOutputComponent, MockPipe(ArtemisDatePipe), diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-student.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-student.integration.spec.ts index c8da711a6482..11036e23a71d 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-student.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-student.integration.spec.ts @@ -7,7 +7,6 @@ import { AccountService } from 'app/core/auth/account.service'; import { DebugElement } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { BehaviorSubject, Subject } from 'rxjs'; -import * as ace from 'brace'; import { ArtemisTestModule } from '../../test.module'; import { ParticipationWebsocketService } from 'app/overview/participation-websocket.service'; import { ProgrammingExerciseParticipationService } from 'app/exercises/programming/manage/services/programming-exercise-participation.service'; @@ -52,7 +51,6 @@ import { KeysPipe } from 'app/shared/pipes/keys.pipe'; import { CodeEditorActionsComponent } from 'app/exercises/programming/shared/code-editor/actions/code-editor-actions.component'; import { CodeEditorFileBrowserComponent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component'; import { CodeEditorBuildOutputComponent } from 'app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component'; -import { CodeEditorAceComponent } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; import { CodeEditorFileBrowserCreateNodeComponent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser-create-node.component'; import { CodeEditorFileBrowserFolderComponent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser-folder.component'; import { CodeEditorFileBrowserFileComponent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser-file.component'; @@ -62,8 +60,6 @@ import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; describe('CodeEditorStudentIntegration', () => { - // needed to make sure ace is defined - ace.acequire('ace/ext/modelist'); let container: CodeEditorStudentContainerComponent; let containerFixture: ComponentFixture; let containerDebugElement: DebugElement; @@ -102,7 +98,6 @@ describe('CodeEditorStudentIntegration', () => { MockComponent(CodeEditorGridComponent), MockComponent(CodeEditorActionsComponent), MockComponent(CodeEditorBuildOutputComponent), - MockComponent(CodeEditorAceComponent), MockComponent(CodeEditorMonacoComponent), MockComponent(CodeEditorFileBrowserCreateNodeComponent), MockComponent(CodeEditorFileBrowserFolderComponent), diff --git a/src/test/javascript/spec/service/orion/orion-connector.service.spec.ts b/src/test/javascript/spec/service/orion/orion-connector.service.spec.ts index fa7e5a2f0a9c..6fb2c239ff92 100644 --- a/src/test/javascript/spec/service/orion/orion-connector.service.spec.ts +++ b/src/test/javascript/spec/service/orion/orion-connector.service.spec.ts @@ -8,10 +8,10 @@ import { Injector } from '@angular/core'; import { MockRouter } from '../../helpers/mocks/mock-router'; import { Router } from '@angular/router'; import { ExerciseView, OrionBuildConnector, OrionExerciseConnector, OrionSharedUtilConnector, OrionState, OrionVCSConnector } from 'app/shared/orion/orion'; -import { Annotation } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; import { AlertService } from 'app/core/util/alert.service'; import { REPOSITORY } from 'app/exercises/programming/manage/code-editor/code-editor-instructor-base-container.component'; import { Feedback } from 'app/entities/feedback.model'; +import { Annotation } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; describe('OrionConnectorService', () => { let serviceUnderTest: OrionConnectorService;