/* * Copyright (c) 2013, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.tools.debug.core.dartium; import com.google.dart.tools.debug.core.DartDebugCorePlugin; import com.google.dart.tools.debug.core.source.WorkspaceSourceContainer; import com.google.dart.tools.debug.core.sourcemaps.SourceMap; import com.google.dart.tools.debug.core.sourcemaps.SourceMapInfo; import com.google.dart.tools.debug.core.util.ResourceChangeManager; import com.google.dart.tools.debug.core.util.ResourceChangeParticipant; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceVisitor; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; // TODO(devoncarew): use the symbol name information in the maps? // it's possible this will help us de-mangle the method names for frames /** * A class to help manage parsing and querying source maps. It automatically parses source maps and * keeps that info up to date. It also helps retrieve information about map sources and map targets. * A source map contains information mapping locations in source files to locations in target files. * For instance foo.dart.js ==> [foo.dart, bar.dart, baz.dart]. The reverse direction is from * targets ==> sources. * * @see SourceMap */ public class SourceMapManager implements ResourceChangeParticipant { public static class SourceLocation { public IFile file; public int line; public int column; public SourceLocation() { } public SourceLocation(IFile file, int line) { this.file = file; this.line = line; } public SourceLocation(IFile file, int line, int column) { this.file = file; this.line = line; this.column = column; } public int getColumn() { return column; } public IFile getFile() { return file; } public int getLine() { return line; } public void setLine(int line) { this.line = line; } @Override public String toString() { return "[" + file + "," + line + "," + column + "]"; } } private static final int MAX_SOURCE_MAPS = 10; private Map<IFile, SourceMap> sourceMaps = new LinkedHashMap<IFile, SourceMap>(); public SourceMapManager(IProject project) { // Collect all maps in the current project. try { project.accept(new IResourceVisitor() { @Override public boolean visit(IResource resource) throws CoreException { if (resource instanceof IFile && isMapFileName((IFile) resource)) { handleFileAdded((IFile) resource); } return true; } }); } catch (CoreException e) { } // Listen for changes to the maps. ResourceChangeManager.getManager().addChangeParticipant(this); } public void dispose() { sourceMaps.clear(); ResourceChangeManager.removeChangeParticipant(this); } /** * Given a source (foo.dart.js) file and a location, return the corresponding target location (in * foo.dart). * * @param file * @param line * @param column * @return */ public SourceLocation getMappingFor(IFile file, int line, int column) { IFile mapFile = file.getParent().getFile(new Path(file.getName() + SourceMap.SOURCE_MAP_EXT)); SourceMap map = sourceMaps.get(mapFile); if (map != null) { // Re-order the map. sourceMaps.remove(mapFile); sourceMaps.put(mapFile, map); // Return mapping info. SourceMapInfo mapping = map.getMappingFor(line, column); if (mapping != null) { IFile resolvedFile = resolveFile(mapFile, mapping.getFile()); if (resolvedFile != null) { return new SourceLocation(resolvedFile, mapping.getLine()); } } } return null; } /** * Given a target location (in foo.dart), return the corresponding source location (in * foo.dart.js). * * @param file * @param line * @return */ public List<SourceLocation> getReverseMappingsFor(IFile targetFile, int line) { List<SourceLocation> mappings = new ArrayList<SourceMapManager.SourceLocation>(); Set<IFile> foundMappings = new HashSet<IFile>(); synchronized (sourceMaps) { for (IFile sourceFile : sourceMaps.keySet()) { SourceMap map = sourceMaps.get(sourceFile); for (String path : map.getSourceNames()) { // TODO(devoncarew): the files in the maps should all be pre-resolved IFile file = resolveFile(sourceFile, path); if (file != null && file.equals(targetFile)) { List<SourceMapInfo> reverseMappings = map.getReverseMappingsFor(path, line); foundMappings.add(file); for (SourceMapInfo reverseMapping : reverseMappings) { if (reverseMapping != null) { IFile mapSource = map.getMapSource(); if (mapSource != null) { mappings.add(new SourceLocation( mapSource, reverseMapping.getLine(), reverseMapping.getColumn())); } } } } } } } for (IFile mapFile : foundMappings) { // Re-order the map. SourceMap map = sourceMaps.remove(mapFile); sourceMaps.put(mapFile, map); } return mappings; } @Override public void handleFileAdded(IFile file) { handleFileChanged(file); } @Override public final void handleFileChanged(IFile file) { if (isMapFileName(file)) { if (shouldFilterMapFile(file)) { synchronized (sourceMaps) { sourceMaps.remove(file); } } else { try { // We speculatively parse the .map file to determine if it is indeed a source map. SourceMap sourceMap = SourceMap.createFrom(file); synchronized (sourceMaps) { // It's a source map file; put it in the source map map. sourceMaps.remove(file); sourceMaps.put(file, sourceMap); // Make sure we bound how many source maps we remember. The source map hashmap is sorted // by least recently used. if (sourceMaps.size() > MAX_SOURCE_MAPS) { IFile firstFile = sourceMaps.keySet().iterator().next(); sourceMaps.remove(firstFile); } } } catch (Exception ce) { synchronized (sourceMaps) { sourceMaps.remove(file); } } } } } @Override public void handleFileRemoved(IFile file) { if (isMapFileName(file)) { synchronized (sourceMaps) { sourceMaps.remove(file); } } } /** * Returns true if the the source map manager contains mapping information for the given file back * to original resources. * * @param resource * @return true if the the source map manager contains mapping information for the given file */ public boolean isMapSource(IFile file) { if (file != null) { IFile mapFile = file.getParent().getFile(new Path(file.getName() + SourceMap.SOURCE_MAP_EXT)); if (mapFile.exists() && sourceMaps.containsKey(mapFile)) { return true; } } return false; } public boolean isMapTarget(IFile targetFile) { if (targetFile != null) { synchronized (sourceMaps) { for (IFile sourceFile : sourceMaps.keySet()) { SourceMap map = sourceMaps.get(sourceFile); for (String path : map.getSourceNames()) { // TODO(devoncarew): the files in the maps should all be pre-resolved IFile file = resolveFile(sourceFile, path); if (file != null && file.equals(targetFile)) { return true; } } } } } return false; } /** * Filter out any source map file that lives in a packages directory. */ boolean shouldFilterMapFile(IFile file) { IPath path = file.getLocation(); if (path != null) { String str = path.toPortableString(); return str.contains("/packages/"); } return false; } private boolean isMapFileName(IFile file) { return file.getName().endsWith(SourceMap.SOURCE_MAP_EXT); } private IFile resolveFile(IFile relativeFile, String path) { if (path.startsWith("file:")) { try { // These incoming uris are not properly uri encoded. If we uri encoded them, we would then // not be able to handle properly uri encoded uris. Instead we handle certain illegal chars. //String encodedPath = URIUtilities.uriEncode(path); String encodedPath = path.replaceAll(" ", "%20"); URI uri = new URI(encodedPath); IResource resource = WorkspaceSourceContainer.locatePathAsResource(uri.getPath()); if (resource instanceof IFile) { return (IFile) resource; } } catch (URISyntaxException ex) { DartDebugCorePlugin.logError(ex); } return null; } else { IFile file = relativeFile.getParent().getFile(new Path(path)); if (file.exists()) { return file; } else { return null; } } } }