<commit_before>/*
 * $Id$
 *
 * Copyright (c) 2004-2005 by the TeXlapse Team.
 * 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
 */
package net.sourceforge.texlipse.viewer;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Map;
import java.util.Properties;

import net.sourceforge.texlipse.DDEClient;
import net.sourceforge.texlipse.PathUtils;
import net.sourceforge.texlipse.SelectedResourceManager;
import net.sourceforge.texlipse.TexlipsePlugin;
import net.sourceforge.texlipse.builder.BuilderRegistry;
import net.sourceforge.texlipse.builder.TexlipseBuilder;
import net.sourceforge.texlipse.builder.TexlipseNature;
import net.sourceforge.texlipse.properties.TexlipseProperties;
import net.sourceforge.texlipse.viewer.util.FileLocationListener;
import net.sourceforge.texlipse.viewer.util.FileLocationServer;
import net.sourceforge.texlipse.viewer.util.ViewerErrorScanner;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;


/**
 * A helper class for opening source files.
 * Defined separately, so that it could be used inside a static method.
 * 
 * @author <NAME>
 */
class FileLocationOpener implements FileLocationListener {
    private IProject project;
    public FileLocationOpener(IProject p) {
        project = p;
    }
    public void showLineOfFile(String file, int lineNumber) {
        ViewerOutputScanner.openInEditor(project, file, lineNumber);
    }
}

/**
 * Previewer helper class. Includes methods for launching the previewer.
 * There's no need to create instances of this class.
 * 
 * @author <NAME>
 * @author <NAME>
 * @author <NAME>
 * @author <NAME>
 */
public class ViewerManager {

    // the file name variable in the arguments
    public static final String FILENAME_PATTERN = "%file";

    // the line number variable in the arguments
    public static final String LINE_NUMBER_PATTERN = "%line";

    // the source file name variable in the arguments
    public static final String TEX_FILENAME_PATTERN = "%texfile";

    // file name with absolute path
    public static final String FILENAME_FULLPATH_PATTERN = "%fullfile";
    
    // viewer attributes
    private ViewerAttributeRegistry registry;

    // environment variables to add to current environment
    private Map envSettings;

    // the current project
    private IProject project;
    
    // progress monitor for current view operation
    private IProgressMonitor monitor;
       
    /**
     * Run the viewer configured in the given viewer attributes.
     * First check if there is a viewer already running,
     * and if there is, return that process.
     * 
     * @param reg the viewer attributes
     * @param addEnv additional environment variables, or null
     * @param monitor monitor for process
     * @return the viewer process
     * @throws CoreException if launching the viewer fails
     */
    public static Process preview(ViewerAttributeRegistry reg, Map addEnv, IProgressMonitor monitor) throws CoreException {
           	
    	ViewerManager mgr = new ViewerManager(reg, addEnv, monitor);
        if (!mgr.initialize()) {
            return null;
        }
        
        Process process = mgr.getExisting();
        if (process != null) {        	
        	// Can send DDE right away
        	mgr.sendDDEViewCommand();
			
        	return process;
        }
                
        // Process must be started first
    	process = mgr.execute();
    	
        // Send DDE if on Win32
    	if (Platform.getOS().equals(Platform.OS_WIN32)) {
        	// Since the process can take a while to start we must wait 
        	// to "make sure" the DDE command gets there. 
        	// This is probably a HACK: should be fixed/changed.
       	   		
        	try {
        		Thread.sleep(1000); // 1000 enough? Who knows!?
        		// The timeout should probably be a config setting?
			} catch (InterruptedException e) {
			}
			
        	mgr.sendDDEViewCommand();
        }
        
        return process;
    }
    
    public static void closeOutputDocument() throws CoreException {
		
        ViewerAttributeRegistry registry = new ViewerAttributeRegistry();
		
    	// Check to see if we have a running launch configuration which should
        // override the DDE close command
	 	ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
	    ILaunch[] launches = manager.getLaunches();
	    
	    for (int i = 0; i < launches.length; i++) {
	    	ILaunch l = launches[i];
	    	ILaunchConfiguration configuration = l.getLaunchConfiguration();
	    	if (configuration.getType().getIdentifier().equals(
	    			TexLaunchConfigurationDelegate.CONFIGURATION_ID)) {
	    		Map regMap = configuration.getAttributes();
		        registry.setValues(regMap);
		        break;
	    	}
		}
	    
    	ViewerManager mgr = new ViewerManager(registry, null, null);    	
		
		 if (!mgr.initialize()) {
	         return;
	     }
		
		// Can only close documents opened by DDE commands themselves
		Process process = mgr.getExisting();        
		if (process != null) {       
			mgr.sendDDECloseCommand();
            
			try {
                Thread.sleep(500); // A small delay required
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            returnFocusToEclipse(false);
		}
	}
    
    /**
     * Returns the application focus to Eclipse after launching an
     * external previewer. This is done by first activating the
     * eclipse window, and then setting focus in the editor in a
     * worker thread. Note the delay needed before setting focus. 
     * 
     * @param useMinimizeTrick if true uses a trick to force focus by
     *                         minimizing and restoring the eclipse window 
     */
    public static void returnFocusToEclipse(final boolean useMinimizeTrick) {

        // Return focus/activation to Eclipse/Texlipse
        Display display = PlatformUI.getWorkbench().getDisplay();
        if (null != display) {
            display.asyncExec(new Runnable() {
                public void run() {
                    IWorkbenchWindow[] workbenchWindows = PlatformUI.getWorkbench().getWorkbenchWindows();
                    for (int i = 0; i < workbenchWindows.length; i++) {
                        Shell shell = workbenchWindows[i].getShell();

                        if (useMinimizeTrick) {
                            shell.setMinimized(true);
                            shell.setMinimized(false);
                        }

                        shell.setActive();
                        shell.forceActive();
                        break;
                    }
                }
            });
        }
        
        // Spawn thread to set focus in the editor after the launch has completed
        // The reason we cannot do this in the current thread is because the progress
        // window is in the way and will not allow us to set focus on the editor.
        new Thread(new Runnable() {
            public void run() {
                  try { Thread.sleep(500); } catch (Exception e) { }
                  Display display = PlatformUI.getWorkbench().getDisplay();
                  if (null != display) {
                      display.asyncExec(new Runnable() {
                          public void run() {
                              IWorkbenchWindow[] workbenchWindows = PlatformUI.getWorkbench().getWorkbenchWindows();
                              for (int i = 0; i < workbenchWindows.length; i++) {
                                  IWorkbenchPage activePage = workbenchWindows[i].getActivePage();
                                  activePage.activate(activePage.getActiveEditor());
                                  // Although setFocus should not be called by clients it is
                                  // required for the focus to work. Activate alone is not enough.
                                  activePage.getActiveEditor().setFocus(); 
                              }
                          }
                      });
                  }
            }
         }).start();
    }

	/**
     * Construct a new viewer launcher.
     * @param reg viewer attributes
     * @param addEnv environment variables to add to the current environment
     */
    protected ViewerManager(ViewerAttributeRegistry reg, Map addEnv, IProgressMonitor monitor) {
        this.monitor = monitor;
    	this.registry = reg;
        this.envSettings = addEnv;
    }
    
    /**
     * Find out the current project.
     * @return true, if success
     */
    protected boolean initialize() {
        
        project = TexlipsePlugin.getCurrentProject();
        if (project == null) {
            BuilderRegistry.printToConsole(TexlipsePlugin.getResourceString("viewerNoCurrentProject"));
            return false;
        }
        
        try { // Make sure it's a LaTeX project
			if (project.getNature(TexlipseNature.NATURE_ID) == null)
			{
				return false;
			}
		} catch (CoreException e) {			
		}
        
        return true;
    }
    
    /**
     * Rebuilds the project if there has been any changes since the last build
     * 
     * @deprecated Eclipse has it's own rebuild-if-changed mechanism which works
     * much better. See Preferences->Run->Launching. Besides, the check to see
     * if a resource had changed is broken, so this method does always build, and
     * the build is done in the GUI thread, so output is not flushed in realtime.
     * 				 
     */
    protected boolean rebuildIfNeeded() {

    	// rebuild, if needed
        IPreferenceStore prefs = TexlipsePlugin.getDefault().getPreferenceStore();
        if (prefs.getBoolean(TexlipseProperties.BUILD_BEFORE_VIEW)) {
            if (TexlipseBuilder.needsRebuild()) {
            	try {
            		project.build(TexlipseBuilder.FULL_BUILD, monitor);
                } catch (OperationCanceledException e) {
                    // build failed, so no output file
                    return false;
				} catch (CoreException e) {
					// build failed, so no output file
                    return false;
				}
            }
        }
    	
        return true; // output file is up to date, continue with viewing
    }
    
    /**
     * Check if viewer already running.
     * This method returns false also, if the user has enabled multiple viewer instances.
     * @return the running viewer process, or null if viewer has already terminated
     */
    protected Process getExisting() {
        Object o = TexlipseProperties.getSessionProperty(project,
                TexlipseProperties.SESSION_ATTRIBUTE_VIEWER);
        
        if (o != null) {
            if (o instanceof Process) {
                Process p = (Process) o;     

                int code = -1;
                try {
                    code = p.exitValue();
                } catch (IllegalThreadStateException e) {
                }
                
                // there is a viewer running and forward search is not supported    
                if (code == -1 && !registry.getForward()) {
                    // ... so don't launch another viewer window
                    return p;
                }
            }
            
            TexlipseProperties.setSessionProperty(project,
                    TexlipseProperties.SESSION_ATTRIBUTE_VIEWER, null);
        }
        
        return null;
    }
    
    /**
     * Run the viewer configured in the given viewer attributes.
     * Paths are resolved so that the viewer program is run in source directory.
     * The viewer program is given a relative pathname and filename as a command line
     * argument. 
     * 
     * @return the viewer process
     * @throws CoreException if launching the viewer fails
     */
    protected Process execute() throws CoreException {

        //load settings, if changed on disk
        if (TexlipseProperties.isProjectPropertiesFileChanged(project)) {
            TexlipseProperties.loadProjectProperties(project);
        }
        
        IResource outputRes = getOuputResource(project);
        
        if (outputRes == null || !outputRes.exists()) {
            String msg = TexlipsePlugin.getResourceString("viewerNothingWithExtension");
            BuilderRegistry.printToConsole(msg.replaceAll("%s", registry.getFormat()));
            return null;
        }

        // resolve the directory to run the viewer in  
        IContainer sourceDir = TexlipseProperties.getProjectSourceDir(project);
        File dir = sourceDir.getLocation().toFile();
                
        try {
            return execute(dir);
        } catch (IOException e) {
            throw new CoreException(TexlipsePlugin.stat("Could not start previewer '"
                + registry.getActiveViewer() + "'. Please make sure you have entered "
                + "the correct path and filename in the viewer preferences.", e));
        }
    }

    protected void sendDDEViewCommand() throws CoreException {

    	if (Platform.getOS().equals(Platform.OS_WIN32)) {
        	String command =  translatePatterns(registry.getDDEViewCommand());
        	String server = registry.getDDEViewServer();
        	String topic = registry.getDDEViewTopic();

        	int error = DDEClient.execute(server, topic, command);
            if (error != 0) {
                String errorMessage = "DDE command " + command + " failed! " +
                "(server: " + server + ", topic: " + topic + ")";
                TexlipsePlugin.log(errorMessage, new Throwable(errorMessage));
            }
    	}
    }
    
    protected void sendDDECloseCommand() throws CoreException {

    	if (Platform.getOS().equals(Platform.OS_WIN32)) {
	    	String command =  translatePatterns(registry.getDDECloseCommand());
	    	String server = registry.getDDECloseServer();
	    	String topic = registry.getDDECloseTopic();
	
	    	int error = DDEClient.execute(server, topic, command);
	  		if (error != 0) {
              String errorMessage = "DDE command " + command + " failed! " +
              "(server: " + server + ", topic: " + topic + ")";
              TexlipsePlugin.log(errorMessage, new Throwable(errorMessage));
            }
    	}
    }
    
    /**
     * Resolves a relative path from one directory to another.
     * The path is returned as an OS-specific string with
     * a terminating separator.
     * 
     * @param sourcePath a directory to start from 
     * @param outputPath a directory to end up to
     * @return a relative path from sourcePath to outputPath
     */
    public static String resolveRelativePath(IPath sourcePath, IPath outputPath) {

        int same = sourcePath.matchingFirstSegments(outputPath);
        if (same == sourcePath.segmentCount()
                && same == outputPath.segmentCount()) {
            return "";
        }
            
        outputPath = outputPath.removeFirstSegments(same);
        sourcePath = sourcePath.removeFirstSegments(same);
        
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < sourcePath.segmentCount(); i++) {
            sb.append("..");
            sb.append(File.separatorChar);
        }
        
        for (int i = 0; i < outputPath.segmentCount(); i++) {
            sb.append(outputPath.segment(i));
            sb.append(File.separatorChar);
        }
        return sb.toString();
    }
    
    /**
     * Determines the Resource which should be shown. Respects partial builds.
     * @return
     * @throws CoreException
     */
    public static IResource getOuputResource(IProject project) throws CoreException { 
    	
    	String outFileName = TexlipseProperties.getProjectProperty(project,
                TexlipseProperties.OUTPUTFILE_PROPERTY);
        //Check for partial build
        Object s = TexlipseProperties.getProjectProperty(project, TexlipseProperties.PARTIAL_BUILD_PROPERTY);
        if (s != null) {
            IFile tmpFile = (IFile)TexlipseProperties.getSessionProperty(project, TexlipseProperties.PARTIAL_BUILD_FILE);
            if (tmpFile != null){
                String fmtProp = TexlipseProperties.getProjectProperty(project,
                        TexlipseProperties.OUTPUT_FORMAT);
                String name = tmpFile.getName();
                name = name.substring(0, name.lastIndexOf('.')) + "." + fmtProp;
                outFileName = name;
            }
        }
        if (outFileName == null || outFileName.length() == 0) {
            throw new CoreException(TexlipsePlugin.stat("Empty output file name."));
        }
        
        // find out the directory where the file should be
        IContainer outputDir = null;
//        String fmtProp = TexlipseProperties.getProjectProperty(project,
//                TexlipseProperties.OUTPUT_FORMAT);
//        if (registry.getFormat().equals(fmtProp)) {
            outputDir = TexlipseProperties.getProjectOutputDir(project);
/*        } else {
            String base = outFileName.substring(0, outFileName.lastIndexOf('.') + 1);
            outFileName = base + registry.getFormat();
            outputDir = TexlipseProperties.getProjectTempDir(project);
        }*/
        if (outputDir == null) {
            outputDir = project;
        }
        
        return outputDir.findMember(outFileName);
    }
    

    /**
     * Returns the current line number of the current page, if possible.
     * 
     * @author <NAME>
     * @return the current line number of the current page
     */
    private int getCurrentLineNumber() {

        int lineNumber = 0;
        /* Commented this out, because the selection-code did not work
         * with launch configurations. IWorkbenchPage.getSelection() needs
         * to run in a UI thread and ILaunchConfigurationDelegate is not one of those.
         *  - <NAME>
         * 
        IWorkbenchPage currentWorkbenchPage = TexlipsePlugin.getCurrentWorkbenchPage();
        if (currentWorkbenchPage != null) {
            
            ISelection selection = currentWorkbenchPage.getSelection();
            if (selection != null) {
                if (selection instanceof ITextSelection) {
                    ITextSelection textSelection = (ITextSelection) selection;
                    // The "srcltx" package's line numbers seem to start from 1
                    // it is also the case with latex's --source-specials option
                    lineNumber = textSelection.getStartLine() + 1;
                }
            }
        }
        */
        lineNumber = SelectedResourceManager.getDefault().getSelectedLine();
        if (lineNumber <= 0) {
            lineNumber = 1;
        }
        return lineNumber;
    }
    
    /**
     * Run the given viewer in the given directory with the given file.
     * Also start viewer output listener to enable inverse search.
     * 
     * @param dir the directory to run the viewer in
     * @return viewer process
     * @throws IOException if launching the viewer fails
     */
    private Process execute(File dir) throws IOException, CoreException {

        // argument list
        ArrayList list = new ArrayList();
        
        // add command as arg0
        String command = registry.getCommand();
        if (command.indexOf(' ') > 0) {
            command = "\"" + command + "\"";
        }
        list.add(command);
        
        // add arguments
        String args = translatePatterns(registry.getArguments());
        PathUtils.tokenizeEscapedString(args, list);
        
        // create environment
        Properties env = PathUtils.getEnv();
        if (envSettings != null) {
            env.putAll(envSettings);
        }
        //String envp[] = PathUtils.getStrings(env);
        String envp[] = PathUtils.mergeEnvFromPrefs(env, TexlipseProperties.VIEWER_ENV_SETTINGS);
        
        // print command
        BuilderRegistry.printToConsole(TexlipsePlugin.getResourceString("viewerRunning")
                + " " + command + " " + args);

        // start viewer process
        Runtime runtime = Runtime.getRuntime();
        Process process = runtime.exec((String[]) list.toArray(new String[0]), envp, dir);
        
        // save attribute
        TexlipseProperties.setSessionProperty(project,
                TexlipseProperties.SESSION_ATTRIBUTE_VIEWER, process);
        
        // start viewer listener
        startOutputListener(process.getInputStream(), registry.getInverse());
        // start error reader
        new Thread(new ViewerErrorScanner(process)).start();
        
        return process;
    }

    /**
     * Fills the %arg of the input pattern with the real values
     * @param input The input pattern
     * @return the filled string
     * @throws CoreException
     */
    private String translatePatterns(String input) throws CoreException {
    	
    	if (input == null) return null;
        
        IContainer sourceDir = TexlipseProperties.getProjectSourceDir(project);

        if (input.indexOf(FILENAME_PATTERN) >= 0) {
            // resolve relative path to the output file
            IResource outputRes = getOuputResource(project);
            String outFileName = outputRes.getName();
            outFileName = resolveRelativePath(sourceDir.getFullPath(), outputRes.getFullPath());
            outFileName = outFileName.substring(0, outFileName.length() - 1);

            input = input.replaceAll(FILENAME_PATTERN, escapeBackslashes(outFileName));
        }
    	
        if (input.indexOf(FILENAME_FULLPATH_PATTERN) >= 0) {
        	input = input.replaceAll(FILENAME_FULLPATH_PATTERN, escapeBackslashes(getOuputResource(project).getLocation().toOSString()));
        }
        
        if (input.indexOf(LINE_NUMBER_PATTERN) >= 0) {
        	input = input.replaceAll(LINE_NUMBER_PATTERN, "" + getCurrentLineNumber());
        }
        
        if (input.indexOf(TEX_FILENAME_PATTERN) >= 0) {
        	
        	IResource selectedRes = SelectedResourceManager.getDefault().getSelectedResource();
        	if (selectedRes.getType() != IResource.FOLDER) {
                selectedRes = SelectedResourceManager.getDefault().getSelectedTexResource();
            }
        	
        	String relPath = resolveRelativePath(sourceDir.getFullPath(), 
                    selectedRes.getFullPath().removeLastSegments(1));
            String texFile = relPath + selectedRes.getName();
        	input = input.replaceAll(TEX_FILENAME_PATTERN, escapeBackslashes(texFile));
        }
        
        return input;
    }
    
    /**
     * Escapes backslashes, so that the string can be given to String.replaceAll()
     * as argument without the backslashes disappearing. 
     * @param file input string, typically a filename
     * @return the input string with backslashes doubled
     */
    private String escapeBackslashes(String file) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < file.length(); i++) {
            char c = file.charAt(i);
            sb.append(c);
            if (c == '\\') {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    /**
     * Start a listener thread for the viewer program's standard output.
     * 
     * @param in input stream where the output of a viewer program will be available
     * @param viewer the name of the viewer
     */
    private void startOutputListener(InputStream in, String inverse) {
        
        if (inverse.equals(ViewerAttributeRegistry.INVERSE_SEARCH_RUN)) {
            
            FileLocationServer server = FileLocationServer.getInstance();
            server.setListener(new FileLocationOpener(project));
            if (!server.isRunning()) {
                new Thread(server).start();
            }
        } else if (inverse.equals(ViewerAttributeRegistry.INVERSE_SEARCH_STD)) {
            new Thread(new ViewerOutputScanner(project, in)).start();
        }
    }
}

<commit_msg>Fixed possible bug when looking up a running launch which was based on a config that was deleted after the launch<commit_after>/*
 * $Id$
 *
 * Copyright (c) 2004-2005 by the TeXlapse Team.
 * 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
 */
package net.sourceforge.texlipse.viewer;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Map;
import java.util.Properties;

import net.sourceforge.texlipse.DDEClient;
import net.sourceforge.texlipse.PathUtils;
import net.sourceforge.texlipse.SelectedResourceManager;
import net.sourceforge.texlipse.TexlipsePlugin;
import net.sourceforge.texlipse.builder.BuilderRegistry;
import net.sourceforge.texlipse.builder.TexlipseBuilder;
import net.sourceforge.texlipse.builder.TexlipseNature;
import net.sourceforge.texlipse.properties.TexlipseProperties;
import net.sourceforge.texlipse.viewer.util.FileLocationListener;
import net.sourceforge.texlipse.viewer.util.FileLocationServer;
import net.sourceforge.texlipse.viewer.util.ViewerErrorScanner;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;


/**
 * A helper class for opening source files.
 * Defined separately, so that it could be used inside a static method.
 * 
 * @author <NAME>
 */
class FileLocationOpener implements FileLocationListener {
    private IProject project;
    public FileLocationOpener(IProject p) {
        project = p;
    }
    public void showLineOfFile(String file, int lineNumber) {
        ViewerOutputScanner.openInEditor(project, file, lineNumber);
    }
}

/**
 * Previewer helper class. Includes methods for launching the previewer.
 * There's no need to create instances of this class.
 * 
 * @author <NAME>
 * @author <NAME>
 * @author <NAME>
 * @author <NAME>
 */
public class ViewerManager {

    // the file name variable in the arguments
    public static final String FILENAME_PATTERN = "%file";

    // the line number variable in the arguments
    public static final String LINE_NUMBER_PATTERN = "%line";

    // the source file name variable in the arguments
    public static final String TEX_FILENAME_PATTERN = "%texfile";

    // file name with absolute path
    public static final String FILENAME_FULLPATH_PATTERN = "%fullfile";
    
    // viewer attributes
    private ViewerAttributeRegistry registry;

    // environment variables to add to current environment
    private Map envSettings;

    // the current project
    private IProject project;
    
    // progress monitor for current view operation
    private IProgressMonitor monitor;
       
    /**
     * Run the viewer configured in the given viewer attributes.
     * First check if there is a viewer already running,
     * and if there is, return that process.
     * 
     * @param reg the viewer attributes
     * @param addEnv additional environment variables, or null
     * @param monitor monitor for process
     * @return the viewer process
     * @throws CoreException if launching the viewer fails
     */
    public static Process preview(ViewerAttributeRegistry reg, Map addEnv, IProgressMonitor monitor) throws CoreException {
           	
    	ViewerManager mgr = new ViewerManager(reg, addEnv, monitor);
        if (!mgr.initialize()) {
            return null;
        }
        
        Process process = mgr.getExisting();
        if (process != null) {        	
        	// Can send DDE right away
        	mgr.sendDDEViewCommand();
			
        	return process;
        }
                
        // Process must be started first
    	process = mgr.execute();
    	
        // Send DDE if on Win32
    	if (Platform.getOS().equals(Platform.OS_WIN32)) {
        	// Since the process can take a while to start we must wait 
        	// to "make sure" the DDE command gets there. 
        	// This is probably a HACK: should be fixed/changed.
       	   		
        	try {
        		Thread.sleep(1000); // 1000 enough? Who knows!?
        		// The timeout should probably be a config setting?
			} catch (InterruptedException e) {
			}
			
        	mgr.sendDDEViewCommand();
        }
        
        return process;
    }
    
    public static void closeOutputDocument() throws CoreException {
		
        ViewerAttributeRegistry registry = new ViewerAttributeRegistry();
		
    	// Check to see if we have a running launch configuration which should
        // override the DDE close command
	 	ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
	    ILaunch[] launches = manager.getLaunches();
	    
	    for (int i = 0; i < launches.length; i++) {
	    	ILaunch l = launches[i];
	    	ILaunchConfiguration configuration = l.getLaunchConfiguration();
	    	if (configuration.exists() && configuration.getType().getIdentifier().equals(
	    			TexLaunchConfigurationDelegate.CONFIGURATION_ID)) {
	    		Map regMap = configuration.getAttributes();
		        registry.setValues(regMap);
		        break;
	    	}
		}
	    
    	ViewerManager mgr = new ViewerManager(registry, null, null);    	
		
		 if (!mgr.initialize()) {
	         return;
	     }
		
		// Can only close documents opened by DDE commands themselves
		Process process = mgr.getExisting();        
		if (process != null) {       
			mgr.sendDDECloseCommand();
            
			try {
                Thread.sleep(500); // A small delay required
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            returnFocusToEclipse(false);
		}
	}
    
    /**
     * Returns the application focus to Eclipse after launching an
     * external previewer. This is done by first activating the
     * eclipse window, and then setting focus in the editor in a
     * worker thread. Note the delay needed before setting focus. 
     * 
     * @param useMinimizeTrick if true uses a trick to force focus by
     *                         minimizing and restoring the eclipse window 
     */
    public static void returnFocusToEclipse(final boolean useMinimizeTrick) {

        // Return focus/activation to Eclipse/Texlipse
        Display display = PlatformUI.getWorkbench().getDisplay();
        if (null != display) {
            display.asyncExec(new Runnable() {
                public void run() {
                    IWorkbenchWindow[] workbenchWindows = PlatformUI.getWorkbench().getWorkbenchWindows();
                    for (int i = 0; i < workbenchWindows.length; i++) {
                        Shell shell = workbenchWindows[i].getShell();

                        if (useMinimizeTrick) {
                            shell.setMinimized(true);
                            shell.setMinimized(false);
                        }

                        shell.setActive();
                        shell.forceActive();
                        break;
                    }
                }
            });
        }
        
        // Spawn thread to set focus in the editor after the launch has completed
        // The reason we cannot do this in the current thread is because the progress
        // window is in the way and will not allow us to set focus on the editor.
        new Thread(new Runnable() {
            public void run() {
                  try { Thread.sleep(500); } catch (Exception e) { }
                  Display display = PlatformUI.getWorkbench().getDisplay();
                  if (null != display) {
                      display.asyncExec(new Runnable() {
                          public void run() {
                              IWorkbenchWindow[] workbenchWindows = PlatformUI.getWorkbench().getWorkbenchWindows();
                              for (int i = 0; i < workbenchWindows.length; i++) {
                                  IWorkbenchPage activePage = workbenchWindows[i].getActivePage();
                                  activePage.activate(activePage.getActiveEditor());
                                  // Although setFocus should not be called by clients it is
                                  // required for the focus to work. Activate alone is not enough.
                                  activePage.getActiveEditor().setFocus(); 
                              }
                          }
                      });
                  }
            }
         }).start();
    }

	/**
     * Construct a new viewer launcher.
     * @param reg viewer attributes
     * @param addEnv environment variables to add to the current environment
     */
    protected ViewerManager(ViewerAttributeRegistry reg, Map addEnv, IProgressMonitor monitor) {
        this.monitor = monitor;
    	this.registry = reg;
        this.envSettings = addEnv;
    }
    
    /**
     * Find out the current project.
     * @return true, if success
     */
    protected boolean initialize() {
        
        project = TexlipsePlugin.getCurrentProject();
        if (project == null) {
            BuilderRegistry.printToConsole(TexlipsePlugin.getResourceString("viewerNoCurrentProject"));
            return false;
        }
        
        try { // Make sure it's a LaTeX project
			if (project.getNature(TexlipseNature.NATURE_ID) == null)
			{
				return false;
			}
		} catch (CoreException e) {			
		}
        
        return true;
    }
    
    /**
     * Rebuilds the project if there has been any changes since the last build
     * 
     * @deprecated Eclipse has it's own rebuild-if-changed mechanism which works
     * much better. See Preferences->Run->Launching. Besides, the check to see
     * if a resource had changed is broken, so this method does always build, and
     * the build is done in the GUI thread, so output is not flushed in realtime.
     * 				 
     */
    protected boolean rebuildIfNeeded() {

    	// rebuild, if needed
        IPreferenceStore prefs = TexlipsePlugin.getDefault().getPreferenceStore();
        if (prefs.getBoolean(TexlipseProperties.BUILD_BEFORE_VIEW)) {
            if (TexlipseBuilder.needsRebuild()) {
            	try {
            		project.build(TexlipseBuilder.FULL_BUILD, monitor);
                } catch (OperationCanceledException e) {
                    // build failed, so no output file
                    return false;
				} catch (CoreException e) {
					// build failed, so no output file
                    return false;
				}
            }
        }
    	
        return true; // output file is up to date, continue with viewing
    }
    
    /**
     * Check if viewer already running.
     * This method returns false also, if the user has enabled multiple viewer instances.
     * @return the running viewer process, or null if viewer has already terminated
     */
    protected Process getExisting() {
        Object o = TexlipseProperties.getSessionProperty(project,
                TexlipseProperties.SESSION_ATTRIBUTE_VIEWER);
        
        if (o != null) {
            if (o instanceof Process) {
                Process p = (Process) o;     

                int code = -1;
                try {
                    code = p.exitValue();
                } catch (IllegalThreadStateException e) {
                }
                
                // there is a viewer running and forward search is not supported    
                if (code == -1 && !registry.getForward()) {
                    // ... so don't launch another viewer window
                    return p;
                }
            }
            
            TexlipseProperties.setSessionProperty(project,
                    TexlipseProperties.SESSION_ATTRIBUTE_VIEWER, null);
        }
        
        return null;
    }
    
    /**
     * Run the viewer configured in the given viewer attributes.
     * Paths are resolved so that the viewer program is run in source directory.
     * The viewer program is given a relative pathname and filename as a command line
     * argument. 
     * 
     * @return the viewer process
     * @throws CoreException if launching the viewer fails
     */
    protected Process execute() throws CoreException {

        //load settings, if changed on disk
        if (TexlipseProperties.isProjectPropertiesFileChanged(project)) {
            TexlipseProperties.loadProjectProperties(project);
        }
        
        IResource outputRes = getOuputResource(project);
        
        if (outputRes == null || !outputRes.exists()) {
            String msg = TexlipsePlugin.getResourceString("viewerNothingWithExtension");
            BuilderRegistry.printToConsole(msg.replaceAll("%s", registry.getFormat()));
            return null;
        }

        // resolve the directory to run the viewer in  
        IContainer sourceDir = TexlipseProperties.getProjectSourceDir(project);
        File dir = sourceDir.getLocation().toFile();
                
        try {
            return execute(dir);
        } catch (IOException e) {
            throw new CoreException(TexlipsePlugin.stat("Could not start previewer '"
                + registry.getActiveViewer() + "'. Please make sure you have entered "
                + "the correct path and filename in the viewer preferences.", e));
        }
    }

    protected void sendDDEViewCommand() throws CoreException {

    	if (Platform.getOS().equals(Platform.OS_WIN32)) {
        	String command =  translatePatterns(registry.getDDEViewCommand());
        	String server = registry.getDDEViewServer();
        	String topic = registry.getDDEViewTopic();

        	int error = DDEClient.execute(server, topic, command);
            if (error != 0) {
                String errorMessage = "DDE command " + command + " failed! " +
                "(server: " + server + ", topic: " + topic + ")";
                TexlipsePlugin.log(errorMessage, new Throwable(errorMessage));
            }
    	}
    }
    
    protected void sendDDECloseCommand() throws CoreException {

    	if (Platform.getOS().equals(Platform.OS_WIN32)) {
	    	String command =  translatePatterns(registry.getDDECloseCommand());
	    	String server = registry.getDDECloseServer();
	    	String topic = registry.getDDECloseTopic();
	
	    	int error = DDEClient.execute(server, topic, command);
	  		if (error != 0) {
              String errorMessage = "DDE command " + command + " failed! " +
              "(server: " + server + ", topic: " + topic + ")";
              TexlipsePlugin.log(errorMessage, new Throwable(errorMessage));
            }
    	}
    }
    
    /**
     * Resolves a relative path from one directory to another.
     * The path is returned as an OS-specific string with
     * a terminating separator.
     * 
     * @param sourcePath a directory to start from 
     * @param outputPath a directory to end up to
     * @return a relative path from sourcePath to outputPath
     */
    public static String resolveRelativePath(IPath sourcePath, IPath outputPath) {

        int same = sourcePath.matchingFirstSegments(outputPath);
        if (same == sourcePath.segmentCount()
                && same == outputPath.segmentCount()) {
            return "";
        }
            
        outputPath = outputPath.removeFirstSegments(same);
        sourcePath = sourcePath.removeFirstSegments(same);
        
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < sourcePath.segmentCount(); i++) {
            sb.append("..");
            sb.append(File.separatorChar);
        }
        
        for (int i = 0; i < outputPath.segmentCount(); i++) {
            sb.append(outputPath.segment(i));
            sb.append(File.separatorChar);
        }
        return sb.toString();
    }
    
    /**
     * Determines the Resource which should be shown. Respects partial builds.
     * @return
     * @throws CoreException
     */
    public static IResource getOuputResource(IProject project) throws CoreException { 
    	
    	String outFileName = TexlipseProperties.getProjectProperty(project,
                TexlipseProperties.OUTPUTFILE_PROPERTY);
        //Check for partial build
        Object s = TexlipseProperties.getProjectProperty(project, TexlipseProperties.PARTIAL_BUILD_PROPERTY);
        if (s != null) {
            IFile tmpFile = (IFile)TexlipseProperties.getSessionProperty(project, TexlipseProperties.PARTIAL_BUILD_FILE);
            if (tmpFile != null){
                String fmtProp = TexlipseProperties.getProjectProperty(project,
                        TexlipseProperties.OUTPUT_FORMAT);
                String name = tmpFile.getName();
                name = name.substring(0, name.lastIndexOf('.')) + "." + fmtProp;
                outFileName = name;
            }
        }
        if (outFileName == null || outFileName.length() == 0) {
            throw new CoreException(TexlipsePlugin.stat("Empty output file name."));
        }
        
        // find out the directory where the file should be
        IContainer outputDir = null;
//        String fmtProp = TexlipseProperties.getProjectProperty(project,
//                TexlipseProperties.OUTPUT_FORMAT);
//        if (registry.getFormat().equals(fmtProp)) {
            outputDir = TexlipseProperties.getProjectOutputDir(project);
/*        } else {
            String base = outFileName.substring(0, outFileName.lastIndexOf('.') + 1);
            outFileName = base + registry.getFormat();
            outputDir = TexlipseProperties.getProjectTempDir(project);
        }*/
        if (outputDir == null) {
            outputDir = project;
        }
        
        return outputDir.findMember(outFileName);
    }
    

    /**
     * Returns the current line number of the current page, if possible.
     * 
     * @author <NAME>
     * @return the current line number of the current page
     */
    private int getCurrentLineNumber() {

        int lineNumber = 0;
        /* Commented this out, because the selection-code did not work
         * with launch configurations. IWorkbenchPage.getSelection() needs
         * to run in a UI thread and ILaunchConfigurationDelegate is not one of those.
         *  - <NAME>
         * 
        IWorkbenchPage currentWorkbenchPage = TexlipsePlugin.getCurrentWorkbenchPage();
        if (currentWorkbenchPage != null) {
            
            ISelection selection = currentWorkbenchPage.getSelection();
            if (selection != null) {
                if (selection instanceof ITextSelection) {
                    ITextSelection textSelection = (ITextSelection) selection;
                    // The "srcltx" package's line numbers seem to start from 1
                    // it is also the case with latex's --source-specials option
                    lineNumber = textSelection.getStartLine() + 1;
                }
            }
        }
        */
        lineNumber = SelectedResourceManager.getDefault().getSelectedLine();
        if (lineNumber <= 0) {
            lineNumber = 1;
        }
        return lineNumber;
    }
    
    /**
     * Run the given viewer in the given directory with the given file.
     * Also start viewer output listener to enable inverse search.
     * 
     * @param dir the directory to run the viewer in
     * @return viewer process
     * @throws IOException if launching the viewer fails
     */
    private Process execute(File dir) throws IOException, CoreException {

        // argument list
        ArrayList list = new ArrayList();
        
        // add command as arg0
        String command = registry.getCommand();
        if (command.indexOf(' ') > 0) {
            command = "\"" + command + "\"";
        }
        list.add(command);
        
        // add arguments
        String args = translatePatterns(registry.getArguments());
        PathUtils.tokenizeEscapedString(args, list);
        
        // create environment
        Properties env = PathUtils.getEnv();
        if (envSettings != null) {
            env.putAll(envSettings);
        }
        //String envp[] = PathUtils.getStrings(env);
        String envp[] = PathUtils.mergeEnvFromPrefs(env, TexlipseProperties.VIEWER_ENV_SETTINGS);
        
        // print command
        BuilderRegistry.printToConsole(TexlipsePlugin.getResourceString("viewerRunning")
                + " " + command + " " + args);

        // start viewer process
        Runtime runtime = Runtime.getRuntime();
        Process process = runtime.exec((String[]) list.toArray(new String[0]), envp, dir);
        
        // save attribute
        TexlipseProperties.setSessionProperty(project,
                TexlipseProperties.SESSION_ATTRIBUTE_VIEWER, process);
        
        // start viewer listener
        startOutputListener(process.getInputStream(), registry.getInverse());
        // start error reader
        new Thread(new ViewerErrorScanner(process)).start();
        
        return process;
    }

    /**
     * Fills the %arg of the input pattern with the real values
     * @param input The input pattern
     * @return the filled string
     * @throws CoreException
     */
    private String translatePatterns(String input) throws CoreException {
    	
    	if (input == null) return null;
        
        IContainer sourceDir = TexlipseProperties.getProjectSourceDir(project);

        if (input.indexOf(FILENAME_PATTERN) >= 0) {
            // resolve relative path to the output file
            IResource outputRes = getOuputResource(project);
            String outFileName = outputRes.getName();
            outFileName = resolveRelativePath(sourceDir.getFullPath(), outputRes.getFullPath());
            outFileName = outFileName.substring(0, outFileName.length() - 1);

            input = input.replaceAll(FILENAME_PATTERN, escapeBackslashes(outFileName));
        }
    	
        if (input.indexOf(FILENAME_FULLPATH_PATTERN) >= 0) {
        	input = input.replaceAll(FILENAME_FULLPATH_PATTERN, escapeBackslashes(getOuputResource(project).getLocation().toOSString()));
        }
        
        if (input.indexOf(LINE_NUMBER_PATTERN) >= 0) {
        	input = input.replaceAll(LINE_NUMBER_PATTERN, "" + getCurrentLineNumber());
        }
        
        if (input.indexOf(TEX_FILENAME_PATTERN) >= 0) {
        	
        	IResource selectedRes = SelectedResourceManager.getDefault().getSelectedResource();
        	if (selectedRes.getType() != IResource.FOLDER) {
                selectedRes = SelectedResourceManager.getDefault().getSelectedTexResource();
            }
        	
        	String relPath = resolveRelativePath(sourceDir.getFullPath(), 
                    selectedRes.getFullPath().removeLastSegments(1));
            String texFile = relPath + selectedRes.getName();
        	input = input.replaceAll(TEX_FILENAME_PATTERN, escapeBackslashes(texFile));
        }
        
        return input;
    }
    
    /**
     * Escapes backslashes, so that the string can be given to String.replaceAll()
     * as argument without the backslashes disappearing. 
     * @param file input string, typically a filename
     * @return the input string with backslashes doubled
     */
    private String escapeBackslashes(String file) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < file.length(); i++) {
            char c = file.charAt(i);
            sb.append(c);
            if (c == '\\') {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    /**
     * Start a listener thread for the viewer program's standard output.
     * 
     * @param in input stream where the output of a viewer program will be available
     * @param viewer the name of the viewer
     */
    private void startOutputListener(InputStream in, String inverse) {
        
        if (inverse.equals(ViewerAttributeRegistry.INVERSE_SEARCH_RUN)) {
            
            FileLocationServer server = FileLocationServer.getInstance();
            server.setListener(new FileLocationOpener(project));
            if (!server.isRunning()) {
                new Thread(server).start();
            }
        } else if (inverse.equals(ViewerAttributeRegistry.INVERSE_SEARCH_STD)) {
            new Thread(new ViewerOutputScanner(project, in)).start();
        }
    }
}


{'fraction_non_alphanumeric': 0.08718739980763066, 'fraction_numerical': 0.0025248477075985893, 'mean_word_length': 2.0889452834860114, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 31, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7657263', 'n_tokens_mistral': 13360, 'n_tokens_neox': 12546, 'n_words': 3605}