mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
* Changes to allow Program (vs Unit) in Object Pascal * add possibility of dpr * more flexibility with pascal filenames * lintfixes * i have no idea what im doing * apply changes to pascal-win * pascal fixes * pascal projectfile changes * work in progress * bugfixes * bla * bugfixes * mostly working Co-authored-by: paul mcgee <paul.mcgee.8@bigpond.com> Co-authored-by: Paul McGee <paulmcgee1969@gmail.com>
514 lines
16 KiB
TypeScript
514 lines
16 KiB
TypeScript
// Copyright (c) 2021, Compiler Explorer Authors
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import _ from 'underscore';
|
|
import path from 'path';
|
|
var options = require('./options');
|
|
var languages = options.languages;
|
|
var JSZip = require('jszip');
|
|
|
|
export class MultifileFile {
|
|
fileId: number;
|
|
isIncluded: boolean;
|
|
isOpen: boolean;
|
|
isMainSource: boolean;
|
|
filename: string;
|
|
content: string;
|
|
editorId: number;
|
|
langId: string;
|
|
}
|
|
|
|
export class FiledataPair {
|
|
filename: string;
|
|
contents: string;
|
|
}
|
|
|
|
export class MultifileServiceState {
|
|
isCMakeProject: boolean;
|
|
compilerLanguageId: string;
|
|
files: MultifileFile[];
|
|
newFileId: number;
|
|
}
|
|
|
|
export class MultifileService {
|
|
private files: Array<MultifileFile>;
|
|
private compilerLanguageId: string;
|
|
private isCMakeProject: boolean;
|
|
private hub: any;
|
|
private newFileId: number;
|
|
private alertSystem: any;
|
|
private validExtraFilenameExtensions: string[];
|
|
private readonly defaultLangIdUnknownExt: string;
|
|
private readonly cmakeLangId: string;
|
|
private readonly cmakeMainSourceFilename: string;
|
|
private readonly maxFilesize: number;
|
|
|
|
constructor(hub, alertSystem, state: MultifileServiceState) {
|
|
this.hub = hub;
|
|
this.alertSystem = alertSystem;
|
|
|
|
this.isCMakeProject = state.isCMakeProject || false;
|
|
this.compilerLanguageId = state.compilerLanguageId || '';
|
|
this.files = state.files || [];
|
|
this.newFileId = state.newFileId || 1;
|
|
|
|
this.validExtraFilenameExtensions = ['.txt', '.md', '.rst', '.sh', '.cmake', '.in'];
|
|
this.defaultLangIdUnknownExt = 'c++';
|
|
this.cmakeLangId = 'cmake';
|
|
this.cmakeMainSourceFilename = 'CMakeLists.txt';
|
|
this.maxFilesize = 1024000;
|
|
}
|
|
|
|
private static isHiddenFile(filename: string): boolean {
|
|
return (filename.length > 0 && filename[0] === '.');
|
|
}
|
|
|
|
private isValidFilename(filename: string): boolean {
|
|
if (MultifileService.isHiddenFile(filename)) return false;
|
|
|
|
const filenameExt = path.extname(filename);
|
|
if (this.validExtraFilenameExtensions.includes(filenameExt)) {
|
|
return true;
|
|
}
|
|
|
|
return _.any(languages, (lang) => {
|
|
return lang.extensions.includes(filenameExt);
|
|
});
|
|
}
|
|
|
|
private isCMakeFile(filename: string): boolean {
|
|
const filenameExt = path.extname(filename);
|
|
if (filenameExt === '.cmake' || filenameExt === '.in') {
|
|
return true;
|
|
}
|
|
|
|
return path.basename(filename) === this.cmakeMainSourceFilename;
|
|
}
|
|
|
|
private getLanguageIdFromFilename(filename: string): string {
|
|
const filenameExt = path.extname(filename);
|
|
|
|
const possibleLang = _.filter(languages, (lang) => {
|
|
return lang.extensions.includes(filenameExt);
|
|
});
|
|
|
|
if (possibleLang.length > 0) {
|
|
return possibleLang[0].id;
|
|
}
|
|
|
|
if (this.isCMakeFile(filename)) {
|
|
return this.cmakeLangId;
|
|
}
|
|
|
|
return this.defaultLangIdUnknownExt;
|
|
}
|
|
|
|
public async loadProjectFromFile(f, callback) {
|
|
this.files = [];
|
|
this.newFileId = 1;
|
|
|
|
const zipFilename = path.basename(f.name, '.zip');
|
|
const mainSourcefilename = this.getDefaultMainCMakeFilename();
|
|
|
|
const zip = await JSZip.loadAsync(f);
|
|
|
|
zip.forEach(async (relativePath, zipEntry) => {
|
|
if (!zipEntry.dir) {
|
|
let removeFromName = 0;
|
|
if (relativePath.indexOf(zipFilename + '/') === 0) {
|
|
removeFromName = zipFilename.length + 1;
|
|
}
|
|
|
|
const properName = relativePath.substring(removeFromName);
|
|
if (!this.isValidFilename(properName)) {
|
|
return;
|
|
}
|
|
|
|
let content = await zip.file(zipEntry.name).async("string");
|
|
if (content.length > this.maxFilesize) {
|
|
return;
|
|
}
|
|
|
|
// remove utf8-bom characters
|
|
content = content.replace(/^(\ufeff)/, '');
|
|
|
|
const file: MultifileFile = {
|
|
fileId: this.newFileId,
|
|
filename: properName,
|
|
isIncluded: true,
|
|
isOpen: false,
|
|
editorId: -1,
|
|
isMainSource: properName === mainSourcefilename,
|
|
content: content,
|
|
langId: this.getLanguageIdFromFilename(properName),
|
|
};
|
|
|
|
this.addFile(file);
|
|
callback(file);
|
|
}
|
|
});
|
|
}
|
|
|
|
public async saveProjectToZipfile(callback: (any) => void) {
|
|
const zip = new JSZip();
|
|
|
|
this.forEachFile((file: MultifileFile) => {
|
|
if (file.isIncluded) {
|
|
zip.file(file.filename, this.getFileContents(file));
|
|
}
|
|
})
|
|
|
|
zip.generateAsync({type:"blob"}).then((blob) => {
|
|
callback(blob);
|
|
}, (err) => {
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
public getState(): MultifileServiceState {
|
|
return {
|
|
isCMakeProject: this.isCMakeProject,
|
|
compilerLanguageId: this.compilerLanguageId,
|
|
files: this.files,
|
|
newFileId: this.newFileId,
|
|
};
|
|
}
|
|
|
|
public getLanguageId() {
|
|
return this.compilerLanguageId;
|
|
}
|
|
|
|
public isCompatibleWithCMake(): boolean {
|
|
return this.compilerLanguageId === 'c++' || this.compilerLanguageId === 'c' ||
|
|
this.compilerLanguageId === 'fortran';
|
|
}
|
|
|
|
public setLanguageId(id: string) {
|
|
this.compilerLanguageId = id;
|
|
}
|
|
|
|
public isACMakeProject(): boolean {
|
|
return this.isCompatibleWithCMake() && this.isCMakeProject;
|
|
}
|
|
|
|
public setAsCMakeProject(yes: boolean) {
|
|
this.isCMakeProject = yes;
|
|
}
|
|
|
|
private checkFileEditor(file: MultifileFile) {
|
|
if (file && file.editorId > 0) {
|
|
const editor = this.hub.getEditorById(file.editorId);
|
|
if (!editor) {
|
|
file.isOpen = false;
|
|
file.editorId = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
public getFileContents(file: MultifileFile) {
|
|
this.checkFileEditor(file);
|
|
|
|
if (file.isOpen) {
|
|
const editor = this.hub.getEditorById(file.editorId);
|
|
return editor.getSource();
|
|
} else {
|
|
return file.content;
|
|
}
|
|
}
|
|
|
|
public isEditorPartOfProject(editorId: Number) {
|
|
var found = _.find(this.files, (file: MultifileFile) => {
|
|
return (file.isIncluded) && file.isOpen && (editorId === file.editorId);
|
|
});
|
|
|
|
return !!found;
|
|
}
|
|
|
|
public getFileByFileId(fileId: Number) {
|
|
const file = _.find(this.files, (file: MultifileFile) => {
|
|
return file.fileId === fileId;
|
|
});
|
|
|
|
this.checkFileEditor(file);
|
|
|
|
return file;
|
|
}
|
|
|
|
public setAsMainSource(mainFileId: Number) {
|
|
for (let file of this.files) {
|
|
file.isMainSource = false;
|
|
}
|
|
|
|
var mainfile = this.getFileByFileId(mainFileId);
|
|
mainfile.isMainSource = true;
|
|
}
|
|
|
|
private static isValidFile(file: MultifileFile): boolean {
|
|
return (file.editorId > 0) || !!file.filename;
|
|
}
|
|
|
|
private filterOutNonsense() {
|
|
this.files = _.filter(this.files, (file: MultifileFile) => MultifileService.isValidFile(file));
|
|
}
|
|
|
|
public getFiles(): Array<FiledataPair> {
|
|
this.filterOutNonsense();
|
|
|
|
var filtered = _.filter(this.files, (file: MultifileFile) => {
|
|
return !file.isMainSource && file.isIncluded;
|
|
});
|
|
|
|
return _.map(filtered, (file: MultifileFile) => {
|
|
return {
|
|
filename: file.filename,
|
|
contents: this.getFileContents(file),
|
|
};
|
|
});
|
|
}
|
|
|
|
private isMainSourceFile(file: MultifileFile): boolean {
|
|
if (this.isCMakeProject) {
|
|
if (file.filename === this.getDefaultMainCMakeFilename()) {
|
|
this.setAsMainSource(file.fileId);
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
if (this.compilerLanguageId === 'pascal') {
|
|
if (file.filename.endsWith('.dpr')) {
|
|
this.setAsMainSource(file.fileId);
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
if (file.filename === MultifileService.getDefaultMainSourceFilename(this.compilerLanguageId)) {
|
|
this.setAsMainSource(file.fileId);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return file.isMainSource;
|
|
}
|
|
|
|
public getMainSource(): string {
|
|
var mainFile = _.find(this.files, (file: MultifileFile) => {
|
|
return file.isIncluded && this.isMainSourceFile(file);
|
|
});
|
|
|
|
if (mainFile) {
|
|
return this.getFileContents(mainFile);
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
public getFileByEditorId(editorId: number): MultifileFile {
|
|
return _.find(this.files, (file: MultifileFile) => {
|
|
return file.editorId === editorId;
|
|
});
|
|
}
|
|
|
|
public getEditorIdByFilename(filename: string): number {
|
|
const file: MultifileFile = _.find(this.files, (file: MultifileFile) => {
|
|
return file.isIncluded && (file.filename === filename);
|
|
});
|
|
|
|
return (file && file.editorId > 0) ? file.editorId : null;
|
|
}
|
|
|
|
public getMainSourceEditorId(): number {
|
|
const file: MultifileFile = _.find(this.files, (file: MultifileFile) => {
|
|
return file.isIncluded && this.isMainSourceFile(file);
|
|
});
|
|
|
|
this.checkFileEditor(file);
|
|
|
|
return (file && file.editorId > 0) ? file.editorId : null;
|
|
}
|
|
|
|
private addFile(file: MultifileFile) {
|
|
this.newFileId++;
|
|
this.files.push(file);
|
|
}
|
|
|
|
public addFileForEditorId(editorId: number) {
|
|
const file: MultifileFile = {
|
|
fileId: this.newFileId,
|
|
isIncluded: false,
|
|
isOpen: true,
|
|
isMainSource: false,
|
|
filename: '',
|
|
content: '',
|
|
editorId: editorId,
|
|
langId: '',
|
|
};
|
|
|
|
this.addFile(file);
|
|
}
|
|
|
|
public removeFileByFileId(fileId: number): MultifileFile {
|
|
const file: MultifileFile = this.getFileByFileId(fileId);
|
|
|
|
this.files = this.files.filter((obj: MultifileFile) => obj.fileId !== fileId);
|
|
|
|
return file;
|
|
}
|
|
|
|
public async excludeByFileId(fileId: number): Promise<void> {
|
|
const file: MultifileFile = this.getFileByFileId(fileId);
|
|
file.isIncluded = false;
|
|
}
|
|
|
|
public async includeByFileId(fileId: number): Promise<void> {
|
|
const file: MultifileFile = this.getFileByFileId(fileId);
|
|
file.isIncluded = true;
|
|
|
|
if (file.filename === '') {
|
|
const isRenamed = await this.renameFile(fileId);
|
|
if (isRenamed) {
|
|
this.includeByFileId(fileId);
|
|
} else {
|
|
file.isIncluded = false;
|
|
}
|
|
} else {
|
|
file.isIncluded = true;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
public async includeByEditorId(editorId: number): Promise<void> {
|
|
const file: MultifileFile = this.getFileByEditorId(editorId);
|
|
|
|
return this.includeByFileId(file.fileId);
|
|
}
|
|
|
|
public forEachOpenFile(callback: (File) => void) {
|
|
this.filterOutNonsense();
|
|
|
|
for (const file of this.files) {
|
|
if (file.isOpen && file.editorId > 0) {
|
|
callback(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
public forEachFile(callback: (File) => void) {
|
|
this.filterOutNonsense();
|
|
|
|
for (const file of this.files) {
|
|
callback(file);
|
|
}
|
|
}
|
|
|
|
private getDefaultMainCMakeFilename() {
|
|
return this.cmakeMainSourceFilename;
|
|
}
|
|
|
|
private static getDefaultMainSourceFilename(langId) {
|
|
const lang = languages[langId];
|
|
const ext0 = lang.extensions[0];
|
|
return 'example' + ext0;
|
|
}
|
|
|
|
private getSuggestedFilename(file: MultifileFile, editor: any): string {
|
|
let suggestedFilename = file.filename;
|
|
if (file.filename === '') {
|
|
let langId: string = file.langId;
|
|
if (editor) {
|
|
langId = editor.currentLanguage.id;
|
|
if (editor.filename) {
|
|
suggestedFilename = editor.filename;
|
|
}
|
|
}
|
|
|
|
if (!suggestedFilename) {
|
|
if (langId === this.cmakeLangId) {
|
|
suggestedFilename = this.getDefaultMainCMakeFilename();
|
|
} else {
|
|
suggestedFilename = MultifileService.getDefaultMainSourceFilename(langId);
|
|
}
|
|
}
|
|
}
|
|
|
|
return suggestedFilename;
|
|
}
|
|
|
|
private fileExists(filename: string, excludeFile: MultifileFile): boolean {
|
|
return !!_.find(this.files, (file: MultifileFile) => {
|
|
return (file !== excludeFile) && (file.filename === filename);
|
|
});
|
|
}
|
|
|
|
public async renameFile(fileId: number): Promise<boolean> {
|
|
var file = this.getFileByFileId(fileId);
|
|
|
|
let editor: any = null;
|
|
if (file.isOpen && file.editorId > 0) {
|
|
editor = this.hub.getEditorById(file.editorId);
|
|
}
|
|
|
|
let suggestedFilename = this.getSuggestedFilename(file, editor);
|
|
|
|
return new Promise((resolve) => {
|
|
this.alertSystem.enterSomething('Rename file', 'Please enter new filename', suggestedFilename, {
|
|
yes: (value) => {
|
|
if (value !== '' && value[0] !== '/') {
|
|
if (!this.fileExists(value, file)) {
|
|
file.filename = value;
|
|
|
|
if (editor) {
|
|
editor.setFilename(file.filename);
|
|
}
|
|
|
|
resolve(true);
|
|
} else {
|
|
this.alertSystem.alert('Rename file', 'Filename already exists');
|
|
resolve(false);
|
|
}
|
|
} else {
|
|
this.alertSystem.alert('Rename file', 'Filename cannot be empty or start with a "/"');
|
|
resolve(false);
|
|
}
|
|
},
|
|
no: () => {
|
|
resolve(false);
|
|
},
|
|
yesClass: 'btn btn-primary',
|
|
yesHtml: 'Rename',
|
|
noClass: 'btn-outline-info',
|
|
noHtml: 'Cancel'
|
|
});
|
|
});
|
|
}
|
|
|
|
public async renameFileByEditorId(editorId: number): Promise<boolean> {
|
|
var file = this.getFileByEditorId(editorId);
|
|
|
|
return this.renameFile(file.fileId);
|
|
}
|
|
}
|