diff --git a/CHANGELOG.md b/CHANGELOG.md index e80d75da..310027ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## `3.0.1` - Bugfix: Added a few rules for JCL syntax highlighter - Bugfix: Set USS path to correct directory, when opening the directory or file in new browser tab respectively +- Added the feature to copy the line content and copy URL link to open a file at a specific line ## `3.0.0` diff --git a/webClient/src/app/app.component.ts b/webClient/src/app/app.component.ts index b119a688..7cd93359 100644 --- a/webClient/src/app/app.component.ts +++ b/webClient/src/app/app.component.ts @@ -54,6 +54,14 @@ export class AppComponent { } handleLaunchOrMessageObject(data: any) { + /** + * selectedLines=[2] -> selected line is number 2 (in URL the info will look like "lines":"2") + * selectedLines=[2,7] -> selected lines are number 2-7 (TODO: Need to add the option to copy multiple lines and pass the info in the URL like "lines":"2-7") + */ + let selectedLines = []; + if(data.lines){ + selectedLines = data.lines.split("-") + } switch (data.type) { case 'test-language': this.log.info(`Setting language test mode`); @@ -87,7 +95,7 @@ export class AppComponent { let fileName = data.name.substring(lastSlash+1); for (let i = 0; i < nodes.length; i++) { if (nodes[i].fileName == fileName) { - this.editorControl.openFile('', nodes[i]).subscribe(x => { + this.editorControl.openFile('', nodes[i], selectedLines).subscribe(x => { this.log.debug(`file loaded through app2app.`); }); this.editorControl.loadDirectory(nodes[i].path ? nodes[i].path : '/'); @@ -99,13 +107,13 @@ export class AppComponent { }); } else { this.log.info(`Opening dataset=${data.name}`); - this.editorControl.openDataset.next(data.name); + this.editorControl.openDataset.next({datasetName: data.name, selectedLines: selectedLines}); } break; case 'openDataset': if (data.name) { this.log.info(`Opening dataset=${data.name}`); - this.editorControl.openDataset.next(data.name); + this.editorControl.openDataset.next({datasetName: data.name, selectedLines: selectedLines}); } else { this.log.warn(`Dataset name missing. Skipping operation`); } diff --git a/webClient/src/app/editor/code-editor/monaco/monaco.component.scss b/webClient/src/app/editor/code-editor/monaco/monaco.component.scss index ead55a68..dc7a0479 100644 --- a/webClient/src/app/editor/code-editor/monaco/monaco.component.scss +++ b/webClient/src/app/editor/code-editor/monaco/monaco.component.scss @@ -27,6 +27,9 @@ background-color: yellow; color: black; } + ::ng-deep .myLineDecoration { + background: #e6e1ad; width: 50px !important; + } } .loading-indicator { position: absolute; diff --git a/webClient/src/app/editor/code-editor/monaco/monaco.component.ts b/webClient/src/app/editor/code-editor/monaco/monaco.component.ts index 9c410dac..d19a474a 100644 --- a/webClient/src/app/editor/code-editor/monaco/monaco.component.ts +++ b/webClient/src/app/editor/code-editor/monaco/monaco.component.ts @@ -23,6 +23,8 @@ import * as monaco from 'monaco-editor'; import { Subscription } from 'rxjs'; import { EditorKeybindingService } from '../../../shared/editor-keybinding.service'; import { KeyCode } from '../../../shared/keycode-enum'; +import { SnackBarService } from '../../../shared/snack-bar.service'; +import { MessageDuration } from "../../../shared/message-duration"; const ReconnectingWebSocket = require('reconnecting-websocket'); @Component({ @@ -65,7 +67,9 @@ export class MonacoComponent implements OnInit, OnChanges { private editorControl: EditorControlService, private languageService: LanguageServerService, private appKeyboard: EditorKeybindingService, + public snackBar: SnackBarService, @Inject(Angular2InjectionTokens.LOGGER) private log: ZLUX.ComponentLogger, + @Inject(Angular2InjectionTokens.PLUGIN_DEFINITION) private pluginDefinition: ZLUX.ContainerPluginDefinition, @Inject(Angular2InjectionTokens.VIEWPORT_EVENTS) private viewportEvents: Angular2PluginViewportEvents) { this.keyBindingSub.add(this.appKeyboard.keydownEvent.subscribe((event) => { if (event.which === KeyCode.KEY_V) { @@ -116,6 +120,21 @@ export class MonacoComponent implements OnInit, OnChanges { this.editorControl.refreshLayout.subscribe(() =>{ setTimeout(() => this.editor.layout(), 1); }); + + this.editor.onContextMenu((e: any) => { + if(e.target.type === 3){ //if right click is on top of the line numbers + this.viewportEvents.spawnContextMenu(e.event.browserEvent.clientX, e.event.browserEvent.clientY, [ + { + text: 'Copy permalink', + action: () => this.copyPermalink(e) + }, + { + text: 'Copy line', + action: () => this.copyLine(e) + } + ], true) + } + }); } focus(e: any) { @@ -125,7 +144,6 @@ export class MonacoComponent implements OnInit, OnChanges { this.editor.layout(); } - ngOnChanges(changes: SimpleChanges) { for (const input in changes) { if (input === 'editorFile' && changes[input].currentValue != null) { @@ -143,6 +161,7 @@ export class MonacoComponent implements OnInit, OnChanges { } } + onMonacoInit(editor) { this.editorControl.editor.next(editor); this.keyBinds(editor); @@ -231,6 +250,37 @@ export class MonacoComponent implements OnInit, OnChanges { }); } + copyPermalink(event: any){ + const lines = event.target.position.lineNumber; + const activeFile = this.editorControl.fetchActiveFile(); + let filePath = ''; + let link = ''; + if(activeFile.model.isDataset){ + filePath = activeFile.model.path; + link = `${window.location.origin}${window.location.pathname}?pluginId=${this.pluginDefinition.getBasePlugin().getIdentifier()}:data:{"type":"openDataset","name":"${encodeURIComponent(filePath)}","lines":"${lines}","toggleTree":true}`; + } else { + filePath = activeFile.model.path + "/" + activeFile.model.name; + link = `${window.location.origin}${window.location.pathname}?pluginId=${this.pluginDefinition.getBasePlugin().getIdentifier()}:data:{"type":"openFile","name":"${encodeURIComponent(filePath)}","lines":"${lines}","toggleTree":true}`; + } + navigator.clipboard.writeText(link).then(() => { + this.log.debug("Permalink copied to clipboard"); + }).catch((error) => { + console.error("Failed to copy permalink Error: " + error); + this.snackBar.open("Failed to copy permalink. Error: " + error, 'Dismiss', { duration: MessageDuration.Short, panelClass: 'center' }); + }); + } + + copyLine(event: any){ + const lines = event.target.position.lineNumber; + const lineContent = this.editor.getModel().getLineContent(lines); + navigator.clipboard.writeText(lineContent).then(() => { + this.log.debug("Line copied to clipboard"); + }).catch((error) => { + console.error("Failed to copy line. Error: " + error); + this.snackBar.open("Failed to copy line. Error: " + error, 'Dismiss', { duration: MessageDuration.Short, panelClass: 'center' }); + }); + } + saveFile() { let fileContext = this.editorControl.fetchActiveFile(); let directory = fileContext.model.path || this.editorControl.activeDirectory; diff --git a/webClient/src/app/editor/code-editor/monaco/monaco.service.ts b/webClient/src/app/editor/code-editor/monaco/monaco.service.ts index 92fdebaa..6ccaf1a0 100644 --- a/webClient/src/app/editor/code-editor/monaco/monaco.service.ts +++ b/webClient/src/app/editor/code-editor/monaco/monaco.service.ts @@ -561,10 +561,16 @@ export class MonacoService implements OnDestroy { this.editorControl.removeActiveFromAllFiles(); fileNode.changed = true; fileNode.active = true; + this.cleanDecoration(); } cleanDecoration() { - this.editorControl.editor.getValue().deltaDecorations(this.decorations, []); + let editorValue = this.editorControl.editor.getValue(); + let decorationIds=[]; + editorValue.getModel().getAllDecorations().forEach((decoration) => { + decorationIds.push(decoration.id); + }); + editorValue.deltaDecorations(decorationIds, []); } } diff --git a/webClient/src/app/editor/project-tree/project-tree.component.ts b/webClient/src/app/editor/project-tree/project-tree.component.ts index f3a48412..c8fd871c 100644 --- a/webClient/src/app/editor/project-tree/project-tree.component.ts +++ b/webClient/src/app/editor/project-tree/project-tree.component.ts @@ -156,7 +156,9 @@ export class ProjectTreeComponent { } }); - this.editorControl.openDataset.subscribe(dirName => { + this.editorControl.openDataset.subscribe(datasetInfo => { + let dirName= datasetInfo.datasetName; + const selectedLines = datasetInfo.selectedLines; if (dirName != null && dirName !== '') { if (dirName[0] != '/') { dirName = dirName.toUpperCase(); @@ -181,9 +183,9 @@ export class ProjectTreeComponent { this.nodes = isMember ? this.dataAdapter.convertDatasetMemberList(response) : this.dataAdapter.convertDatasetList(response); this.editorControl.setProjectNode(this.nodes); if(isMember){ - this.editorControl.openFile('',this.nodes.find(item => item.name === dsMemberName)).subscribe(x=> {this.log.debug('Dataset Member opened')}); + this.editorControl.openFile('',this.nodes.find(item => item.name === dsMemberName), datasetInfo.selectedLines).subscribe(x=> {this.log.debug('Dataset Member opened')}); } else{ - this.editorControl.openFile('',this.nodes[0]).subscribe(x=> {this.log.debug('Dataset opened')}); + this.editorControl.openFile('',this.nodes[0], datasetInfo.selectedLines).subscribe(x=> {this.log.debug('Dataset opened')}); } }, e => { // TODO diff --git a/webClient/src/app/shared/editor-control/editor-control.service.ts b/webClient/src/app/shared/editor-control/editor-control.service.ts index fd8e9945..fed5921c 100644 --- a/webClient/src/app/shared/editor-control/editor-control.service.ts +++ b/webClient/src/app/shared/editor-control/editor-control.service.ts @@ -53,7 +53,7 @@ export class EditorControlService implements ZLUX.IEditor, ZLUX.IEditorMultiBuff public createDirectory: EventEmitter = new EventEmitter(); public openProject: EventEmitter = new EventEmitter(); public openDirectory: EventEmitter = new EventEmitter(); - public openDataset: EventEmitter = new EventEmitter(); + public openDataset: EventEmitter = new EventEmitter(); public toggleFileTreeSearch: EventEmitter = new EventEmitter(); public closeAllFiles: EventEmitter = new EventEmitter(); public undoCloseAllFiles: EventEmitter = new EventEmitter(); @@ -1042,9 +1042,10 @@ export class EditorControlService implements ZLUX.IEditor, ZLUX.IEditorMultiBuff * * @param file The path of the file that should be opened * @param targetBuffer The buffer into which the file should be opened, or null to open a new buffer + * @param selectedLines Array that stores the first and last selected lines * @returns An observable that pushes a handle to the buffer into which the file was opened */ - openFile(file: string, targetBuffer: ZLUX.EditorBufferHandle | null): Observable { + openFile(file: string, targetBuffer: ZLUX.EditorBufferHandle | null, selectedLines?: any): Observable { // targetBuffer is a context of project in GCE. let resultOpenObs: Observable; let fileOpenSub: Subscription; @@ -1054,11 +1055,36 @@ export class EditorControlService implements ZLUX.IEditor, ZLUX.IEditorMultiBuff resultOpenObs = new Observable((observer) => { resultObserver = observer; }); - fileOpenSub = this.fileOpened.subscribe((e: ZLUX.EditorFileOpenedEvent) => { let model = e.buffer.model; lastFile = `${model.fileName}:${model.path}`; - + //If we are opening a file with selected line or lines via URL link to file + if(selectedLines && selectedLines.length > 0){ + let firstLine = 1; + let lastLine = 1; + if(selectedLines.length == 1){ + firstLine = Number(selectedLines[0]); + lastLine = firstLine; + } else if(selectedLines.length == 2){ + firstLine = Number(selectedLines[0]); + lastLine = Number(selectedLines[1]); + } + let editor = this.editor.getValue(); + this.editor.subscribe((value)=> { + value.revealRangeAtTop(new monaco.Range(firstLine, 1, lastLine, 1)); + value.deltaDecorations( + [], + [ + { + range: new monaco.Range(firstLine, 1, lastLine, 1000000), + options: { + marginClassName: 'myLineDecoration' + } + } + ] + ); + }) + } // if have subscriber if (resultObserver) { if (e.buffer != null && e.buffer.id === targetBuffer.id) {