| // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| library sourcemap.diff; | |
| import 'package:compiler/src/io/source_file.dart'; | |
| import 'html_parts.dart'; | |
| import 'output_structure.dart'; | |
| import 'sourcemap_helper.dart'; | |
| import 'sourcemap_html_helper.dart'; | |
| enum DiffKind { UNMATCHED, MATCHING, IDENTICAL } | |
| /// Id for an output column. | |
| class DiffColumn { | |
| final String type; | |
| final int? index; | |
| const DiffColumn(this.type, [this.index]); | |
| @override | |
| int get hashCode => type.hashCode * 19 + index.hashCode * 23; | |
| @override | |
| bool operator ==(other) { | |
| if (identical(this, other)) return true; | |
| if (other is! DiffColumn) return false; | |
| return type == other.type && index == other.index; | |
| } | |
| @override | |
| String toString() => '$type${index ?? ''}'; | |
| } | |
| /// A block of code in an output column. | |
| abstract class DiffColumnBlock { | |
| void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context); | |
| } | |
| /// A block consisting of pure HTML parts. | |
| class PartsColumnBlock extends DiffColumnBlock { | |
| final List<HtmlPart> parts; | |
| PartsColumnBlock(this.parts); | |
| @override | |
| void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context) { | |
| if (parts.isNotEmpty) { | |
| for (HtmlPart part in parts) { | |
| part.printHtmlOn(htmlBuffer, context); | |
| } | |
| } | |
| } | |
| } | |
| /// A block consisting of line-per-line JavaScript and source mapped Dart code. | |
| class CodeLinesColumnBlock extends DiffColumnBlock { | |
| final List<CodeLine> jsCodeLines; | |
| final Map<CodeLine, List<CodeLine>> jsToDartMap; | |
| CodeLinesColumnBlock(this.jsCodeLines, this.jsToDartMap); | |
| @override | |
| void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context) { | |
| if (jsCodeLines.isNotEmpty) { | |
| htmlBuffer.write('<table style="width:100%">'); | |
| for (CodeLine codeLine in jsCodeLines) { | |
| htmlBuffer.write('<tr><td class="${ClassNames.innerCell}">'); | |
| codeLine.printHtmlOn(htmlBuffer, context); | |
| htmlBuffer.write( | |
| '</td><td ' | |
| 'class="${ClassNames.innerCell} ${ClassNames.sourceMapped}">', | |
| ); | |
| List<CodeLine>? lines = jsToDartMap[codeLine]; | |
| if (lines != null) { | |
| for (CodeLine line in lines) { | |
| line.printHtmlOn( | |
| htmlBuffer, | |
| context.from( | |
| includeAnnotation: (a) { | |
| CodeLineAnnotation annotation = a.data; | |
| return annotation.annotationType.isSourceMapped; | |
| }, | |
| ), | |
| ); | |
| } | |
| } | |
| htmlBuffer.write('</td></tr>'); | |
| } | |
| htmlBuffer.write('</table>'); | |
| } | |
| } | |
| } | |
| /// A list of columns that should align in output. | |
| class DiffBlock { | |
| final DiffKind kind; | |
| final Map<DiffColumn, DiffColumnBlock> _columns = | |
| <DiffColumn, DiffColumnBlock>{}; | |
| DiffBlock(this.kind); | |
| void addColumnBlock(DiffColumn column, DiffColumnBlock block) { | |
| _columns[column] = block; | |
| } | |
| Iterable<DiffColumn> get columns => _columns.keys; | |
| void printHtmlOn( | |
| DiffColumn column, | |
| StringBuffer htmlBuffer, | |
| HtmlPrintContext context, | |
| ) { | |
| DiffColumnBlock? block = _columns[column]; | |
| if (block != null) { | |
| block.printHtmlOn(htmlBuffer, context); | |
| } | |
| } | |
| } | |
| bool _eq(a, b) => a == b; | |
| /// Align the content of [list1] and [list2]. | |
| /// | |
| /// If provided, [range1] and [range2] aligned the subranges of [list1] and | |
| /// [list2], otherwise the whole lists are aligned. | |
| /// | |
| /// If provided, [match] determines the equality between members of [list1] and | |
| /// [list2], otherwise `==` is used. | |
| /// | |
| /// [handleSkew] is called when a subrange of one list is not found in the | |
| /// other. | |
| /// | |
| /// [handleMatched] is called when two indices match up. | |
| /// | |
| /// [handleUnmatched] is called when two indices don't match up (none are found | |
| /// in the other list). | |
| void align( | |
| List list1, | |
| List list2, { | |
| Interval? range1, | |
| Interval? range2, | |
| bool match(a, b) = _eq, | |
| required void handleSkew(int listIndex, Interval range), | |
| required void handleMatched(List<int> indices), | |
| required void handleUnmatched(List<int> indices), | |
| }) { | |
| range1 ??= Interval(0, list1.length); | |
| range2 ??= Interval(0, list2.length); | |
| Interval? findInOther( | |
| List thisLines, | |
| Interval thisRange, | |
| List otherLines, | |
| Interval otherRange, | |
| ) { | |
| for (int index = otherRange.from; index < otherRange.to; index++) { | |
| if (match(thisLines[thisRange.from], otherLines[index])) { | |
| int offset = 1; | |
| while (thisRange.from + offset < thisRange.to && | |
| otherRange.from + offset < otherRange.to && | |
| match( | |
| thisLines[thisRange.from + offset], | |
| otherLines[otherRange.from + offset], | |
| )) { | |
| offset++; | |
| } | |
| return Interval(index, index + offset); | |
| } | |
| } | |
| return null; | |
| } | |
| int start1 = range1.from; | |
| int end1 = range1.to; | |
| int start2 = range2.from; | |
| int end2 = range2.to; | |
| const int ALIGN1 = -1; | |
| const int UNMATCHED = 0; | |
| const int ALIGN2 = 1; | |
| while (start1 < end1 && start2 < end2) { | |
| if (match(list1[start1], list2[start2])) { | |
| handleMatched([start1++, start2++]); | |
| } else { | |
| Interval subrange1 = Interval(start1, end1); | |
| Interval subrange2 = Interval(start2, end2); | |
| Interval? element2inList1 = findInOther( | |
| list1, | |
| subrange1, | |
| list2, | |
| subrange2, | |
| ); | |
| Interval? element1inList2 = findInOther( | |
| list2, | |
| subrange2, | |
| list1, | |
| subrange1, | |
| ); | |
| int choice = 0; | |
| if (element2inList1 != null) { | |
| if (element1inList2 != null) { | |
| if (element1inList2.length > 1 && element2inList1.length > 1) { | |
| choice = element2inList1.from < element1inList2.from | |
| ? ALIGN2 | |
| : ALIGN1; | |
| } else if (element2inList1.length > 1) { | |
| choice = ALIGN2; | |
| } else if (element1inList2.length > 1) { | |
| choice = ALIGN1; | |
| } else { | |
| choice = element2inList1.from < element1inList2.from | |
| ? ALIGN2 | |
| : ALIGN1; | |
| } | |
| } else { | |
| choice = ALIGN2; | |
| } | |
| } else if (element1inList2 != null) { | |
| choice = ALIGN1; | |
| } | |
| switch (choice) { | |
| case ALIGN1: | |
| handleSkew(0, Interval(start1, element1inList2!.from)); | |
| start1 = element1inList2.from; | |
| break; | |
| case ALIGN2: | |
| handleSkew(1, Interval(start2, element2inList1!.from)); | |
| start2 = element2inList1.from; | |
| break; | |
| case UNMATCHED: | |
| handleUnmatched([start1++, start2++]); | |
| break; | |
| } | |
| } | |
| } | |
| if (start1 < end1) { | |
| handleSkew(0, Interval(start1, end1)); | |
| } | |
| if (start2 < end2) { | |
| handleSkew(1, Interval(start2, end2)); | |
| } | |
| } | |
| /// Create a list of blocks containing the diff of the two output [structures] | |
| /// and the corresponding Dart code. | |
| List<DiffBlock> createDiffBlocks( | |
| List<OutputStructure> structures, | |
| SourceFileManager sourceFileManager, | |
| ) { | |
| return DiffCreator(structures, sourceFileManager).computeBlocks(); | |
| } | |
| class DiffCreator { | |
| final List<OutputStructure> structures; | |
| final SourceFileManager sourceFileManager; | |
| List<List<CodeLine>> inputLines; | |
| List<int> nextInputLine = [0, 0]; | |
| List<DiffBlock> blocks = <DiffBlock>[]; | |
| DiffCreator(List<OutputStructure> structures, this.sourceFileManager) | |
| : this.structures = structures, | |
| this.inputLines = structures.map((s) => s.lines).toList(); | |
| /// Compute [CodeSource]s defined by [entities]. | |
| Iterable<CodeSource> codeSourceFromEntities(Iterable<OutputEntity> entities) { | |
| Set<CodeSource> sources = Set<CodeSource>(); | |
| for (OutputEntity entity in entities) { | |
| if (entity.codeSource != null) { | |
| sources.add(entity.codeSource!); | |
| } | |
| } | |
| return sources; | |
| } | |
| /// Create a block with the code from [codeSources]. The [CodeSource]s in | |
| /// [mainSources] are tagged as original code sources, the rest as inlined | |
| /// code sources. | |
| DiffColumnBlock codeLinesFromCodeSources( | |
| Iterable<CodeSource> mainSources, | |
| Iterable<CodeSource> codeSources, | |
| ) { | |
| List<HtmlPart> parts = <HtmlPart>[]; | |
| for (CodeSource codeSource in codeSources) { | |
| //parts.addAll(codeLinesFromCodeSource(codeSource)); | |
| String className = mainSources.contains(codeSource) | |
| ? ClassNames.originalDart | |
| : ClassNames.inlinedDart; | |
| parts.add( | |
| new TagPart( | |
| 'div', | |
| properties: {'class': className}, | |
| content: codeLinesFromCodeSource(codeSource), | |
| ), | |
| ); | |
| } | |
| return PartsColumnBlock(parts); | |
| } | |
| /// Adds all [CodeSource]s used in [dartCodeLines] to [codeSourceSet]. | |
| void collectCodeSources( | |
| Set<CodeSource> codeSourceSet, | |
| Map<CodeLine, List<CodeLine>> dartCodeLines, | |
| ) { | |
| for (List<CodeLine> codeLines in dartCodeLines.values) { | |
| for (CodeLine dartCodeLine in codeLines) { | |
| if (dartCodeLine.lineAnnotation != null) { | |
| codeSourceSet.add(dartCodeLine.lineAnnotation); | |
| } | |
| } | |
| } | |
| } | |
| /// Checks that lines are added in sequence without gaps or duplicates. | |
| void checkLineInvariant(int index, Interval range) { | |
| int expectedLineNo = nextInputLine[index]; | |
| if (range.from != expectedLineNo) { | |
| print('Expected line no $expectedLineNo, found ${range.from}'); | |
| if (range.from < expectedLineNo) { | |
| print('Duplicate lines:'); | |
| int i = range.from; | |
| while (i <= expectedLineNo) { | |
| print(inputLines[index][i++].code); | |
| } | |
| } else { | |
| print('Missing lines:'); | |
| int i = expectedLineNo; | |
| while (i <= range.from) { | |
| print(inputLines[index][i++].code); | |
| } | |
| } | |
| } | |
| nextInputLine[index] = range.to; | |
| } | |
| /// Creates a block containing the code lines in [range] from input number | |
| /// [index]. If [codeSource] is provided, the block will contain a | |
| /// corresponding Dart code column. | |
| void handleSkew( | |
| int index, | |
| Interval range, [ | |
| Iterable<CodeSource> mainCodeSources = const <CodeSource>[], | |
| ]) { | |
| if (range.isEmpty) return; | |
| Set<CodeSource> codeSources = Set<CodeSource>(); | |
| codeSources.addAll(mainCodeSources); | |
| DiffBlock block = DiffBlock(DiffKind.UNMATCHED); | |
| checkLineInvariant(index, range); | |
| List<CodeLine> jsCodeLines = inputLines[index].sublist( | |
| range.from, | |
| range.to, | |
| ); | |
| Map<CodeLine, List<CodeLine>> dartCodeLines = dartCodeLinesFromJsCodeLines( | |
| jsCodeLines, | |
| ); | |
| block.addColumnBlock( | |
| new DiffColumn('js', index), | |
| CodeLinesColumnBlock(jsCodeLines, dartCodeLines), | |
| ); | |
| collectCodeSources(codeSources, dartCodeLines); | |
| if (codeSources.isNotEmpty) { | |
| block.addColumnBlock( | |
| const DiffColumn('dart'), | |
| codeLinesFromCodeSources(mainCodeSources, codeSources), | |
| ); | |
| } | |
| blocks.add(block); | |
| } | |
| /// Create a block containing the code lines in [ranges] from the | |
| /// corresponding JavaScript inputs. If [codeSource] is provided, the block | |
| /// will contain a corresponding Dart code column. | |
| void addLines( | |
| DiffKind kind, | |
| List<Interval> ranges, [ | |
| Iterable<CodeSource> mainCodeSources = const <CodeSource>[], | |
| ]) { | |
| if (ranges.every((range) => range.isEmpty)) return; | |
| Set<CodeSource> codeSources = Set<CodeSource>(); | |
| codeSources.addAll(mainCodeSources); | |
| DiffBlock block = DiffBlock(kind); | |
| for (int i = 0; i < ranges.length; i++) { | |
| checkLineInvariant(i, ranges[i]); | |
| List<CodeLine> jsCodeLines = inputLines[i].sublist( | |
| ranges[i].from, | |
| ranges[i].to, | |
| ); | |
| Map<CodeLine, List<CodeLine>> dartCodeLines = | |
| dartCodeLinesFromJsCodeLines(jsCodeLines); | |
| block.addColumnBlock( | |
| new DiffColumn('js', i), | |
| CodeLinesColumnBlock(jsCodeLines, dartCodeLines), | |
| ); | |
| collectCodeSources(codeSources, dartCodeLines); | |
| } | |
| if (codeSources.isNotEmpty) { | |
| block.addColumnBlock( | |
| const DiffColumn('dart'), | |
| codeLinesFromCodeSources(mainCodeSources, codeSources), | |
| ); | |
| } | |
| blocks.add(block); | |
| } | |
| /// Merge the code lines in [range1] and [range2] of the corresponding input. | |
| void addRaw(Interval range1, Interval range2) { | |
| if (range1.isEmpty && range2.isEmpty) return; | |
| match(a, b) => a.code == b.code; | |
| List<Interval>? currentMatchedIntervals; | |
| List<Interval>? currentUnmatchedIntervals; | |
| void flushMatching() { | |
| if (currentMatchedIntervals != null) { | |
| addLines(DiffKind.IDENTICAL, currentMatchedIntervals!); | |
| } | |
| currentMatchedIntervals = null; | |
| } | |
| void flushUnmatched() { | |
| if (currentUnmatchedIntervals != null) { | |
| addLines(DiffKind.UNMATCHED, currentUnmatchedIntervals!); | |
| } | |
| currentUnmatchedIntervals = null; | |
| } | |
| List<Interval> updateIntervals(List<Interval>? current, List<int> indices) { | |
| if (current == null) { | |
| return [ | |
| Interval(indices[0], indices[0] + 1), | |
| Interval(indices[1], indices[1] + 1), | |
| ]; | |
| } else { | |
| current[0] = Interval(current[0].from, indices[0] + 1); | |
| current[1] = Interval(current[1].from, indices[1] + 1); | |
| return current; | |
| } | |
| } | |
| align( | |
| inputLines[0], | |
| inputLines[1], | |
| range1: range1, | |
| range2: range2, | |
| match: match, | |
| handleSkew: (int listIndex, Interval range) { | |
| flushMatching(); | |
| flushUnmatched(); | |
| handleSkew(listIndex, range); | |
| }, | |
| handleMatched: (List<int> indices) { | |
| flushUnmatched(); | |
| currentMatchedIntervals = updateIntervals( | |
| currentMatchedIntervals, | |
| indices, | |
| ); | |
| }, | |
| handleUnmatched: (List<int> indices) { | |
| flushMatching(); | |
| currentUnmatchedIntervals = updateIntervals( | |
| currentUnmatchedIntervals, | |
| indices, | |
| ); | |
| }, | |
| ); | |
| flushMatching(); | |
| flushUnmatched(); | |
| } | |
| /// Adds the top level blocks in [childRange] for structure [index]. | |
| void addBlock(int index, Interval childRange) { | |
| addSkewedChildren(index, structures[index], childRange); | |
| } | |
| /// Adds the [entity] from structure [index]. If the [entity] supports child | |
| /// entities, these are process individually. Otherwise the lines from | |
| /// [entity] are added directly. | |
| void addSkewedEntity(int index, OutputEntity entity) { | |
| if (entity.canHaveChildren) { | |
| handleSkew(index, entity.header); | |
| addSkewedChildren(index, entity, Interval(0, entity.children.length)); | |
| handleSkew(index, entity.footer); | |
| } else { | |
| handleSkew(index, entity.interval, codeSourceFromEntities([entity])); | |
| } | |
| } | |
| /// Adds the children of [parent] in [childRange] from structure [index]. | |
| void addSkewedChildren(int index, OutputEntity parent, Interval childRange) { | |
| for (int i = childRange.from; i < childRange.to; i++) { | |
| addSkewedEntity(index, parent.getChild(i)); | |
| } | |
| } | |
| /// Adds the members of the [classes] aligned. | |
| void addMatchingContainers(List<OutputEntity> classes) { | |
| addLines(DiffKind.MATCHING, classes.map((c) => c.header).toList()); | |
| align( | |
| classes[0].children, | |
| classes[1].children, | |
| match: (a, b) => a.name == b.name, | |
| handleSkew: (int listIndex, Interval childRange) { | |
| addSkewedChildren(listIndex, classes[listIndex], childRange); | |
| }, | |
| handleMatched: (List<int> indices) { | |
| List<OutputEntity> entities = [ | |
| classes[0].getChild(indices[0]), | |
| classes[1].getChild(indices[1]), | |
| ]; | |
| if (entities.every((e) => e is Statics)) { | |
| addMatchingContainers(entities); | |
| } else { | |
| addLines( | |
| DiffKind.MATCHING, | |
| entities.map((e) => e.interval).toList(), | |
| codeSourceFromEntities(entities), | |
| ); | |
| } | |
| }, | |
| handleUnmatched: (List<int> indices) { | |
| List<Interval> intervals = [ | |
| classes[0].getChild(indices[0]).interval, | |
| classes[1].getChild(indices[1]).interval, | |
| ]; | |
| addLines(DiffKind.UNMATCHED, intervals); | |
| }, | |
| ); | |
| addLines(DiffKind.MATCHING, classes.map((c) => c.footer).toList()); | |
| } | |
| /// Adds the library blocks in [indices] from the corresponding | |
| /// [OutputStructure]s, aligning their content. | |
| void addMatchingBlocks(List<int> indices) { | |
| List<OutputEntity> blocks = [ | |
| structures[0].getChild(indices[0]), | |
| structures[1].getChild(indices[1]), | |
| ]; | |
| addLines(DiffKind.MATCHING, blocks.map((b) => b.header).toList()); | |
| align( | |
| blocks[0].children, | |
| blocks[1].children, | |
| match: (a, b) => a.name == b.name, | |
| handleSkew: (int listIndex, Interval childRange) { | |
| addSkewedChildren(listIndex, blocks[listIndex], childRange); | |
| }, | |
| handleMatched: (List<int> indices) { | |
| List<OutputEntity> entities = [ | |
| blocks[0].getChild(indices[0]), | |
| blocks[1].getChild(indices[1]), | |
| ]; | |
| if (entities.every((e) => e is LibraryClass)) { | |
| addMatchingContainers(entities); | |
| } else { | |
| addLines( | |
| DiffKind.MATCHING, | |
| entities.map((e) => e.interval).toList(), | |
| codeSourceFromEntities(entities), | |
| ); | |
| } | |
| }, | |
| handleUnmatched: (List<int> indices) { | |
| List<Interval> intervals = [ | |
| blocks[0].getChild(indices[0]).interval, | |
| blocks[1].getChild(indices[1]).interval, | |
| ]; | |
| addLines(DiffKind.UNMATCHED, intervals); | |
| }, | |
| ); | |
| addLines(DiffKind.MATCHING, blocks.map((b) => b.footer).toList()); | |
| } | |
| /// Adds the lines of the blocks in [indices] from the corresponding | |
| /// [OutputStructure]s. | |
| void addUnmatchedBlocks(List<int> indices) { | |
| List<OutputEntity> blocks = [ | |
| structures[0].getChild(indices[0]), | |
| structures[1].getChild(indices[1]), | |
| ]; | |
| addLines(DiffKind.UNMATCHED, [blocks[0].interval, blocks[1].interval]); | |
| } | |
| /// Computes the diff blocks for [OutputStructure]s. | |
| List<DiffBlock> computeBlocks() { | |
| addRaw(structures[0].header, structures[1].header); | |
| align( | |
| structures[0].children, | |
| structures[1].children, | |
| match: (a, b) => a.name == b.name, | |
| handleSkew: addBlock, | |
| handleMatched: addMatchingBlocks, | |
| handleUnmatched: addUnmatchedBlocks, | |
| ); | |
| addRaw(structures[0].footer, structures[1].footer); | |
| return blocks; | |
| } | |
| /// Creates html lines for code lines in [codeSource]. The [sourceFileManager] | |
| /// is used to read that text from the source URIs. | |
| List<HtmlPart> codeLinesFromCodeSource(CodeSource codeSource) { | |
| List<HtmlPart> lines = <HtmlPart>[]; | |
| SourceFile sourceFile = sourceFileManager.getSourceFile(codeSource.uri)!; | |
| String elementName = codeSource.name; | |
| HtmlLine line = HtmlLine(); | |
| line.htmlParts.add(new ConstHtmlPart('<span class="comment">')); | |
| line.htmlParts.add(new HtmlText('${elementName}: ${sourceFile.filename}')); | |
| line.htmlParts.add(new ConstHtmlPart('</span>')); | |
| lines.add(line); | |
| if (codeSource.begin != null) { | |
| int startLine = sourceFile.getLocation(codeSource.begin!).line - 1; | |
| int endLine = sourceFile.getLocation(codeSource.end!).line; | |
| for (CodeLine codeLine in convertAnnotatedCodeToCodeLines( | |
| sourceFile.slowText(), | |
| const <Annotation>[], | |
| startLine: startLine, | |
| endLine: endLine, | |
| )) { | |
| codeLine.lineAnnotation = codeSource; | |
| lines.add(codeLine); | |
| } | |
| } | |
| return lines; | |
| } | |
| /// Creates a map from JavaScript [CodeLine]s in [jsCodeLines] to the Dart | |
| /// [CodeLine]s references in the source information. | |
| Map<CodeLine, List<CodeLine>> dartCodeLinesFromJsCodeLines( | |
| List<CodeLine> jsCodeLines, | |
| ) { | |
| Map<CodeLine, Interval> codeLineInterval = <CodeLine, Interval>{}; | |
| Map<CodeLine, List<CodeLine>> jsToDartMap = <CodeLine, List<CodeLine>>{}; | |
| List<Annotation> annotations = <Annotation>[]; | |
| Uri? currentUri; | |
| late Interval interval; | |
| Map<Uri, Set<CodeSource>> codeSourceMap = <Uri, Set<CodeSource>>{}; | |
| for (CodeLine jsCodeLine in jsCodeLines) { | |
| for (Annotation annotation in jsCodeLine.annotations) { | |
| CodeLineAnnotation codeLineAnnotation = annotation.data; | |
| for (CodeSource codeSource in codeLineAnnotation.codeSources) { | |
| codeSourceMap | |
| .putIfAbsent(codeSource.uri, () => Set<CodeSource>()) | |
| .add(codeSource); | |
| } | |
| } | |
| } | |
| void flush() { | |
| if (currentUri == null) return; | |
| Set<CodeSource>? codeSources = codeSourceMap[currentUri]; | |
| SourceFile sourceFile = sourceFileManager.getSourceFile(currentUri)!; | |
| List<CodeLine> annotatedDartCodeLines = convertAnnotatedCodeToCodeLines( | |
| sourceFile.slowText(), | |
| annotations, | |
| startLine: interval.from, | |
| endLine: interval.to, | |
| uri: currentUri, | |
| ); | |
| if (codeSources != null) { | |
| CodeSource? currentCodeSource; | |
| late Interval currentLineInterval; | |
| for (CodeLine dartCodeLine in annotatedDartCodeLines) { | |
| if (currentCodeSource == null || | |
| !currentLineInterval.contains(dartCodeLine.lineNo)) { | |
| currentCodeSource = null; | |
| for (CodeSource codeSource in codeSources) { | |
| Interval interval = Interval( | |
| sourceFile.getLocation(codeSource.begin!).line - 1, | |
| sourceFile.getLocation(codeSource.end!).line, | |
| ); | |
| if (interval.contains(dartCodeLine.lineNo)) { | |
| currentCodeSource = codeSource; | |
| currentLineInterval = interval; | |
| break; | |
| } | |
| } | |
| } | |
| if (currentCodeSource != null) { | |
| dartCodeLine.lineAnnotation = currentCodeSource; | |
| } | |
| } | |
| } | |
| int index = 0; | |
| for (CodeLine jsCodeLine in codeLineInterval.keys) { | |
| List<CodeLine> dartCodeLines = jsToDartMap.putIfAbsent( | |
| jsCodeLine, | |
| () => <CodeLine>[], | |
| ); | |
| if (dartCodeLines.isEmpty && index < annotatedDartCodeLines.length) { | |
| dartCodeLines.add(annotatedDartCodeLines[index++]); | |
| } | |
| } | |
| while (index < annotatedDartCodeLines.length) { | |
| jsToDartMap[codeLineInterval.keys.last]!.add( | |
| annotatedDartCodeLines[index++], | |
| ); | |
| } | |
| currentUri = null; | |
| } | |
| void restart(CodeLine codeLine, CodeLocation codeLocation, int line) { | |
| flush(); | |
| currentUri = codeLocation.uri; | |
| interval = Interval(line, line + 1); | |
| annotations = <Annotation>[]; | |
| codeLineInterval.clear(); | |
| codeLineInterval[codeLine] = interval; | |
| } | |
| for (CodeLine jsCodeLine in jsCodeLines) { | |
| for (Annotation annotation in jsCodeLine.annotations) { | |
| CodeLineAnnotation codeLineAnnotation = annotation.data; | |
| for (CodeLocation location in codeLineAnnotation.codeLocations) { | |
| SourceFile sourceFile = sourceFileManager.getSourceFile( | |
| location.uri, | |
| )!; | |
| int line = sourceFile.getLocation(location.offset).line - 1; | |
| if (currentUri != location.uri) { | |
| restart(jsCodeLine, location, line); | |
| } else if (interval.inWindow(line, windowSize: 2)) { | |
| interval = interval.include(line); | |
| codeLineInterval[jsCodeLine] = interval; | |
| } else { | |
| restart(jsCodeLine, location, line); | |
| } | |
| annotations.add( | |
| new Annotation( | |
| codeLineAnnotation.annotationType, | |
| location.offset, | |
| 'id=${codeLineAnnotation.annotationId}', | |
| data: codeLineAnnotation, | |
| ), | |
| ); | |
| } | |
| } | |
| } | |
| flush(); | |
| return jsToDartMap; | |
| } | |
| } | |
| const DiffColumn column_js0 = const DiffColumn('js', 0); | |
| const DiffColumn column_js1 = const DiffColumn('js', 1); | |
| const DiffColumn column_dart = const DiffColumn('dart'); | |
| class ClassNames { | |
| static String column(DiffColumn column) => 'column_${column}'; | |
| static String identical(bool alternate) => | |
| 'identical${alternate ? '1' : '2'}'; | |
| static String corresponding(bool alternate) => | |
| 'corresponding${alternate ? '1' : '2'}'; | |
| static const String buttons = 'buttons'; | |
| static const String comment = 'comment'; | |
| static const String header = 'header'; | |
| static const String headerTable = 'header_table'; | |
| static const String headerColumn = 'header_column'; | |
| static const String legend = 'legend'; | |
| static const String table = 'table'; | |
| static const String cell = 'cell'; | |
| static const String innerCell = 'inner_cell'; | |
| static const String originalDart = 'main_dart'; | |
| static const String inlinedDart = 'inlined_dart'; | |
| static const String line = 'line'; | |
| static const String lineNumber = 'line_number'; | |
| static String colored(int index) => 'colored${index}'; | |
| static const String withSourceInfo = 'with_source_info'; | |
| static const String withoutSourceInfo = 'without_source_info'; | |
| static const String additionalSourceInfo = 'additional_source_info'; | |
| static const String unusedSourceInfo = 'unused_source_info'; | |
| static const String sourceMapped = 'source_mapped'; | |
| static const String sourceMapping = 'source_mapping'; | |
| static String sourceMappingIndex(int index) => 'source_mapping${index}'; | |
| static const String markers = 'markers'; | |
| static const String marker = 'marker'; | |
| } | |
| enum AnnotationType { | |
| withSourceInfo(ClassNames.withSourceInfo, true), | |
| withoutSourceInfo(ClassNames.withoutSourceInfo, false), | |
| additionalSourceInfo(ClassNames.additionalSourceInfo, true), | |
| unusedSourceInfo(ClassNames.unusedSourceInfo, false); | |
| final String className; | |
| final bool isSourceMapped; | |
| const AnnotationType(this.className, this.isSourceMapped); | |
| } | |
| class CodeLineAnnotation { | |
| final int annotationId; | |
| final AnnotationType annotationType; | |
| final List<CodeLocation> codeLocations; | |
| final List<CodeSource> codeSources; | |
| final String? stepInfo; | |
| int? sourceMappingIndex; | |
| CodeLineAnnotation({ | |
| required this.annotationId, | |
| required this.annotationType, | |
| required this.codeLocations, | |
| required this.codeSources, | |
| this.stepInfo, | |
| this.sourceMappingIndex, | |
| }); | |
| Map toJson(JsonStrategy strategy) { | |
| return { | |
| 'annotationId': annotationId, | |
| 'annotationType': annotationType.index, | |
| 'codeLocations': codeLocations.map((l) => l.toJson(strategy)).toList(), | |
| 'codeSources': codeSources.map((c) => c.toJson()).toList(), | |
| 'stepInfo': stepInfo, | |
| 'sourceMappingIndex': sourceMappingIndex, | |
| }; | |
| } | |
| static fromJson(Map json, JsonStrategy strategy) { | |
| return CodeLineAnnotation( | |
| annotationId: json['id'], | |
| annotationType: AnnotationType.values[json['annotationType']], | |
| codeLocations: json['codeLocations'] | |
| .map((j) => CodeLocation.fromJson(j, strategy)) | |
| .toList(), | |
| codeSources: json['codeSources'] | |
| .map((j) => CodeSource.fromJson(j)) | |
| .toList(), | |
| stepInfo: json['stepInfo'], | |
| sourceMappingIndex: json['sourceMappingIndex'], | |
| ); | |
| } | |
| } |