Automapping.js 11 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. export default class Automapping{constructor(workspace,onStatusAdded,onStatusRemoved){this._workspace=workspace;this._onStatusAdded=onStatusAdded;this._onStatusRemoved=onStatusRemoved;this._statuses=new Set();this._statusSymbol=Symbol('Automapping.Status');this._processingPromiseSymbol=Symbol('Automapping.ProcessingPromise');this._metadataSymbol=Symbol('Automapping.Metadata');this._fileSystemUISourceCodes=new Map();this._sweepThrottler=new Common.Throttler(100);const pathEncoder=new Persistence.PathEncoder();this._filesIndex=new FilePathIndex(pathEncoder);this._projectFoldersIndex=new FolderIndex(pathEncoder);this._activeFoldersIndex=new FolderIndex(pathEncoder);this._interceptors=[];this._workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded,event=>this._onUISourceCodeAdded((event.data)));this._workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeRemoved,event=>this._onUISourceCodeRemoved((event.data)));this._workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeRenamed,this._onUISourceCodeRenamed,this);this._workspace.addEventListener(Workspace.Workspace.Events.ProjectAdded,event=>this._onProjectAdded((event.data)),this);this._workspace.addEventListener(Workspace.Workspace.Events.ProjectRemoved,event=>this._onProjectRemoved((event.data)),this);for(const fileSystem of workspace.projects()){this._onProjectAdded(fileSystem);}
  2. for(const uiSourceCode of workspace.uiSourceCodes()){this._onUISourceCodeAdded(uiSourceCode);}}
  3. addNetworkInterceptor(interceptor){this._interceptors.push(interceptor);this.scheduleRemap();}
  4. scheduleRemap(){for(const status of this._statuses.valuesArray()){this._clearNetworkStatus(status.network);}
  5. this._scheduleSweep();}
  6. _scheduleSweep(){this._sweepThrottler.schedule(sweepUnmapped.bind(this));function sweepUnmapped(){const networkProjects=this._workspace.projectsForType(Workspace.projectTypes.Network);for(const networkProject of networkProjects){for(const uiSourceCode of networkProject.uiSourceCodes()){this._computeNetworkStatus(uiSourceCode);}}
  7. this._onSweepHappenedForTest();return Promise.resolve();}}
  8. _onSweepHappenedForTest(){}
  9. _onProjectRemoved(project){for(const uiSourceCode of project.uiSourceCodes()){this._onUISourceCodeRemoved(uiSourceCode);}
  10. if(project.type()!==Workspace.projectTypes.FileSystem){return;}
  11. const fileSystem=(project);for(const gitFolder of fileSystem.initialGitFolders()){this._projectFoldersIndex.removeFolder(gitFolder);}
  12. this._projectFoldersIndex.removeFolder(fileSystem.fileSystemPath());this.scheduleRemap();}
  13. _onProjectAdded(project){if(project.type()!==Workspace.projectTypes.FileSystem){return;}
  14. const fileSystem=(project);for(const gitFolder of fileSystem.initialGitFolders()){this._projectFoldersIndex.addFolder(gitFolder);}
  15. this._projectFoldersIndex.addFolder(fileSystem.fileSystemPath());project.uiSourceCodes().forEach(this._onUISourceCodeAdded.bind(this));this.scheduleRemap();}
  16. _onUISourceCodeAdded(uiSourceCode){const project=uiSourceCode.project();if(project.type()===Workspace.projectTypes.FileSystem){if(!Persistence.FileSystemWorkspaceBinding.fileSystemSupportsAutomapping(project)){return;}
  17. this._filesIndex.addPath(uiSourceCode.url());this._fileSystemUISourceCodes.set(uiSourceCode.url(),uiSourceCode);this._scheduleSweep();}else if(project.type()===Workspace.projectTypes.Network){this._computeNetworkStatus(uiSourceCode);}}
  18. _onUISourceCodeRemoved(uiSourceCode){if(uiSourceCode.project().type()===Workspace.projectTypes.FileSystem){this._filesIndex.removePath(uiSourceCode.url());this._fileSystemUISourceCodes.delete(uiSourceCode.url());const status=uiSourceCode[this._statusSymbol];if(status){this._clearNetworkStatus(status.network);}}else if(uiSourceCode.project().type()===Workspace.projectTypes.Network){this._clearNetworkStatus(uiSourceCode);}}
  19. _onUISourceCodeRenamed(event){const uiSourceCode=(event.data.uiSourceCode);const oldURL=(event.data.oldURL);if(uiSourceCode.project().type()!==Workspace.projectTypes.FileSystem){return;}
  20. this._filesIndex.removePath(oldURL);this._fileSystemUISourceCodes.delete(oldURL);const status=uiSourceCode[this._statusSymbol];if(status){this._clearNetworkStatus(status.network);}
  21. this._filesIndex.addPath(uiSourceCode.url());this._fileSystemUISourceCodes.set(uiSourceCode.url(),uiSourceCode);this._scheduleSweep();}
  22. _computeNetworkStatus(networkSourceCode){if(networkSourceCode[this._processingPromiseSymbol]||networkSourceCode[this._statusSymbol]){return;}
  23. if(this._interceptors.some(interceptor=>interceptor(networkSourceCode))){return;}
  24. if(networkSourceCode.url().startsWith('wasm://')){return;}
  25. const createBindingPromise=this._createBinding(networkSourceCode).then(validateStatus.bind(this)).then(onStatus.bind(this));networkSourceCode[this._processingPromiseSymbol]=createBindingPromise;async function validateStatus(status){if(!status){return null;}
  26. if(networkSourceCode[this._processingPromiseSymbol]!==createBindingPromise){return null;}
  27. if(status.network.contentType().isFromSourceMap()||!status.fileSystem.contentType().isTextType()){return status;}
  28. if(status.fileSystem.isDirty()&&(status.network.isDirty()||status.network.hasCommits())){return null;}
  29. const[fileSystemContent,networkContent]=await Promise.all([status.fileSystem.requestContent(),status.network.project().requestFileContent(status.network)]);if(fileSystemContent.content===null||networkContent===null){return null;}
  30. if(networkSourceCode[this._processingPromiseSymbol]!==createBindingPromise){return null;}
  31. const target=Bindings.NetworkProject.targetForUISourceCode(status.network);let isValid=false;const fileContent=fileSystemContent.content;if(target&&target.type()===SDK.Target.Type.Node){const rewrappedNetworkContent=Persistence.Persistence.rewrapNodeJSContent(status.fileSystem,fileContent,networkContent.content);isValid=fileContent===rewrappedNetworkContent;}else{isValid=fileContent.trimRight()===networkContent.content.trimRight();}
  32. if(!isValid){this._prevalidationFailedForTest(status);return null;}
  33. return status;}
  34. function onStatus(status){if(networkSourceCode[this._processingPromiseSymbol]!==createBindingPromise){return;}
  35. networkSourceCode[this._processingPromiseSymbol]=null;if(!status){this._onBindingFailedForTest();return;}
  36. if(status.network[this._statusSymbol]||status.fileSystem[this._statusSymbol]){return;}
  37. this._statuses.add(status);status.network[this._statusSymbol]=status;status.fileSystem[this._statusSymbol]=status;if(status.exactMatch){const projectFolder=this._projectFoldersIndex.closestParentFolder(status.fileSystem.url());const newFolderAdded=projectFolder?this._activeFoldersIndex.addFolder(projectFolder):false;if(newFolderAdded){this._scheduleSweep();}}
  38. this._onStatusAdded.call(null,status);}}
  39. _prevalidationFailedForTest(binding){}
  40. _onBindingFailedForTest(){}
  41. _clearNetworkStatus(networkSourceCode){if(networkSourceCode[this._processingPromiseSymbol]){networkSourceCode[this._processingPromiseSymbol]=null;return;}
  42. const status=networkSourceCode[this._statusSymbol];if(!status){return;}
  43. this._statuses.delete(status);status.network[this._statusSymbol]=null;status.fileSystem[this._statusSymbol]=null;if(status.exactMatch){const projectFolder=this._projectFoldersIndex.closestParentFolder(status.fileSystem.url());if(projectFolder){this._activeFoldersIndex.removeFolder(projectFolder);}}
  44. this._onStatusRemoved.call(null,status);}
  45. _createBinding(networkSourceCode){if(networkSourceCode.url().startsWith('file://')||networkSourceCode.url().startsWith('snippet://')){const decodedUrl=decodeURI(networkSourceCode.url());const fileSourceCode=this._fileSystemUISourceCodes.get(decodedUrl);const status=fileSourceCode?new AutomappingStatus(networkSourceCode,fileSourceCode,false):null;return Promise.resolve(status);}
  46. let networkPath=Common.ParsedURL.extractPath(networkSourceCode.url());if(networkPath===null){return Promise.resolve((null));}
  47. if(networkPath.endsWith('/')){networkPath+='index.html';}
  48. const urlDecodedNetworkPath=decodeURI(networkPath);const similarFiles=this._filesIndex.similarFiles(urlDecodedNetworkPath).map(path=>this._fileSystemUISourceCodes.get(path));if(!similarFiles.length){return Promise.resolve((null));}
  49. return this._pullMetadatas(similarFiles.concat(networkSourceCode)).then(onMetadatas.bind(this));function onMetadatas(){const activeFiles=similarFiles.filter(file=>!!this._activeFoldersIndex.closestParentFolder(file.url()));const networkMetadata=networkSourceCode[this._metadataSymbol];if(!networkMetadata||(!networkMetadata.modificationTime&&typeof networkMetadata.contentSize!=='number')){if(activeFiles.length!==1){return null;}
  50. return new AutomappingStatus(networkSourceCode,activeFiles[0],false);}
  51. let exactMatches=this._filterWithMetadata(activeFiles,networkMetadata);if(!exactMatches.length){exactMatches=this._filterWithMetadata(similarFiles,networkMetadata);}
  52. if(exactMatches.length!==1){return null;}
  53. return new AutomappingStatus(networkSourceCode,exactMatches[0],true);}}
  54. _pullMetadatas(uiSourceCodes){return Promise.all(uiSourceCodes.map(async file=>{file[this._metadataSymbol]=await file.requestMetadata();}));}
  55. _filterWithMetadata(files,networkMetadata){return files.filter(file=>{const fileMetadata=file[this._metadataSymbol];if(!fileMetadata){return false;}
  56. const timeMatches=!networkMetadata.modificationTime||Math.abs(networkMetadata.modificationTime-fileMetadata.modificationTime)<1000;const contentMatches=!networkMetadata.contentSize||fileMetadata.contentSize===networkMetadata.contentSize;return timeMatches&&contentMatches;});}}
  57. class FilePathIndex{constructor(encoder){this._encoder=encoder;this._reversedIndex=new Common.Trie();}
  58. addPath(path){const encodedPath=this._encoder.encode(path);this._reversedIndex.add(encodedPath.reverse());}
  59. removePath(path){const encodedPath=this._encoder.encode(path);this._reversedIndex.remove(encodedPath.reverse());}
  60. similarFiles(networkPath){const encodedPath=this._encoder.encode(networkPath);const longestCommonPrefix=this._reversedIndex.longestPrefix(encodedPath.reverse(),false);if(!longestCommonPrefix){return[];}
  61. return this._reversedIndex.words(longestCommonPrefix).map(encodedPath=>this._encoder.decode(encodedPath.reverse()));}}
  62. class FolderIndex{constructor(encoder){this._encoder=encoder;this._index=new Common.Trie();this._folderCount=new Map();}
  63. addFolder(path){if(path.endsWith('/')){path=path.substring(0,path.length-1);}
  64. const encodedPath=this._encoder.encode(path);this._index.add(encodedPath);const count=this._folderCount.get(encodedPath)||0;this._folderCount.set(encodedPath,count+1);return count===0;}
  65. removeFolder(path){if(path.endsWith('/')){path=path.substring(0,path.length-1);}
  66. const encodedPath=this._encoder.encode(path);const count=this._folderCount.get(encodedPath)||0;if(!count){return false;}
  67. if(count>1){this._folderCount.set(encodedPath,count-1);return false;}
  68. this._index.remove(encodedPath);this._folderCount.delete(encodedPath);return true;}
  69. closestParentFolder(path){const encodedPath=this._encoder.encode(path);const commonPrefix=this._index.longestPrefix(encodedPath,true);return this._encoder.decode(commonPrefix);}}
  70. export class AutomappingStatus{constructor(network,fileSystem,exactMatch){this.network=network;this.fileSystem=fileSystem;this.exactMatch=exactMatch;}}
  71. self.Persistence=self.Persistence||{};Persistence=Persistence||{};Persistence.Automapping=Automapping;Persistence.AutomappingStatus=AutomappingStatus;