/** * Copyright (c) 2015-2016 Angelo ZERR. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Angelo Zerr <[email protected]> - initial API and implementation */ package ts.eclipse.ide.internal.core.resources; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.jface.text.IDocument; import org.eclipse.osgi.util.NLS; import ts.TypeScriptException; import ts.TypeScriptNoContentAvailableException; import ts.client.ITypeScriptServiceClient; import ts.client.compileonsave.CompileOnSaveAffectedFileListSingleProject; import ts.client.diagnostics.DiagnosticEventBody; import ts.client.diagnostics.IDiagnostic; import ts.client.diagnostics.IDiagnostic.DiagnosticCategory; import ts.cmd.tsc.ITypeScriptCompiler; import ts.cmd.tslint.ITypeScriptLint; import ts.eclipse.ide.core.TypeScriptCorePlugin; import ts.eclipse.ide.core.compiler.IIDETypeScriptCompiler; import ts.eclipse.ide.core.console.ITypeScriptConsoleConnector; import ts.eclipse.ide.core.resources.IIDETypeScriptFile; import ts.eclipse.ide.core.resources.IIDETypeScriptProject; import ts.eclipse.ide.core.resources.IIDETypeScriptProjectSettings; import ts.eclipse.ide.core.resources.buildpath.ITsconfigBuildPath; import ts.eclipse.ide.core.resources.buildpath.ITypeScriptBuildPath; import ts.eclipse.ide.core.resources.jsconfig.IDETsconfigJson; import ts.eclipse.ide.core.resources.watcher.IFileWatcherListener; import ts.eclipse.ide.core.resources.watcher.ProjectWatcherListenerAdapter; import ts.eclipse.ide.core.tslint.IIDETypeScriptLint; import ts.eclipse.ide.core.utils.TypeScriptResourceUtil; import ts.eclipse.ide.core.utils.WorkbenchResourceUtil; import ts.eclipse.ide.internal.core.Trace; import ts.eclipse.ide.internal.core.TypeScriptCoreMessages; import ts.eclipse.ide.internal.core.compiler.IDETypeScriptCompiler; import ts.eclipse.ide.internal.core.console.TypeScriptConsoleConnectorManager; import ts.eclipse.ide.internal.core.resources.jsonconfig.JsonConfigResourcesManager; import ts.eclipse.ide.internal.core.tslint.IDETypeScriptLint; import ts.resources.TypeScriptProject; import ts.utils.FileUtils; /** * IDE TypeScript project implementation. * */ public class IDETypeScriptProject extends TypeScriptProject implements IIDETypeScriptProject { private final static Map<IProject, IDETypeScriptProject> tsProjects = new HashMap<IProject, IDETypeScriptProject>(); private IFileWatcherListener tsconfigFileListener = new IFileWatcherListener() { @Override public void onDeleted(IFile file) { // on delete of "tsconfig.json" // stope the tsserver IDETypeScriptProject.this.disposeServer(); // Remove cache of tsconfig.json Pojo JsonConfigResourcesManager.getInstance().remove(file); // Update build path ITypeScriptBuildPath buildPath = getTypeScriptBuildPath().copy(); buildPath.removeEntry(file); buildPath.save(); } @Override public void onAdded(IFile file) { // on create of "tsconfig.json" // stope the tsserver IDETypeScriptProject.this.disposeServer(); // Remove cache of tsconfig.json Pojo JsonConfigResourcesManager.getInstance().remove(file); // When new project is imported, there are none build path // check if the tsconfig.json which is added is a default build path // (like tsconfig.json or src/tsconfig.json) if (!getTypeScriptBuildPath().hasRootContainers()) { ITypeScriptBuildPath tempBuildPath = createBuildPath(); if (tempBuildPath.hasRootContainers()) { buildPath = tempBuildPath; } } } @Override public void onChanged(IFile file) { IDETypeScriptProject.this.disposeServer(); JsonConfigResourcesManager.getInstance().remove(file); } }; private final IProject project; private ITypeScriptBuildPath buildPath; public IDETypeScriptProject(IProject project) throws CoreException { super(project.getLocation().toFile(), null); this.project = project; super.setProjectSettings(new IDETypeScriptProjectSettings(this)); synchronized (tsProjects) { tsProjects.put(project, this); } // Stop tsserver + dispose settings when project is closed, deleted. TypeScriptCorePlugin.getResourcesWatcher().addProjectWatcherListener(getProject(), new ProjectWatcherListenerAdapter() { @Override public void onClosed(IProject project) { try { dispose(); } catch (TypeScriptException e) { Trace.trace(Trace.SEVERE, "Error while closing project", e); } } @Override public void onDeleted(IProject project) { try { dispose(); } catch (TypeScriptException e) { Trace.trace(Trace.SEVERE, "Error while deleting project", e); } } private void dispose() throws TypeScriptException { IDETypeScriptProject.this.dispose(); synchronized (tsProjects) { tsProjects.remove(IDETypeScriptProject.this.getProject()); } } }); // Stop tsserver when tsconfig.json/jsconfig.json of the project is // created, deleted or modified TypeScriptCorePlugin.getResourcesWatcher().addFileWatcherListener(getProject(), FileUtils.TSCONFIG_JSON, tsconfigFileListener); TypeScriptCorePlugin.getResourcesWatcher().addFileWatcherListener(getProject(), FileUtils.JSCONFIG_JSON, tsconfigFileListener); // Should be removed when tslint-language-service will support // fs.watcher. TypeScriptCorePlugin.getResourcesWatcher().addFileWatcherListener(getProject(), FileUtils.TSLINT_JSON, tsconfigFileListener); } /** * Returns the Eclispe project. * * @return */ @Override public IProject getProject() { return project; } public static IDETypeScriptProject getTypeScriptProject(IProject project) throws CoreException { synchronized (tsProjects) { return tsProjects.get(project); } } public void load() throws IOException { } @Override public synchronized IIDETypeScriptFile openFile(IResource file, IDocument document) throws TypeScriptException { IIDETypeScriptFile tsFile = getOpenedFile(file); if (tsFile == null) { tsFile = new IDETypeScriptFile(file, document, this); } if (!tsFile.isOpened()) { tsFile.open(); } ((IDETypeScriptFile) tsFile).update(document); return tsFile; } @Override public IIDETypeScriptFile getOpenedFile(IResource file) { String fileName = WorkbenchResourceUtil.getFileName(file); return (IIDETypeScriptFile) super.getOpenedFile(fileName); } @Override public void closeFile(IResource file) throws TypeScriptException { IIDETypeScriptFile tsFile = getOpenedFile(file); if (tsFile != null) { tsFile.close(); } } @Override protected void onCreateClient(ITypeScriptServiceClient client) { configureConsole(); } @Override public void configureConsole() { synchronized (serverLock) { if (hasClient()) { // There is a TypeScript client instance., Retrieve the well // connector // the // the eclipse console. try { ITypeScriptServiceClient client = getClient(); ITypeScriptConsoleConnector connector = TypeScriptConsoleConnectorManager.getManager() .getConnector(client); if (connector != null) { if (isTraceOnConsole()) { // connect the tsserver to the eclipse console. connector.connectToConsole(client, this); } else { // disconnect the tsserver to the eclipse // console. connector.disconnectToConsole(client, this); } // Enable Install @types console (ATA) ? if (isEnableTelemetry()) { connector.connectToInstallTypesConsole(client); } else { connector.disconnectToInstallTypesConsole(client); } } } catch (TypeScriptException e) { Trace.trace(Trace.SEVERE, "Error while getting TypeScript client", e); } } } } private boolean isEnableTelemetry() { return getProjectSettings().isEnableTelemetry(); } private boolean isTraceOnConsole() { return getProjectSettings().isTraceOnConsole(); } @Override public IIDETypeScriptProjectSettings getProjectSettings() { return (IIDETypeScriptProjectSettings) super.getProjectSettings(); } @Override public boolean isInScope(IResource resource) { try { // check if the given resource is a file IFile file = resource.getType() == IResource.FILE ? (IFile) resource : null; if (file == null) { return false; } // Use project preferences, which defines include/exclude path ITsconfigBuildPath tsContainer = getTypeScriptBuildPath().findTsconfigBuildPath(resource); if (tsContainer == null) { return false; } boolean isJSFile = IDEResourcesManager.getInstance().isJsFile(resource) || IDEResourcesManager.getInstance().isJsxFile(resource); if (isJSFile) { // Can validate js file? return isJsFileIsInScope(file, tsContainer); } // is ts file is included ? return isTsFileIsInScope(file, tsContainer); } catch (CoreException e) { Trace.trace(Trace.SEVERE, "Error while getting tsconfig.json for canValidate", e); } return true; } /** * Returns true if the given js, jsx file can be validated and false otherwise. * * @param file * @return true if the given js, jsx file can be validated and false otherwise. * @throws CoreException */ private boolean isJsFileIsInScope(IFile file, ITsconfigBuildPath tsContainer) throws CoreException { if (TypeScriptResourceUtil.isEmittedFile(file)) { // the js file is an emitted file return false; } // Search if a jsconfig.json exists? IFile jsconfigFile = JsonConfigResourcesManager.getInstance().findJsconfigFile(file); if (jsconfigFile != null) { return true; } // Search if tsconfig.json exists and defines alloyJs IDETsconfigJson tsconfig = tsContainer.getTsconfig(); if (tsconfig != null && tsconfig.getCompilerOptions() != null && tsconfig.getCompilerOptions().isAllowJs() != null && tsconfig.getCompilerOptions().isAllowJs()) { return true; } // jsconfig.json was not found (ex : MyProject/node_modules), // validation must not be done. return false; } /** * Returns true if the given ts, tsx file can be validated and false otherwise. * * @param file * @return true if the given ts, tsx file can be validated and false otherwise. * @throws CoreException */ private boolean isTsFileIsInScope(IFile file, ITsconfigBuildPath tsContainer) throws CoreException { IDETsconfigJson tsconfig = tsContainer.getTsconfig(); if (tsconfig != null) { return tsconfig.isInScope(file); } // tsconfig.json was not found (ex : MyProject/node_modules), // validation must not be done. return false; } @Override public ITypeScriptBuildPath getTypeScriptBuildPath() { if (buildPath == null) { buildPath = createBuildPath(); } return buildPath; } private ITypeScriptBuildPath createBuildPath() { return ((IDETypeScriptProjectSettings) getProjectSettings()).getTypeScriptBuildPath(); } public void disposeBuildPath() { ITypeScriptBuildPath oldBuildPath = getTypeScriptBuildPath(); buildPath = null; ITypeScriptBuildPath newBuildPath = getTypeScriptBuildPath(); IDEResourcesManager.getInstance().fireBuildPathChanged(this, oldBuildPath, newBuildPath); } @Override public IIDETypeScriptCompiler getCompiler() throws TypeScriptException { return (IIDETypeScriptCompiler) super.getCompiler(); } @Override protected ITypeScriptCompiler createCompiler(File tscFile, File nodejsFile) { return new IDETypeScriptCompiler(tscFile, nodejsFile, this); } @Override public IIDETypeScriptLint getTslint() throws TypeScriptException { return (IIDETypeScriptLint) super.getTslint(); } @Override protected ITypeScriptLint createTslint(File tslintFile, File tslintJsonFile, File nodejsFile) { return new IDETypeScriptLint(tslintFile, tslintJsonFile, nodejsFile); } // --------------------------------------- Compile with tsserver @Override public void compileWithTsserver(List<IFile> updatedTsFiles, List<IFile> removedTsFiles, IProgressMonitor monitor) throws TypeScriptException { SubMonitor subMonitor = SubMonitor.convert(monitor, TypeScriptCoreMessages.IDETypeScriptProject_compile_task, 100); List<IFile> tsFilesToClose = new ArrayList<>(); try { List<String> tsFilesToCompile = new ArrayList<>(); // Collect ts files to compile by using tsserver to retrieve // dependencies files. // It works only if tsconfig.json declares "compileOnSave: true". collectTsFilesToCompile(updatedTsFiles, tsFilesToCompile, tsFilesToClose, getClient(), subMonitor); // Compile ts files with tsserver. compileTsFiles(tsFilesToCompile, getClient(), subMonitor); if (removedTsFiles.size() > 0) { // ts files was removed, how to get referenced files which must // be recompiled (with errors)? } } catch (TypeScriptException e) { throw e; } catch (Exception e) { throw new TypeScriptException(e); } finally { for (IFile tsFile : tsFilesToClose) { closeFile(tsFile); } subMonitor.done(); } } /** * Collect ts files to compile from the given ts files list. * * @param updatedTsFiles * list of TypeScript files which have changed. * @param tsFilesToCompile * list of collected ts files to compile. * @param tsFilesToClose * list of ts files to close. * @param client * @param subMonitor * @throws Exception */ private void collectTsFilesToCompile(List<IFile> updatedTsFiles, List<String> tsFilesToCompile, List<IFile> tsFilesToClose, ITypeScriptServiceClient client, SubMonitor subMonitor) throws Exception { SubMonitor loopMonitor = subMonitor.split(50).setWorkRemaining(updatedTsFiles.size()); loopMonitor.subTask(TypeScriptCoreMessages.IDETypeScriptProject_compile_collecting_step); for (IFile tsFile : updatedTsFiles) { if (loopMonitor.isCanceled()) { throw new OperationCanceledException(); } String filename = WorkbenchResourceUtil.getFileName(tsFile); loopMonitor .subTask(NLS.bind(TypeScriptCoreMessages.IDETypeScriptProject_compile_collecting_file, filename)); if (!tsFilesToCompile.contains(filename)) { collectTsFilesToCompile(filename, client, tsFilesToCompile, tsFilesToClose, loopMonitor); } loopMonitor.worked(1); // loopMonitor.split(1); } // subMonitor.setWorkRemaining(50); } /** * Collect ts files to compile from the given TypeScript file. * * @param filename * @param client * @param tsFilesToCompile * @param tsFilesToClose * @param monitor * @throws Exception */ private void collectTsFilesToCompile(String filename, ITypeScriptServiceClient client, List<String> tsFilesToCompile, List<IFile> tsFilesToClose, IProgressMonitor monitor) throws Exception { while (!monitor.isCanceled()) { try { // When tsserver is not started, it takes time, we try to collect TypeScript // files every time and stop the search if user stops the builder. List<CompileOnSaveAffectedFileListSingleProject> affectedProjects = client .compileOnSaveAffectedFileList(filename).get(5000, TimeUnit.MILLISECONDS); if (affectedProjects.size() == 0 && getOpenedFile(filename) == null) { // Case when none TypeScript files are opened. // In this case, compileOnSaveAffectedFileList returns null, the tsserver needs // having just one opened TypeScript file // in order to compileOnSaveAffectedFileList returns the well list. IFile tsFile = WorkbenchResourceUtil.findFileFromWorkspace(filename); openFile(tsFile, null); tsFilesToClose.add(tsFile); affectedProjects = client.compileOnSaveAffectedFileList(filename).get(5000, TimeUnit.MILLISECONDS); } for (CompileOnSaveAffectedFileListSingleProject affectedProject : affectedProjects) { List<String> affectedTsFilenames = affectedProject.getFileNames(); for (String affectedFilename : affectedTsFilenames) { if (!tsFilesToCompile.contains(affectedFilename)) { // In some case, tsserver returns *.d.ts files (see // https://github.com/angelozerr/typescript.java/issues/190#issuecomment-317876026) // those *.d.ts files must be ignored for compilation. if (!TypeScriptResourceUtil.isDefinitionTsFile(affectedFilename)) { tsFilesToCompile.add(affectedFilename); } } } } return; } catch (TimeoutException e) { // tsserver is not initialized, retry again... } } } /** * Compile ts files list with tsserver. * * @param tsFilesToCompile * @param client * @param subMonitor * @throws Exception */ private void compileTsFiles(List<String> tsFilesToCompile, ITypeScriptServiceClient client, SubMonitor subMonitor) throws Exception { SubMonitor loopMonitor = subMonitor.newChild(50).setWorkRemaining(tsFilesToCompile.size());// subMonitor.split(50).setWorkRemaining(tsFilesToCompile.size()); loopMonitor.subTask(TypeScriptCoreMessages.IDETypeScriptProject_compile_compiling_step); for (String filename : tsFilesToCompile) { try { if (loopMonitor.isCanceled()) { throw new OperationCanceledException(); } loopMonitor.subTask( NLS.bind(TypeScriptCoreMessages.IDETypeScriptProject_compile_compiling_file, filename)); compileTsFile(filename, client); loopMonitor.worked(1); // loopMonitor.split(1); } catch (ExecutionException e) { if (e.getCause() instanceof TypeScriptNoContentAvailableException) { // Ignore "No content available" error. } else { throw e; } } } // subMonitor.setWorkRemaining(100); } /** * Compile ts file with tsserver. * * @param filename * @param client * @throws Exception */ private void compileTsFile(String filename, ITypeScriptServiceClient client) throws Exception { // Compile the given ts filename with tsserver Boolean result = client.compileOnSaveEmitFile(filename, true).get(5000, TimeUnit.MILLISECONDS); IFile tsFile = WorkbenchResourceUtil.findFileFromWorkspace(filename); if (tsFile != null) { // Delete TypeScript error marker TypeScriptResourceUtil.deleteTscMarker(tsFile); // Add TypeScript error marker if there are errors. DiagnosticEventBody event = client.syntacticDiagnosticsSync(filename, true).get(5000, TimeUnit.MILLISECONDS); addMarker(tsFile, event); event = client.semanticDiagnosticsSync(filename, true).get(5000, TimeUnit.MILLISECONDS); addMarker(tsFile, event); } } public void addMarker(IFile tsFile, DiagnosticEventBody event) throws CoreException { List<IDiagnostic> diagnostics = event.getDiagnostics(); for (IDiagnostic diagnostic : diagnostics) { TypeScriptResourceUtil.addTscMarker(tsFile, diagnostic.getFullText(), getSeverity(diagnostic.getCategory()), diagnostic.getStartLocation().getLine()); } } private int getSeverity(DiagnosticCategory category) { switch (category) { case Message: return IMarker.SEVERITY_INFO; case Warning: return IMarker.SEVERITY_WARNING; default: return IMarker.SEVERITY_ERROR; } } }