733 lines
24 KiB
Dart
733 lines
24 KiB
Dart
import 'dart:collection';
|
|
import 'dart:io';
|
|
import 'dart:async';
|
|
import 'dart:isolate';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
|
import 'package:subwrite/annotation.dart';
|
|
|
|
import 'package:subwrite/section.dart';
|
|
import 'package:subwrite/sourcelink.dart';
|
|
import 'package:subwrite/spellcheck/spellchecker.dart';
|
|
import 'package:subwrite/view.dart';
|
|
import 'cursor_provider.dart';
|
|
import 'line_info.dart';
|
|
|
|
class LineFile extends ChangeNotifier {
|
|
List<LineInfo> backingLines = List.empty(growable: true);
|
|
|
|
CursorProvider cursor = CursorProvider();
|
|
FocusNode focus = FocusNode();
|
|
late String path;
|
|
|
|
ItemScrollController scrollController = ItemScrollController();
|
|
|
|
var status = "";
|
|
|
|
var showSuggestions = false;
|
|
|
|
SpellChecker? spellchecker;
|
|
|
|
buildDictionary() async {
|
|
ReceivePort receivePort = ReceivePort();
|
|
|
|
receivePort.listen((dynamic message) {
|
|
updateStatus(message as double);
|
|
});
|
|
|
|
status = "loading dictionary";
|
|
notifyListeners();
|
|
|
|
compute(SpellChecker.load, DictionaryLoadRequest("dicts/english.txt", receivePort.sendPort)).then(
|
|
(value) {
|
|
spellchecker = value;
|
|
status = "dictionary loaded";
|
|
|
|
status = "spellchecking document";
|
|
updateStatus(0.0);
|
|
notifyListeners();
|
|
spellCheckAll().then((value) {
|
|
status = "spellchecking complete";
|
|
updateStatus(1.0);
|
|
notifyListeners();
|
|
});
|
|
notifyListeners();
|
|
},
|
|
);
|
|
}
|
|
|
|
void updateStatus(double update) {
|
|
statusProgress = update;
|
|
print("New Progress Status...$statusProgress");
|
|
notifyListeners();
|
|
}
|
|
|
|
double statusProgress = 0.0;
|
|
|
|
void cancelSuggestions() {
|
|
suggestionIndex = 0;
|
|
suggestLine = 0;
|
|
suggestCol = 0;
|
|
showSuggestions = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
int suggestLine = 0;
|
|
int suggestCol = 0;
|
|
|
|
void showSuggestionsFor(int line, int col) {
|
|
if (showSuggestions == false) {
|
|
if (line != suggestLine || col != suggestCol) {
|
|
suggestLine = line;
|
|
suggestCol = col;
|
|
showSuggestions = true;
|
|
loadSuggestions();
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
ScrollController suggestionListController = ScrollController(initialScrollOffset: 0.0);
|
|
int suggestionIndex = 0;
|
|
List<Suggestion> suggestions = List.empty();
|
|
|
|
void suggestListUp() {
|
|
suggestionIndex = (suggestionIndex - 1) % suggestions.length;
|
|
if (suggestions.length - suggestionIndex >= 14 || suggestionIndex == 0 || suggestionIndex == suggestions.length - 1) {
|
|
suggestionListController.animateTo(suggestionIndex * 13.0, duration: const Duration(milliseconds: 100), curve: Curves.decelerate);
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
void suggestListDown() {
|
|
suggestionIndex = (suggestionIndex + 1) % suggestions.length;
|
|
if (suggestions.length - suggestionIndex >= 14 || suggestionIndex == 0 || suggestionIndex == suggestions.length - 1) {
|
|
suggestionListController.animateTo(suggestionIndex * 13.0, duration: const Duration(milliseconds: 100), curve: Curves.decelerate);
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
void suggestListInsert() {
|
|
// TODO replace
|
|
}
|
|
|
|
void loadSuggestions() {
|
|
List<Suggestion> suggest = List.empty(growable: true);
|
|
|
|
if (suggestLine > 0) {
|
|
List<LineAnnotation> suggestions = backingLines[suggestLine - 1]
|
|
.annotations
|
|
.where((element) => element.tool == "spellcheck")
|
|
.where((element) => suggestCol >= element.sourceLink.colStart! && suggestCol < element.sourceLink.colEnd!)
|
|
.toList();
|
|
|
|
if (suggestions.isNotEmpty) {
|
|
var words = suggestions.first.description!.split(":");
|
|
for (var i = 0; i < words.length - 1; i++) {
|
|
var spell = words[i];
|
|
if (spell.trim().isNotEmpty) {
|
|
suggest.add(Suggestion("spelling", spell, correctSpelling, suggestions.first.sourceLink));
|
|
}
|
|
}
|
|
suggest.add(Suggestion("addto", words[words.length - 1], customDic, suggestions.first.sourceLink));
|
|
|
|
this.suggestions = suggest;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
void openFile(String path, int startLine) {
|
|
// reset path
|
|
this.path = path;
|
|
// purge backinglines...
|
|
backingLines = List.empty(growable: true);
|
|
status = "Opened $path";
|
|
File f = File(path);
|
|
var line = 0;
|
|
f.readAsLinesSync().forEach((text) {
|
|
backingLines.insert(line, LineInfo(path, line + 1, text.replaceAll("\t", " ").trimRight()));
|
|
line += 1;
|
|
});
|
|
|
|
File notesFile = File("$path.subwrite.notes");
|
|
if (notesFile.existsSync()) {
|
|
notesFile.readAsLinesSync().forEach((notetext) {
|
|
var parts = notetext.split(":");
|
|
var line = int.parse(parts[0]);
|
|
var note = parts.sublist(1).join(":");
|
|
addNoteAtLine(line, note);
|
|
});
|
|
}
|
|
|
|
// handle the empty file case
|
|
if (backingLines.isEmpty) {
|
|
backingLines.insert(0, LineInfo(path, 1, ""));
|
|
}
|
|
parse();
|
|
cursor.moveCursorToDocStart();
|
|
focus.requestFocus();
|
|
notifyListeners();
|
|
}
|
|
|
|
KeyEventResult handleKey(RawKeyEvent event) {
|
|
// only process key down events...
|
|
if (event.runtimeType.toString() == 'RawKeyDownEvent') {
|
|
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
|
cursor.moveCursor(-1, 0, backingLines, keepAnchor: event.isShiftPressed);
|
|
cursor.publishCursor(backingLines);
|
|
cancelSuggestions();
|
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
|
cursor.moveCursor(1, 0, backingLines, keepAnchor: event.isShiftPressed);
|
|
cursor.publishCursor(backingLines);
|
|
cancelSuggestions();
|
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
|
if (showSuggestions == false) {
|
|
cursor.moveCursor(0, -1, backingLines, keepAnchor: event.isShiftPressed);
|
|
cursor.publishCursor(backingLines);
|
|
} else {
|
|
suggestListUp();
|
|
}
|
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
|
if (showSuggestions == false) {
|
|
cursor.moveCursor(0, 1, backingLines, keepAnchor: event.isShiftPressed);
|
|
cursor.publishCursor(backingLines);
|
|
} else {
|
|
suggestListDown();
|
|
}
|
|
} else if (event.logicalKey == LogicalKeyboardKey.home) {
|
|
cancelSuggestions();
|
|
cursor.moveCursorToLineStart();
|
|
cursor.clearSelection();
|
|
cursor.publishCursor(backingLines);
|
|
} else if (event.logicalKey == LogicalKeyboardKey.end) {
|
|
cancelSuggestions();
|
|
cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
|
|
cursor.clearSelection();
|
|
cursor.publishCursor(backingLines);
|
|
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
|
if (showSuggestions == false) {
|
|
insertLine();
|
|
cursor.clearSelection();
|
|
cursor.publishCursor(backingLines);
|
|
} else {
|
|
suggestListInsert();
|
|
}
|
|
} else if (event.logicalKey == LogicalKeyboardKey.tab) {
|
|
insertChar(" ");
|
|
cursor.publishCursor(backingLines);
|
|
cancelSuggestions();
|
|
} else if (event.logicalKey == LogicalKeyboardKey.delete) {
|
|
handleDelete();
|
|
cursor.clearSelection();
|
|
cursor.publishCursor(backingLines);
|
|
cancelSuggestions();
|
|
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
|
handleBackspace();
|
|
cursor.clearSelection();
|
|
cursor.publishCursor(backingLines);
|
|
cancelSuggestions();
|
|
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
|
showSuggestions = false;
|
|
notifyListeners();
|
|
} else if (event.logicalKey == LogicalKeyboardKey.f4) {
|
|
status = "spellchecking document";
|
|
notifyListeners();
|
|
updateStatus(0.0);
|
|
|
|
spellCheckAll().then((value) {
|
|
status = "spellcheck complete";
|
|
notifyListeners();
|
|
});
|
|
} else {
|
|
if (event.isControlPressed) {
|
|
if (event.logicalKey == LogicalKeyboardKey.space) {
|
|
notifyListeners();
|
|
}
|
|
if (event.character != null && event.character == 's') {
|
|
saveFile();
|
|
notifyListeners();
|
|
}
|
|
if (event.character != null && event.character == 'c') {
|
|
copy();
|
|
}
|
|
if (event.character != null && event.character == 'v') {
|
|
paste();
|
|
notifyListeners();
|
|
}
|
|
} else {
|
|
if (event.logicalKey == LogicalKeyboardKey.space) {
|
|
insertChar(" ");
|
|
} else if (event.character != null) {
|
|
insertChar(event.character!);
|
|
}
|
|
}
|
|
cursor.publishCursor(backingLines);
|
|
}
|
|
}
|
|
|
|
parse();
|
|
spellcheck(cursor.line - 1).then((value) {
|
|
notifyListeners();
|
|
});
|
|
return KeyEventResult.handled;
|
|
}
|
|
|
|
void saveFile() {
|
|
status = "Saving...";
|
|
notifyListeners();
|
|
String content = '';
|
|
String notes = '';
|
|
for (var l in backingLines) {
|
|
content += '${l.text}\n';
|
|
for (var annot in l.annotations) {
|
|
if (annot.description != null && annot.tool != "spellcheck") {
|
|
notes += '${annot.sourceLink.lineStart}:${annot.description}\n';
|
|
}
|
|
}
|
|
}
|
|
|
|
File("$path.subwrite.notes").writeAsStringSync(notes);
|
|
File(path).writeAsStringSync(content);
|
|
|
|
status = "Saved";
|
|
notifyListeners();
|
|
}
|
|
|
|
void handleDelete() {
|
|
if (cursor.hasSelection()) {
|
|
deleteSelected();
|
|
return;
|
|
}
|
|
|
|
// if at the end of the line then we need to move the next line onto
|
|
// our line, and delete what remains
|
|
if (cursor.column == backingLines[cursor.line - 1].text.length) {
|
|
backingLines[cursor.line - 1].text += backingLines[cursor.line].text;
|
|
backingLines[cursor.line - 1].notifyListeners();
|
|
backingLines.removeAt(cursor.line);
|
|
renumberFrom(cursor.line);
|
|
} else {
|
|
// otherwise simply delete a char
|
|
var beforeDeletedChar = backingLines[cursor.line - 1].text.substring(0, cursor.column);
|
|
var afterDeletedChar = backingLines[cursor.line - 1].text.substring(cursor.column + 1);
|
|
backingLines[cursor.line - 1].text = beforeDeletedChar + afterDeletedChar;
|
|
backingLines[cursor.line - 1].notifyListeners();
|
|
}
|
|
}
|
|
|
|
void deleteSelected() {
|
|
var ncursor = cursor.normalized();
|
|
if (ncursor.hasSelection()) {
|
|
if (ncursor.anchorLine == ncursor.line) {
|
|
// simple case, just trim column
|
|
var beforeDeletedChar = backingLines[ncursor.line - 1].text.substring(0, ncursor.column);
|
|
var afterDeletedChar = backingLines[ncursor.line - 1].text.substring(ncursor.anchorColumn);
|
|
backingLines[ncursor.line - 1].text = beforeDeletedChar + afterDeletedChar;
|
|
backingLines[ncursor.line - 1].notifyListeners();
|
|
cursor.clearSelection();
|
|
} else {
|
|
// First Line
|
|
var withDeletedRemainder = backingLines[ncursor.line - 1].text.substring(0, ncursor.column);
|
|
var withDeletedPrefix = backingLines[ncursor.anchorLine - 1].text.substring(ncursor.anchorColumn);
|
|
backingLines[ncursor.line - 1].text = withDeletedRemainder + withDeletedPrefix;
|
|
|
|
// Delete 2nd to n-1 Lines
|
|
for (var line = (ncursor.anchorLine - 1); line >= ncursor.line; line -= 1) {
|
|
backingLines.removeAt(line);
|
|
}
|
|
renumberFrom(ncursor.line);
|
|
cursor.clearSelection();
|
|
}
|
|
}
|
|
cursor.clearSelection();
|
|
}
|
|
|
|
void handleBackspace() {
|
|
if (cursor.hasSelection()) {
|
|
deleteSelected();
|
|
return;
|
|
}
|
|
|
|
if (cursor.column > 0) {
|
|
var beforeDeletedChar = backingLines[cursor.line - 1].text.substring(0, cursor.column - 1);
|
|
var afterDeletedChar = backingLines[cursor.line - 1].text.substring(cursor.column);
|
|
backingLines[cursor.line - 1].text = beforeDeletedChar + afterDeletedChar;
|
|
backingLines[cursor.line - 1].notifyListeners();
|
|
cursor.moveCursor(-1, 0, backingLines);
|
|
} else if (cursor.line > 1) {
|
|
// join current line to previous line
|
|
cursor.moveCursor(0, -1, backingLines);
|
|
cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
|
|
backingLines[cursor.line - 1].text += backingLines[cursor.line].text;
|
|
backingLines[cursor.line - 1].notifyListeners();
|
|
// delete current line
|
|
backingLines.removeAt(cursor.line);
|
|
renumberFrom(cursor.line);
|
|
// renumber
|
|
}
|
|
}
|
|
|
|
void insertChar(String ch) {
|
|
status = "Unsaved Changes";
|
|
notifyListeners();
|
|
|
|
var beforeInsertedChar = backingLines[cursor.line - 1].text.substring(0, cursor.column);
|
|
var afterInsertedChar = backingLines[cursor.line - 1].text.substring(cursor.column);
|
|
backingLines[cursor.line - 1].text = beforeInsertedChar + ch + afterInsertedChar;
|
|
cursor.moveCursor(ch.length, 0, backingLines);
|
|
backingLines[cursor.line - 1].notify();
|
|
}
|
|
|
|
void insertLine() {
|
|
status = "Unsaved Changes";
|
|
notifyListeners();
|
|
|
|
// get the content that will be moved to the next line
|
|
var restOfLine = backingLines[cursor.line - 1].text.substring(cursor.column);
|
|
// split off the line after the cursor
|
|
backingLines[cursor.line - 1].text = backingLines[cursor.line - 1].text.substring(0, cursor.column);
|
|
// insert a line directly into this position
|
|
backingLines.insert(cursor.line, LineInfo(path, cursor.line + 1, restOfLine));
|
|
// Move the Cursor Down
|
|
cursor.moveCursor(0, 1, backingLines);
|
|
// renumber all the lines afterwards
|
|
renumberFrom(cursor.line - 1);
|
|
cursor.moveCursorToLineStart();
|
|
}
|
|
|
|
void renumberFrom(int startLine) {
|
|
// Catch many unsaved changes here...
|
|
status = "Unsaved Changes";
|
|
notifyListeners();
|
|
|
|
for (int line = startLine; line < backingLines.length; line++) {
|
|
backingLines[line].lineNumber = line + 1;
|
|
backingLines[line].notifyListeners();
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
int lines() {
|
|
return backingLines.length;
|
|
}
|
|
|
|
final _boldRegex = RegExp(
|
|
r'(\*\*([^*]*)\*\*)',
|
|
caseSensitive: false,
|
|
dotAll: true,
|
|
);
|
|
|
|
final _codeRegex = RegExp(
|
|
r'(\`([^`]*)\`)',
|
|
caseSensitive: false,
|
|
dotAll: true,
|
|
);
|
|
|
|
final _imageRegex = RegExp(
|
|
r'!\[([^\[]*)\]\((.*)\)',
|
|
caseSensitive: false,
|
|
dotAll: true,
|
|
);
|
|
|
|
List<Section> sections = List.empty(growable: true);
|
|
|
|
Future<void> spellcheck(int i) async {
|
|
var line = backingLines[i];
|
|
line.annotations.removeWhere((element) => element.tool == "spellcheck");
|
|
|
|
if (spellchecker == null) {
|
|
return;
|
|
}
|
|
|
|
List<LineAnnotation> annotations = List.empty(growable: true);
|
|
var wordStart = 0;
|
|
var wordEnd = 0;
|
|
var lowerCase = line.text.toLowerCase();
|
|
for (var c = 0; c < lowerCase.length; c++) {
|
|
if (lowerCase.substring(c, c + 1) == " ") {
|
|
wordEnd = c;
|
|
var annotationStart = wordStart;
|
|
var word = lowerCase.substring(wordStart, wordEnd);
|
|
wordStart = c + 1;
|
|
|
|
if (word.endsWith(".")) {
|
|
word = word.substring(0, word.length - 1);
|
|
}
|
|
|
|
if (word.endsWith(":")) {
|
|
word = word.substring(0, word.length - 1);
|
|
}
|
|
|
|
if (word.endsWith(";")) {
|
|
word = word.substring(0, word.length - 1);
|
|
}
|
|
|
|
if (word.endsWith(",")) {
|
|
word = word.substring(0, word.length - 1);
|
|
}
|
|
|
|
// clean up quotes
|
|
word = word.replaceAll("\"", "");
|
|
word = word.replaceAll("'", "");
|
|
word = word.replaceAll("`", "");
|
|
word = word.replaceAll("(", "");
|
|
word = word.replaceAll(")", "");
|
|
word = word.replaceAll("[", "");
|
|
word = word.replaceAll("]", "");
|
|
word = word.replaceAll("^", "");
|
|
word = word.replaceAll("*", "");
|
|
word = word.replaceAll("?", "");
|
|
word = word.replaceAll("!", "");
|
|
word = word.replaceAll("#", "");
|
|
word = word.replaceAll("*", "");
|
|
|
|
// ignore numbers
|
|
if (word.contains(RegExp(r'[0-9]'))) {
|
|
continue;
|
|
}
|
|
|
|
// ignore code...
|
|
if (word.contains("/") | word.contains("=") | word.contains(">") || word.contains("<") || word.contains("@") || word.contains("`") || word.contains(";")) {
|
|
continue;
|
|
}
|
|
|
|
if (word.length < 2 || word.length > 10) {
|
|
continue;
|
|
}
|
|
|
|
HashSet<String>? candidates = spellchecker!.candidates(word);
|
|
if (candidates == null) {
|
|
await compute(mutations2, word).then((results) {
|
|
spellchecker!.dictionary.putIfAbsent(word, () => HashSet());
|
|
results.forEach((element) {
|
|
if (spellchecker!.known.contains(element)) {
|
|
spellchecker!.dictionary[word]!.add(element);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Candidates should now not be null...
|
|
candidates = spellchecker!.candidates(word);
|
|
if (candidates!.contains(word) == false) {
|
|
annotations.add(LineAnnotation(SourceLink("", lineStart: 0, colStart: annotationStart, colEnd: wordEnd + 1, lineEnd: 0), "spellcheck", AnnotationType.highlight, "spellcheck",
|
|
description: "${candidates.join(":")}:$word"));
|
|
}
|
|
}
|
|
|
|
for (var annot in annotations) {
|
|
annot.sourceLink = SourceLink(path, lineStart: i + 1, lineEnd: i + 1, colStart: annot.sourceLink.colStart, colEnd: annot.sourceLink.colEnd);
|
|
backingLines[i].annotations.add(annot);
|
|
}
|
|
notifyListeners();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// rebuild the annotations manager
|
|
void parse() {
|
|
sections.clear();
|
|
|
|
for (var i = 0; i < backingLines.length; i++) {
|
|
var line = backingLines[i];
|
|
|
|
backingLines[i].annotations.removeWhere((element) => element.tool == "markdown");
|
|
|
|
if (line.text.startsWith("//")) {
|
|
backingLines[i]
|
|
.annotations
|
|
.add(LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.highlight, "comment"));
|
|
|
|
if (line.text.contains("TODO")) {
|
|
backingLines[i]
|
|
.annotations
|
|
.add(LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.todo, "todo"));
|
|
}
|
|
}
|
|
|
|
if (line.text.startsWith("#")) {
|
|
// this is a header
|
|
backingLines[i]
|
|
.annotations
|
|
.add(LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.highlight, "header"));
|
|
|
|
var level = 0;
|
|
for (var lvl = 0; lvl < 6; lvl++) {
|
|
if (line.text.length > lvl) {
|
|
if (line.text[lvl] == '#') {
|
|
level += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
sections.add(Section(backingLines[i].lineNumber, level, line.text.substring(level + 1)));
|
|
}
|
|
|
|
if (line.text.startsWith(" ")) {
|
|
backingLines[i]
|
|
.annotations
|
|
.add(LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.highlight, "code"));
|
|
}
|
|
|
|
var imageMatch = _imageRegex.matchAsPrefix(line.text);
|
|
if (imageMatch != null) {
|
|
backingLines[i].annotations.add(LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, lineEnd: i), "markdown", AnnotationType.image, imageMatch.group(2)!));
|
|
}
|
|
|
|
_boldRegex.allMatches(backingLines[i].text).forEach((match) {
|
|
backingLines[i]
|
|
.annotations
|
|
.add(LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: match.start, colEnd: match.end + 1, lineEnd: i), "markdown", AnnotationType.highlight, "bold"));
|
|
});
|
|
|
|
_codeRegex.allMatches(backingLines[i].text).forEach((match) {
|
|
backingLines[i]
|
|
.annotations
|
|
.add(LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: match.start, colEnd: match.end + 1, lineEnd: i), "markdown", AnnotationType.highlight, "code"));
|
|
});
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
void copy() {
|
|
var ncursor = cursor.normalized();
|
|
var firstLine = "${backingLines[ncursor.line - 1].text.substring(ncursor.column)}\n";
|
|
for (var line = ncursor.line + 1; line < ncursor.anchorLine; line++) {
|
|
firstLine += "${backingLines[line - 1].text}\n";
|
|
}
|
|
var textToCopy = firstLine;
|
|
if (ncursor.anchorLine != ncursor.line) {
|
|
var lastLine = backingLines[ncursor.anchorLine - 1].text.substring(0, ncursor.anchorColumn);
|
|
textToCopy += lastLine;
|
|
}
|
|
Clipboard.setData(ClipboardData(text: textToCopy));
|
|
}
|
|
|
|
void paste() {
|
|
deleteSelected(); // if we have a selection we need to delete it before pasting new text
|
|
Clipboard.getData("text/plain").then((value) {
|
|
if (value != null && value.text != null) {
|
|
for (var i = 0; i < value.text!.length; i++) {
|
|
var char = value.text![i];
|
|
if (char == "\n") {
|
|
insertLine();
|
|
} else {
|
|
insertChar(char);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void mouseDownUpdate(int line, int col) {
|
|
focus.requestFocus();
|
|
showSuggestions = false;
|
|
cursor.mouseDownUpdate(line, col, backingLines);
|
|
notifyListeners();
|
|
}
|
|
|
|
void mouseDownMoveUpdate(int col) {
|
|
cursor.mouseDownMoveUpdate(col, backingLines);
|
|
}
|
|
|
|
void mouseDownEnd() {
|
|
cursor.mouseDownEnd();
|
|
}
|
|
|
|
void moveLines(int startLine, int endLine, int newLine) {
|
|
List<LineInfo> insert = List.empty(growable: true);
|
|
|
|
// backup the section
|
|
for (int line = startLine; line < endLine; line++) {
|
|
insert.add(backingLines[line - 1]);
|
|
}
|
|
|
|
// remove the section
|
|
for (int line = startLine; line < endLine; line++) {
|
|
backingLines.removeAt(startLine - 1);
|
|
}
|
|
|
|
// if we want to put a line that was on e.g. 5 on 10 then we have to remove 5...
|
|
// (unlike if it was on 10 and we want to put it on 5...it hasn't changed)
|
|
if (newLine > startLine) {
|
|
newLine -= endLine - startLine;
|
|
}
|
|
|
|
// reinsert
|
|
for (int line = 0; line < insert.length; line++) {
|
|
backingLines.insert(newLine - 1 + line, insert[line]);
|
|
}
|
|
|
|
renumberFrom(0);
|
|
parse();
|
|
notifyListeners();
|
|
}
|
|
|
|
addNote(String note) {
|
|
addNoteAtLine(cursor.line, note);
|
|
}
|
|
|
|
addNoteAtLine(int line, String note) {
|
|
var notetype = "note";
|
|
if (note.startsWith("[incomplete]")) {
|
|
notetype = "incomplete";
|
|
} else if (note.startsWith("[edit]")) {
|
|
notetype = "edit";
|
|
}
|
|
|
|
backingLines[line - 1].annotations.add(LineAnnotation(
|
|
SourceLink(
|
|
path,
|
|
lineStart: line,
|
|
),
|
|
"notes",
|
|
AnnotationType.note,
|
|
notetype,
|
|
description: note));
|
|
notifyListeners();
|
|
}
|
|
|
|
int wordCount() {
|
|
return backingLines.map((e) => e.text.trim().split(" ").length).fold(0, (previousValue, element) => previousValue + element);
|
|
}
|
|
|
|
void setSuggestIndex(int index) {
|
|
suggestionIndex = index;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<bool> spellCheckAll() async {
|
|
for (int i = 0; i < backingLines.length; i++) {
|
|
await spellcheck(i);
|
|
updateStatus(i.toDouble() / backingLines.length.toDouble());
|
|
}
|
|
return Future.value(true);
|
|
}
|
|
|
|
customDic(BuildContext context, Suggestion suggestion) {
|
|
spellchecker!.addToCustomDictionary(suggestion.name);
|
|
int line = suggestion.link.lineStart! - 1;
|
|
spellcheck(line);
|
|
}
|
|
|
|
correctSpelling(BuildContext context, Suggestion suggestion) {
|
|
int start = suggestion.link.colStart!;
|
|
int end = suggestion.link.colEnd!;
|
|
int line = suggestion.link.lineStart! - 1;
|
|
backingLines[line].text = backingLines[line].text.replaceRange(start, end - 1, suggestion.name);
|
|
backingLines[line].notifyListeners();
|
|
notifyListeners();
|
|
spellcheck(line);
|
|
}
|
|
}
|
|
|
|
class SpellcheckerRequest {
|
|
final String text;
|
|
SpellcheckerRequest(this.text);
|
|
}
|