/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * 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: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.debug; import com.google.common.base.Optional; import com.google.gwt.storage.client.Storage; import com.google.inject.Inject; import com.google.web.bindery.event.shared.EventBus; import org.eclipse.che.api.promises.client.Function; import org.eclipse.che.api.promises.client.FunctionException; import org.eclipse.che.api.promises.client.Operation; import org.eclipse.che.api.promises.client.OperationException; import org.eclipse.che.api.promises.client.Promise; import org.eclipse.che.api.promises.client.PromiseError; import org.eclipse.che.api.promises.client.PromiseProvider; import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.ide.api.app.AppContext; import org.eclipse.che.ide.api.debug.Breakpoint; import org.eclipse.che.ide.api.debug.Breakpoint.Type; import org.eclipse.che.ide.api.debug.BreakpointManager; import org.eclipse.che.ide.api.debug.BreakpointManagerObservable; import org.eclipse.che.ide.api.debug.BreakpointManagerObserver; import org.eclipse.che.ide.api.debug.BreakpointRenderer; import org.eclipse.che.ide.api.debug.BreakpointRenderer.LineChangeAction; import org.eclipse.che.ide.api.debug.HasBreakpointRenderer; import org.eclipse.che.ide.api.editor.EditorAgent; import org.eclipse.che.ide.api.editor.EditorOpenedEvent; import org.eclipse.che.ide.api.editor.EditorOpenedEventHandler; import org.eclipse.che.ide.api.editor.EditorPartPresenter; import org.eclipse.che.ide.api.editor.document.Document; import org.eclipse.che.ide.api.editor.texteditor.TextEditor; import org.eclipse.che.ide.api.event.project.DeleteProjectEvent; import org.eclipse.che.ide.api.event.project.DeleteProjectHandler; import org.eclipse.che.ide.api.resources.File; import org.eclipse.che.ide.api.resources.Project; import org.eclipse.che.ide.api.resources.Resource; import org.eclipse.che.ide.api.resources.ResourceChangedEvent; import org.eclipse.che.ide.api.resources.ResourceDelta; import org.eclipse.che.ide.api.resources.VirtualFile; import org.eclipse.che.ide.api.workspace.WorkspaceReadyEvent; import org.eclipse.che.ide.api.debug.dto.StorableBreakpointDto; import org.eclipse.che.ide.dto.DtoFactory; import org.eclipse.che.ide.resource.Path; import org.eclipse.che.ide.util.loging.Log; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Logger; import static org.eclipse.che.ide.api.debug.Breakpoint.Type.BREAKPOINT; /** * Implementation of {@link BreakpointManager} for editor. * * @author Anatoliy Bazko * @author Valeriy Svydenko * @author Dmytro Nochevnov */ public class BreakpointManagerImpl implements BreakpointManager, LineChangeAction, BreakpointManagerObservable, DebuggerManagerObserver { private static final Logger LOG = Logger.getLogger(BreakpointManagerImpl.class.getName()); private static final String LOCAL_STORAGE_BREAKPOINTS_KEY = "che-breakpoints"; private final Map<String, List<Breakpoint>> breakpoints; private final EditorAgent editorAgent; private final AppContext appContext; private final PromiseProvider promises; private final DebuggerManager debuggerManager; private final DtoFactory dtoFactory; private final List<BreakpointManagerObserver> observers; private Breakpoint currentBreakpoint; @Inject public BreakpointManagerImpl(EditorAgent editorAgent, DebuggerManager debuggerManager, EventBus eventBus, DtoFactory dtoFactory, AppContext appContext, PromiseProvider promises) { this.editorAgent = editorAgent; this.appContext = appContext; this.promises = promises; this.breakpoints = new HashMap<>(); this.debuggerManager = debuggerManager; this.dtoFactory = dtoFactory; this.observers = new ArrayList<>(); this.debuggerManager.addObserver(this); registerEventHandlers(eventBus); } @Override public void changeBreakpointState(final int lineNumber) { EditorPartPresenter editor = editorAgent.getActiveEditor(); if (editor == null) { return; } final VirtualFile activeFile = editor.getEditorInput().getFile(); List<Breakpoint> pathBreakpoints = breakpoints.get(activeFile.getLocation().toString()); if (pathBreakpoints != null) { for (final Breakpoint breakpoint : pathBreakpoints) { if (breakpoint.getLineNumber() == lineNumber) { // breakpoint already exists at given line deleteBreakpoint(activeFile, breakpoint); return; } } } if (isLineNotEmpty(activeFile, lineNumber)) { Breakpoint breakpoint = new Breakpoint(BREAKPOINT, lineNumber, activeFile.getLocation().toString(), activeFile, false); addBreakpoint(breakpoint); } } /** * Deletes breakpoint from the list and JVM. * Removes breakpoint mark. */ private void deleteBreakpoint(final VirtualFile activeFile, final Breakpoint breakpoint) { doDeleteBreakpoint(breakpoint); for (BreakpointManagerObserver observer : observers) { observer.onBreakpointDeleted(breakpoint); } Debugger debugger = debuggerManager.getActiveDebugger(); if (debugger != null) { debugger.deleteBreakpoint(activeFile, breakpoint.getLineNumber()); } } /** * Deletes breakpoint from the list. */ private void doDeleteBreakpoint(Breakpoint breakpoint) { BreakpointRenderer breakpointRenderer = getBreakpointRendererForFile(breakpoint.getPath()); if (breakpointRenderer != null) { breakpointRenderer.removeBreakpointMark(breakpoint.getLineNumber()); } String path = breakpoint.getPath(); List<Breakpoint> pathBreakpoints = breakpoints.get(path); if (pathBreakpoints != null) { pathBreakpoints.remove(breakpoint); if (pathBreakpoints.isEmpty()) { breakpoints.remove(breakpoint.getPath()); } } preserveBreakpoints(); } /** * Deletes breakpoints linked to paths from the list and JVM. * Removes breakpoints' marks. */ private void deleteBreakpoints(final Set<String> paths) { for (String path : paths) { List<Breakpoint> breakpointsToDelete = breakpoints.get(path); if (breakpointsToDelete != null) { for (Breakpoint breakpoint : new ArrayList<>(breakpointsToDelete)) { deleteBreakpoint(breakpoint.getFile(), breakpoint); } } } } /** * Adds breakpoint to the list and JVM. */ private void addBreakpoint(final Breakpoint breakpoint) { List<Breakpoint> pathBreakpoints = breakpoints.get(breakpoint.getPath()); if (pathBreakpoints == null) { pathBreakpoints = new ArrayList<>(); breakpoints.put(breakpoint.getPath(), pathBreakpoints); } if (!pathBreakpoints.contains(breakpoint)) { pathBreakpoints.add(breakpoint); } preserveBreakpoints(); final BreakpointRenderer breakpointRenderer = getBreakpointRendererForFile(breakpoint.getPath()); if (breakpointRenderer != null) { breakpointRenderer.addBreakpointMark(breakpoint.getLineNumber(), new LineChangeAction() { @Override public void onLineChange(VirtualFile file, int firstLine, int linesAdded, int linesRemoved) { BreakpointManagerImpl.this.onLineChange(file, firstLine, linesAdded, linesRemoved); } }); breakpointRenderer.setBreakpointActive(breakpoint.getLineNumber(), breakpoint.isActive()); } for (BreakpointManagerObserver observer : observers) { observer.onBreakpointAdded(breakpoint); } Debugger debugger = debuggerManager.getActiveDebugger(); if (debugger != null) { debugger.addBreakpoint(breakpoint.getFile(), breakpoint.getLineNumber()); } } /** * Indicates if line of code to add breakpoint at is executable. */ private boolean isLineNotEmpty(final VirtualFile activeFile, int lineNumber) { EditorPartPresenter editor = getEditorForFile(activeFile.getLocation().toString()); if (editor instanceof TextEditor) { Document document = ((TextEditor)editor).getDocument(); return !document.getLineContent(lineNumber).trim().isEmpty(); } return false; } private void removeBreakpointsForPath(final List<Breakpoint> pathBreakpoints) { for (final Breakpoint breakpoint : pathBreakpoints) { BreakpointRenderer breakpointRenderer = getBreakpointRendererForFile(breakpoint.getPath()); if (breakpointRenderer != null) { breakpointRenderer.removeBreakpointMark(breakpoint.getLineNumber()); } } } @Override public List<Breakpoint> getBreakpointList() { final List<Breakpoint> result = new ArrayList<>(); for (final List<Breakpoint> fileBreakpoints : breakpoints.values()) { result.addAll(fileBreakpoints); } return result; } private void setCurrentBreakpoint(String filePath, int lineNumber) { deleteCurrentBreakpoint(); EditorPartPresenter editor = getEditorForFile(filePath); if (editor != null) { VirtualFile activeFile = editor.getEditorInput().getFile(); doSetCurrentBreakpoint(activeFile, lineNumber); } } private void doSetCurrentBreakpoint(VirtualFile activeFile, int lineNumber) { currentBreakpoint = new Breakpoint(Type.CURRENT, lineNumber, activeFile.getLocation().toString(), activeFile, true); BreakpointRenderer breakpointRenderer = getBreakpointRendererForFile(activeFile.getLocation().toString()); if (breakpointRenderer != null) { breakpointRenderer.setLineActive(lineNumber, true); } } @Override public void deleteAllBreakpoints() { for (List<Breakpoint> pathBreakpoints : breakpoints.values()) { removeBreakpointsForPath(pathBreakpoints); } breakpoints.clear(); preserveBreakpoints(); for (BreakpointManagerObserver observer : observers) { observer.onAllBreakpointsDeleted(); } Debugger debugger = debuggerManager.getActiveDebugger(); if (debugger != null) { debugger.deleteAllBreakpoints(); } } public void deleteCurrentBreakpoint() { if (currentBreakpoint != null) { int oldLineNumber = currentBreakpoint.getLineNumber(); BreakpointRenderer breakpointRenderer = getBreakpointRendererForFile(currentBreakpoint.getPath()); if (breakpointRenderer != null) { breakpointRenderer.setLineActive(oldLineNumber, false); } currentBreakpoint = null; } } @Nullable private EditorPartPresenter getEditorForFile(String path) { return editorAgent.getOpenedEditor(Path.valueOf(path)); } @Nullable private BreakpointRenderer getBreakpointRendererForFile(String path) { final EditorPartPresenter editor = getEditorForFile(path); if (editor != null) { return getBreakpointRendererForEditor(editor); } else { return null; } } @Nullable private BreakpointRenderer getBreakpointRendererForEditor(final EditorPartPresenter editor) { if (editor instanceof HasBreakpointRenderer) { final BreakpointRenderer renderer = ((HasBreakpointRenderer)editor).getBreakpointRenderer(); if (renderer != null && renderer.isReady()) { return renderer; } } return null; } /** * {@inheritDoc} */ @Override public void onLineChange(final VirtualFile file, final int firstLine, final int linesAdded, final int linesRemoved) { final List<Breakpoint> fileBreakpoints = breakpoints.get(file.getLocation().toString()); final int delta = linesAdded - linesRemoved; if (fileBreakpoints != null) { LOG.fine("Change in file with breakpoints " + file.getLocation().toString()); final List<Breakpoint> toRemove = new ArrayList<>(); final List<Breakpoint> toAdd = new ArrayList<>(); for (final Breakpoint breakpoint : fileBreakpoints) { final int lineNumber = breakpoint.getLineNumber(); if (lineNumber < firstLine) { // we're before any change continue; } toRemove.add(breakpoint); toAdd.add(new Breakpoint(breakpoint.getType(), breakpoint.getLineNumber() + delta, breakpoint.getPath(), breakpoint.getFile(), breakpoint.isActive())); } for (final Breakpoint breakpoint : toRemove) { deleteBreakpoint(file, breakpoint); } for (final Breakpoint breakpoint : toAdd) { if (isLineNotEmpty(file, breakpoint.getLineNumber())) { addBreakpoint(new Breakpoint(breakpoint.getType(), breakpoint.getLineNumber(), breakpoint.getPath(), file, false)); } } } } /** * Registers events handlers. */ private void registerEventHandlers(EventBus eventBus) { eventBus.addHandler(WorkspaceReadyEvent.getType(), new WorkspaceReadyEvent.WorkspaceReadyHandler() { @Override public void onWorkspaceReady(WorkspaceReadyEvent event) { restoreBreakpoints(); } }); eventBus.addHandler(EditorOpenedEvent.TYPE, new EditorOpenedEventHandler() { @Override public void onEditorOpened(EditorOpenedEvent event) { onOpenEditor(event.getFile().getLocation().toString(), event.getEditor()); } }); eventBus.addHandler(DeleteProjectEvent.TYPE, new DeleteProjectHandler() { @Override public void onProjectDeleted(DeleteProjectEvent event) { if (breakpoints.isEmpty()) { return; } ProjectConfigDto config = event.getProjectConfig(); String path = config.getPath() + "/"; deleteBreakpoints(getBreakpointPaths(path)); } }); eventBus.addHandler(ResourceChangedEvent.getType(), new ResourceChangedEvent.ResourceChangedHandler() { @Override public void onResourceChanged(ResourceChangedEvent event) { if (event.getDelta().getKind() == ResourceDelta.REMOVED) { if (breakpoints.isEmpty()) { return; } final Resource resource = event.getDelta().getResource(); Path path = resource.getLocation(); if (resource.isFolder()) { path.addTrailingSeparator(); deleteBreakpoints(getBreakpointPaths(path.toString())); } else if (resource.isFile()) { deleteBreakpoints(Collections.singleton(path.toString())); } } } }); } /** * @param pathToFind * examples: "/test-spring/", "/test-spring/src/", "/test-spring/src/main/java/Test.java" * @return set of breakpoint paths which related to pathToFind */ private Set<String> getBreakpointPaths(String pathToFind) { Set<String> foundPaths = new HashSet<>(breakpoints.size()); for (Entry<String, List<Breakpoint>> breakpointsForPath : breakpoints.entrySet()) { String path = breakpointsForPath.getKey(); if (path.startsWith(pathToFind)) { foundPaths.add(path); } } return foundPaths; } /** * The new file has been opened in the editor. * Method reads breakpoints. */ private void onOpenEditor(String path, EditorPartPresenter editor) { final List<Breakpoint> fileBreakpoints = breakpoints.get(path); if (fileBreakpoints != null) { final BreakpointRenderer breakpointRenderer = getBreakpointRendererForEditor(editor); if (breakpointRenderer != null) { for (final Breakpoint breakpoint : fileBreakpoints) { reAddBreakpointMark(breakpointRenderer, breakpoint); } } } if (currentBreakpoint != null && path.equals(currentBreakpoint.getPath())) { BreakpointRenderer breakpointRenderer = getBreakpointRendererForFile(path); if (breakpointRenderer != null) { breakpointRenderer.setLineActive(currentBreakpoint.getLineNumber(), true); } } } private void reAddBreakpointMark(BreakpointRenderer breakpointRenderer, Breakpoint breakpoint) { int lineNumber = breakpoint.getLineNumber(); breakpointRenderer.addBreakpointMark(lineNumber, new LineChangeAction() { @Override public void onLineChange(VirtualFile file, int firstLine, int linesAdded, int linesRemoved) { BreakpointManagerImpl.this.onLineChange(file, firstLine, linesAdded, linesRemoved); } }); breakpointRenderer.setBreakpointActive(lineNumber, breakpoint.isActive()); } private void preserveBreakpoints() { Storage localStorage = Storage.getLocalStorageIfSupported(); if (localStorage != null) { List<StorableBreakpointDto> allDtoBreakpoints = new LinkedList<StorableBreakpointDto>(); List<Breakpoint> allBreakpoints = getBreakpointList(); for (Breakpoint breakpoint : allBreakpoints) { StorableBreakpointDto dto = dtoFactory.createDto(StorableBreakpointDto.class); dto.setType(breakpoint.getType()); dto.setPath(breakpoint.getPath()); dto.setLineNumber(breakpoint.getLineNumber()); if (breakpoint.getFile() instanceof Resource) { final Optional<Project> project = ((Resource)breakpoint.getFile()).getRelatedProject(); if (project.isPresent()) { final ProjectConfigDto projectDto = dtoFactory.createDto(ProjectConfigDto.class) .withName(project.get().getName()) .withPath(project.get().getPath()) .withType(project.get().getType()) .withDescription(project.get().getDescription()) .withAttributes(project.get().getAttributes()) .withMixins(project.get().getMixins()); dto.setFileProjectConfig(projectDto); //TODO need to think to change argument type from dto to model interface } } allDtoBreakpoints.add(dto); } String data = dtoFactory.toJson(allDtoBreakpoints); localStorage.setItem(LOCAL_STORAGE_BREAKPOINTS_KEY, data); } } private void restoreBreakpoints() { Storage localStorage = Storage.getLocalStorageIfSupported(); if (localStorage == null) { return; } String data = localStorage.getItem(LOCAL_STORAGE_BREAKPOINTS_KEY); if (data == null || data.isEmpty()) { return; } List<StorableBreakpointDto> allDtoBreakpoints = dtoFactory.createListDtoFromJson(data, StorableBreakpointDto.class); Promise<Void> bpPromise = promises.resolve(null); for (final StorableBreakpointDto dto : allDtoBreakpoints) { bpPromise.thenPromise(new Function<Void, Promise<Void>>() { @Override public Promise<Void> apply(Void ignored) throws FunctionException { return appContext.getWorkspaceRoot().getFile(dto.getPath()).then(new Function<Optional<File>, Void>() { @Override public Void apply(Optional<File> file) throws FunctionException { if (file.isPresent() && dto.getType() == Type.BREAKPOINT) { addBreakpoint(new Breakpoint(dto.getType(), dto.getLineNumber(), dto.getPath(), file.get(), false)); } return null; } }).catchError(new Operation<PromiseError>() { @Override public void apply(PromiseError arg) throws OperationException { Log.error(getClass(), "Failed to restore breakpoint. ", arg.getCause()); } }); } }); } } // Debugger events @Override public void onActiveDebuggerChanged(@Nullable Debugger activeDebugger) { } @Override public void onDebuggerAttached(DebuggerDescriptor debuggerDescriptor, Promise<Void> connect) { } @Override public void onDebuggerDisconnected() { for (Entry<String, List<Breakpoint>> entry : breakpoints.entrySet()) { List<Breakpoint> breakpointsForPath = entry.getValue(); for (int i = 0; i < breakpointsForPath.size(); i++) { Breakpoint breakpoint = breakpointsForPath.get(i); if (breakpoint.isActive()) { Breakpoint newInactiveBreakpoint = new Breakpoint(breakpoint.getType(), breakpoint.getLineNumber(), breakpoint.getPath(), breakpoint.getFile(), false); breakpointsForPath.set(i, newInactiveBreakpoint); BreakpointRenderer breakpointRenderer = getBreakpointRendererForFile(breakpoint.getPath()); if (breakpointRenderer != null) { breakpointRenderer.setBreakpointActive(breakpoint.getLineNumber(), false); } } } } deleteCurrentBreakpoint(); } @Override public void onBreakpointAdded(Breakpoint breakpoint) { String path = breakpoint.getPath(); List<Breakpoint> breakpointsForPath = breakpoints.get(path); if (breakpointsForPath == null) { breakpointsForPath = new ArrayList<>(); breakpoints.put(path, breakpointsForPath); } int i = breakpointsForPath.indexOf(breakpoint); if (i == -1) { breakpointsForPath.add(breakpoint); } else { breakpointsForPath.set(i, breakpoint); } BreakpointRenderer breakpointRenderer = getBreakpointRendererForFile(breakpoint.getPath()); if (breakpointRenderer != null) { breakpointRenderer.setBreakpointActive(breakpoint.getLineNumber(), breakpoint.isActive()); } preserveBreakpoints(); } @Override public void onBreakpointActivated(String filePath, int lineNumber) { List<Breakpoint> breakpointsForPath = breakpoints.get(filePath); if (breakpointsForPath == null) { return; } for (int i = 0; i < breakpointsForPath.size(); i++) { Breakpoint breakpoint = breakpointsForPath.get(i); if (breakpoint.getLineNumber() == lineNumber) { Breakpoint newActiveBreakpoint = new Breakpoint(breakpoint.getType(), breakpoint.getLineNumber(), breakpoint.getPath(), breakpoint.getFile(), true); breakpointsForPath.set(i, newActiveBreakpoint); preserveBreakpoints(); BreakpointRenderer breakpointRenderer = getBreakpointRendererForFile(breakpoint.getPath()); if (breakpointRenderer != null) { breakpointRenderer.setBreakpointActive(breakpoint.getLineNumber(), true); } } } } @Override public void onBreakpointDeleted(Breakpoint breakpoint) { } @Override public void onAllBreakpointsDeleted() { } @Override public void onPreStepInto() { deleteCurrentBreakpoint(); } @Override public void onPreStepOut() { deleteCurrentBreakpoint(); } @Override public void onPreStepOver() { deleteCurrentBreakpoint(); } @Override public void onPreResume() { deleteCurrentBreakpoint(); } @Override public void onBreakpointStopped(String filePath, String className, int lineNumber) { setCurrentBreakpoint(filePath, lineNumber - 1); } @Override public void onValueChanged(List<String> path, String newValue) { } @Override public void addObserver(BreakpointManagerObserver observer) { observers.add(observer); } @Override public void removeObserver(BreakpointManagerObserver observer) { observers.remove(observer); } }