blob: 71383f718b66fb45f302863eb3b698ca65121b48 [file] [log] [blame]
// Copyright (c) 2014, 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 services.correction.change;
import 'package:analysis_server/src/constants.dart';
import 'package:analysis_server/src/services/json.dart';
import 'package:analyzer/src/generated/source.dart';
/**
* A description of a single change to one or more files. 
*/
class Change implements HasToJson {
/**
* A textual description of the change to be applied. 
*/
final String message;
/**
* A list of the [FileEdit]s used to effect the change. 
*/
final List<FileEdit> fileEdits = <FileEdit>[];
/**
* A list of the [LinkedEditGroup]s in the change. 
*/
final List<LinkedEditGroup> linkedEditGroups = <LinkedEditGroup>[];
/**
* The position that should be selected after the edits have been applied.
*/
Position selection;
Change(this.message);
/**
* Adds [edit] to the [FileEdit] for the given [file].
*/
void addEdit(String file, Edit edit) {
FileEdit fileEdit = getFileEdit(file);
if (fileEdit == null) {
fileEdit = new FileEdit(file);
addFileEdit(fileEdit);
}
fileEdit.add(edit);
}
/**
* Adds the given [FileEdit].
*/
void addFileEdit(FileEdit edit) {
fileEdits.add(edit);
}
/**
* Adds the given [LinkedEditGroup].
*/
void addLinkedEditGroup(LinkedEditGroup linkedEditGroup) {
linkedEditGroups.add(linkedEditGroup);
}
/**
* Returns the [FileEdit] for the given [file], maybe `null`.
*/
FileEdit getFileEdit(String file) {
for (FileEdit fileEdit in fileEdits) {
if (fileEdit.file == file) {
return fileEdit;
}
}
return null;
}
@override
Map<String, Object> toJson() {
Map<String, Object> json = {
MESSAGE: message,
EDITS: objectToJson(fileEdits),
LINKED_EDIT_GROUPS: objectToJson(linkedEditGroups)
};
if (selection != null) {
json[SELECTION] = selection.toJson();
}
return json;
}
@override
String toString() =>
'Change(message=$message, edits=$fileEdits, '
'linkedEditGroups=$linkedEditGroups, selection=$selection)';
}
/**
* A description of a single change to a single file. 
*/
class Edit implements HasToJson {
/**
* The offset of the region to be modified. 
*/
final int offset;
/**
* The length of the region to be modified.
*/
final int length;
/**
* The text that is to replace the specified region in the original text. 
*/
final String replacement;
/**
* An identifier that uniquely identifies this source edit from other edits in
* the same response. This field is omitted unless a containing structure
* needs to be able to identify the edit for some reason.
*
* For example, some refactoring operations can produce edits that might not
* be appropriate (referred to as potential edits). Such edits will have an id
* so that they can be referenced. Edits in the same response that do not need
* to be referenced will not have an id.
*/
String id;
Edit(this.offset, this.length, this.replacement);
Edit.range(SourceRange range, String replacement)
: this(range.offset, range.length, replacement);
/**
* The offset of a character immediately after the region to be modified. 
*/
int get end => offset + length;
bool operator ==(other) {
if (other is Edit) {
return other.offset == offset &&
other.length == length &&
other.replacement == replacement;
}
return false;
}
/**
* Get the result of applying the edit to the given [code].
*/
String apply(String code) {
return code.substring(0, offset) + replacement + code.substring(end);
}
@override
Map<String, Object> toJson() {
return {
OFFSET: offset,
LENGTH: length,
REPLACEMENT: replacement
};
}
@override
String toString() {
StringBuffer sb = new StringBuffer();
sb.write('Edit(offset=');
sb.write(offset);
sb.write(', length=');
sb.write(length);
sb.write(', replacement=:>');
sb.write(replacement);
sb.write('<:');
if (id != null) {
sb.write(', id=');
sb.write(id);
}
sb.write(')');
return sb.toString();
}
/**
* Get the result of applying a set of [edits] to the given [code]. Edits
* are applied in the order they appear in [edits].
*/
static String applySequence(String code, Iterable<Edit> edits) {
edits.forEach((Edit edit) {
code = edit.apply(code);
});
return code;
}
}
/**
* A description of a set of changes to a single file.
*
* [Edit]s are added in the order of decreasing offset, so they are easy to
* apply to the original file content without correcting offsets.
*/
class FileEdit implements HasToJson {
/**
* The file to be modified.
*/
final String file;
/**
* A list of the [Edit]s used to effect the change. 
*/
final List<Edit> edits = <Edit>[];
FileEdit(this.file);
/**
* Adds the given [Edit] to the list.
*/
void add(Edit edit) {
int index = 0;
while (index < edits.length && edits[index].offset > edit.offset) {
index++;
}
edits.insert(index, edit);
}
/**
* Adds the given [Edit]s.
*/
void addAll(Iterable<Edit> edits) {
edits.forEach(add);
}
@override
Map<String, Object> toJson() {
return {
FILE: file,
EDITS: objectToJson(edits)
};
}
@override
String toString() => "FileEdit(file=$file, edits=$edits)";
}
/**
* A group of linked [Position]s in multiple files that are simultaneously
* modified - if one gets edited, all other positions in a group are edited the
* same way. All linked positions in a group have the same content.
*/
class LinkedEditGroup implements HasToJson {
final String id;
int length;
final List<Position> positions = <Position>[];
final List<LinkedEditSuggestion> suggestions = <LinkedEditSuggestion>[];
LinkedEditGroup(this.id);
void addPosition(Position position, int length) {
positions.add(position);
this.length = length;
}
void addSuggestion(LinkedEditSuggestion suggestion) {
suggestions.add(suggestion);
}
@override
Map<String, Object> toJson() {
return {
ID: id,
LENGTH: length,
POSITIONS: objectToJson(positions),
SUGGESTIONS: objectToJson(suggestions)
};
}
@override
String toString() =>
'LinkedEditGroup(id=$id, length=$length, '
'positions=$positions, suggestions=$suggestions)';
}
/**
* A suggestion of a value that could be used to replace all of the linked edit
* regions in a [LinkedEditGroup].
*/
class LinkedEditSuggestion implements HasToJson {
final LinkedEditSuggestionKind kind;
final String value;
LinkedEditSuggestion(this.kind, this.value);
bool operator ==(other) {
if (other is LinkedEditSuggestion) {
return other.kind == kind && other.value == value;
}
return false;
}
@override
Map<String, Object> toJson() {
return {
KIND: kind.name,
VALUE: value
};
}
@override
String toString() => '(kind=$kind, value=$value)';
}
/**
* An enumeration of the kind of values that can be suggested for a linked edit.
*/
class LinkedEditSuggestionKind {
static const METHOD = const LinkedEditSuggestionKind('METHOD');
static const PARAMETER = const LinkedEditSuggestionKind('PARAMETER');
static const TYPE = const LinkedEditSuggestionKind('TYPE');
static const VARIABLE = const LinkedEditSuggestionKind('VARIABLE');
final String name;
const LinkedEditSuggestionKind(this.name);
@override
String toString() => name;
}
/**
* A position in a file.
*/
class Position implements HasToJson {
final String file;
final int offset;
Position(this.file, this.offset);
int get hashCode {
int hash = file.hashCode;
hash = hash * 31 + offset;
return hash;
}
bool operator ==(other) {
if (other is Position) {
return other.file == file && other.offset == offset;
}
return false;
}
@override
Map<String, Object> toJson() {
return {
FILE: file,
OFFSET: offset
};
}
@override
String toString() => 'Position(file=$file, offset=$offset)';
}