Spellchecking + Logo
This commit is contained in:
parent
96e4f0e32c
commit
27430fa5f2
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
File diff suppressed because it is too large
Load Diff
221
lib/file.dart
221
lib/file.dart
|
@ -1,6 +1,9 @@
|
||||||
|
import 'dart:collection';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
@ -8,6 +11,8 @@ import 'package:subwrite/annotation.dart';
|
||||||
|
|
||||||
import 'package:subwrite/section.dart';
|
import 'package:subwrite/section.dart';
|
||||||
import 'package:subwrite/sourcelink.dart';
|
import 'package:subwrite/sourcelink.dart';
|
||||||
|
import 'package:subwrite/spellcheck/spellchecker.dart';
|
||||||
|
import 'package:subwrite/view.dart';
|
||||||
import 'cursor_provider.dart';
|
import 'cursor_provider.dart';
|
||||||
import 'line_info.dart';
|
import 'line_info.dart';
|
||||||
|
|
||||||
|
@ -16,16 +21,93 @@ class LineFile extends ChangeNotifier {
|
||||||
|
|
||||||
CursorProvider cursor = CursorProvider();
|
CursorProvider cursor = CursorProvider();
|
||||||
FocusNode focus = FocusNode();
|
FocusNode focus = FocusNode();
|
||||||
bool showSuggestions = false;
|
|
||||||
late String projectPath;
|
|
||||||
late String path;
|
late String path;
|
||||||
|
|
||||||
ItemScrollController scrollController = ItemScrollController();
|
ItemScrollController scrollController = ItemScrollController();
|
||||||
|
|
||||||
var status = "";
|
var status = "";
|
||||||
|
|
||||||
|
var showSuggestions = false;
|
||||||
|
|
||||||
|
final SpellChecker spellchecker = SpellChecker.load("dicts/english.txt");
|
||||||
|
|
||||||
|
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) {
|
||||||
|
backingLines[suggestLine - 1].annotations.forEach((element) {
|
||||||
|
print("${element.description} ${element.tool} $suggestCol ${element.sourceLink.colStart} ${element.sourceLink.colEnd}");
|
||||||
|
});
|
||||||
|
List<String> suggestions = backingLines[suggestLine - 1]
|
||||||
|
.annotations
|
||||||
|
.where((element) => element.tool == "spellcheck")
|
||||||
|
.where((element) => suggestCol >= element.sourceLink.colStart! && suggestCol < element.sourceLink.colEnd!)
|
||||||
|
.map((e) => e.description!)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (suggestions.isNotEmpty) {
|
||||||
|
for (var spell in suggestions.first.split(":")) {
|
||||||
|
suggest.add(Suggestion("spelling", spell));
|
||||||
|
}
|
||||||
|
this.suggestions = suggest;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void openFile(String path, int startLine) {
|
void openFile(String path, int startLine) {
|
||||||
|
// reset path
|
||||||
this.path = path;
|
this.path = path;
|
||||||
|
// purge backinglines...
|
||||||
|
backingLines = List.empty(growable: true);
|
||||||
|
status = "Opened $path";
|
||||||
File f = File(path);
|
File f = File(path);
|
||||||
var line = 0;
|
var line = 0;
|
||||||
f.readAsLinesSync().forEach((text) {
|
f.readAsLinesSync().forEach((text) {
|
||||||
|
@ -59,45 +141,67 @@ class LineFile extends ChangeNotifier {
|
||||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||||
cursor.moveCursor(-1, 0, backingLines, keepAnchor: event.isShiftPressed);
|
cursor.moveCursor(-1, 0, backingLines, keepAnchor: event.isShiftPressed);
|
||||||
cursor.publishCursor(backingLines);
|
cursor.publishCursor(backingLines);
|
||||||
|
cancelSuggestions();
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||||
cursor.moveCursor(1, 0, backingLines, keepAnchor: event.isShiftPressed);
|
cursor.moveCursor(1, 0, backingLines, keepAnchor: event.isShiftPressed);
|
||||||
cursor.publishCursor(backingLines);
|
cursor.publishCursor(backingLines);
|
||||||
|
cancelSuggestions();
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||||
|
if (showSuggestions == false) {
|
||||||
cursor.moveCursor(0, -1, backingLines, keepAnchor: event.isShiftPressed);
|
cursor.moveCursor(0, -1, backingLines, keepAnchor: event.isShiftPressed);
|
||||||
cursor.publishCursor(backingLines);
|
cursor.publishCursor(backingLines);
|
||||||
|
} else {
|
||||||
|
suggestListUp();
|
||||||
|
}
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||||
|
if (showSuggestions == false) {
|
||||||
cursor.moveCursor(0, 1, backingLines, keepAnchor: event.isShiftPressed);
|
cursor.moveCursor(0, 1, backingLines, keepAnchor: event.isShiftPressed);
|
||||||
cursor.publishCursor(backingLines);
|
cursor.publishCursor(backingLines);
|
||||||
|
} else {
|
||||||
|
suggestListDown();
|
||||||
|
}
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.home) {
|
} else if (event.logicalKey == LogicalKeyboardKey.home) {
|
||||||
|
cancelSuggestions();
|
||||||
cursor.moveCursorToLineStart();
|
cursor.moveCursorToLineStart();
|
||||||
cursor.clearSelection();
|
cursor.clearSelection();
|
||||||
cursor.publishCursor(backingLines);
|
cursor.publishCursor(backingLines);
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.end) {
|
} else if (event.logicalKey == LogicalKeyboardKey.end) {
|
||||||
|
cancelSuggestions();
|
||||||
cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
|
cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
|
||||||
cursor.clearSelection();
|
cursor.clearSelection();
|
||||||
cursor.publishCursor(backingLines);
|
cursor.publishCursor(backingLines);
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||||
|
if (showSuggestions == false) {
|
||||||
insertLine();
|
insertLine();
|
||||||
cursor.clearSelection();
|
cursor.clearSelection();
|
||||||
cursor.publishCursor(backingLines);
|
cursor.publishCursor(backingLines);
|
||||||
|
} else {
|
||||||
|
suggestListInsert();
|
||||||
|
}
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.tab) {
|
} else if (event.logicalKey == LogicalKeyboardKey.tab) {
|
||||||
insertChar(" ");
|
insertChar(" ");
|
||||||
cursor.publishCursor(backingLines);
|
cursor.publishCursor(backingLines);
|
||||||
|
cancelSuggestions();
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.delete) {
|
} else if (event.logicalKey == LogicalKeyboardKey.delete) {
|
||||||
handleDelete();
|
handleDelete();
|
||||||
cursor.clearSelection();
|
cursor.clearSelection();
|
||||||
cursor.publishCursor(backingLines);
|
cursor.publishCursor(backingLines);
|
||||||
|
cancelSuggestions();
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||||
handleBackspace();
|
handleBackspace();
|
||||||
cursor.clearSelection();
|
cursor.clearSelection();
|
||||||
cursor.publishCursor(backingLines);
|
cursor.publishCursor(backingLines);
|
||||||
|
cancelSuggestions();
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
showSuggestions = false;
|
showSuggestions = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
} else if (event.logicalKey == LogicalKeyboardKey.f4) {
|
||||||
|
spellCheckAll().then((value) {
|
||||||
|
status = "spellcheck complete";
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if (event.isControlPressed) {
|
if (event.isControlPressed) {
|
||||||
if (event.logicalKey == LogicalKeyboardKey.space) {
|
if (event.logicalKey == LogicalKeyboardKey.space) {
|
||||||
showSuggestions = true;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
if (event.character != null && event.character == 's') {
|
if (event.character != null && event.character == 's') {
|
||||||
|
@ -133,7 +237,7 @@ class LineFile extends ChangeNotifier {
|
||||||
for (var l in backingLines) {
|
for (var l in backingLines) {
|
||||||
content += '${l.text}\n';
|
content += '${l.text}\n';
|
||||||
for (var annot in l.annotations) {
|
for (var annot in l.annotations) {
|
||||||
if (annot.description != null) {
|
if (annot.description != null && annot.tool != "spellcheck") {
|
||||||
notes += '${annot.sourceLink.lineStart}:${annot.description}\n';
|
notes += '${annot.sourceLink.lineStart}:${annot.description}\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -278,6 +382,94 @@ class LineFile extends ChangeNotifier {
|
||||||
|
|
||||||
List<Section> sections = List.empty(growable: 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");
|
||||||
|
|
||||||
|
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("!", "");
|
||||||
|
|
||||||
|
// 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(":")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var annot in annotations) {
|
||||||
|
annot.sourceLink = SourceLink(path, lineStart: i + 1, lineEnd: i + 1, colStart: annot.sourceLink.colStart, colEnd: annot.sourceLink.colEnd);
|
||||||
|
line.annotations.add(annot);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// rebuild the annotations manager
|
// rebuild the annotations manager
|
||||||
void parse() {
|
void parse() {
|
||||||
sections.clear();
|
sections.clear();
|
||||||
|
@ -372,7 +564,9 @@ class LineFile extends ChangeNotifier {
|
||||||
|
|
||||||
void mouseDownUpdate(int line, int col) {
|
void mouseDownUpdate(int line, int col) {
|
||||||
focus.requestFocus();
|
focus.requestFocus();
|
||||||
|
showSuggestions = false;
|
||||||
cursor.mouseDownUpdate(line, col, backingLines);
|
cursor.mouseDownUpdate(line, col, backingLines);
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void mouseDownMoveUpdate(int col) {
|
void mouseDownMoveUpdate(int col) {
|
||||||
|
@ -440,4 +634,21 @@ class LineFile extends ChangeNotifier {
|
||||||
int wordCount() {
|
int wordCount() {
|
||||||
return backingLines.map((e) => e.text.trim().split(" ").length).fold(0, (previousValue, element) => previousValue + element);
|
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);
|
||||||
|
}
|
||||||
|
return Future.value(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpellcheckerRequest {
|
||||||
|
final String text;
|
||||||
|
SpellcheckerRequest(this.text);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:subwrite/theme.dart';
|
||||||
|
import 'package:subwrite/view.dart';
|
||||||
|
import 'file.dart';
|
||||||
|
|
||||||
|
class Ghostsense extends StatefulWidget {
|
||||||
|
List<Suggestion> suggestions;
|
||||||
|
Ghostsense(this.suggestions);
|
||||||
|
@override
|
||||||
|
_Ghostsense createState() => _Ghostsense();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Ghostsense extends State<Ghostsense> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: Provider.of<LineFile>(context).suggestionListController,
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
itemCount: widget.suggestions.length,
|
||||||
|
padding: EdgeInsets.all(2.0),
|
||||||
|
itemBuilder: (BuildContext bcontext, int index) {
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: (event) {
|
||||||
|
// TODO do insert...
|
||||||
|
},
|
||||||
|
onPointerHover: (event) {
|
||||||
|
Provider.of<LineFile>(bcontext).setSuggestIndex(index);
|
||||||
|
},
|
||||||
|
child: Container(color: Provider.of<LineFile>(context).suggestionIndex == index ? sidebarHighlight : sidebar, child: widget.suggestions[index].getWidget()));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,9 @@ class Highlighter {
|
||||||
case "code":
|
case "code":
|
||||||
d.color = function;
|
d.color = function;
|
||||||
break;
|
break;
|
||||||
|
case "spellcheck":
|
||||||
|
d.color = variable;
|
||||||
|
d.underline = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
decors.add(d);
|
decors.add(d);
|
||||||
|
@ -72,6 +75,9 @@ class Highlighter {
|
||||||
if (d.bold = true) {
|
if (d.bold = true) {
|
||||||
style = style.copyWith(fontWeight: FontWeight.bold);
|
style = style.copyWith(fontWeight: FontWeight.bold);
|
||||||
}
|
}
|
||||||
|
if (d.underline = true) {
|
||||||
|
style = style.copyWith(fontStyle: FontStyle.italic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:subwrite/theme.dart';
|
import 'package:subwrite/theme.dart';
|
||||||
|
@ -37,7 +38,6 @@ class LineInfo extends ChangeNotifier {
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
|
|
||||||
var highlightedLine = RichText(
|
var highlightedLine = RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
children: spans,
|
children: spans,
|
||||||
|
@ -88,12 +88,13 @@ class LineInfo extends ChangeNotifier {
|
||||||
var cursorPosTop = ((cursorStart! / maxCharsBeforeWrapping).floorToDouble() * lineHeight).roundToDouble();
|
var cursorPosTop = ((cursorStart! / maxCharsBeforeWrapping).floorToDouble() * lineHeight).roundToDouble();
|
||||||
var cursorPosLeft = max(0, (((cursorStart! % maxCharsBeforeWrapping) * charWidth) - charWidth / 2.0).roundToDouble());
|
var cursorPosLeft = max(0, (((cursorStart! % maxCharsBeforeWrapping) * charWidth) - charWidth / 2.0).roundToDouble());
|
||||||
TextStyle cursorStyle = TextStyle(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, backgroundColor: Colors.transparent, letterSpacing: -2.0);
|
TextStyle cursorStyle = TextStyle(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, backgroundColor: Colors.transparent, letterSpacing: -2.0);
|
||||||
var cursorPos = Positioned(top: cursorPosTop.toDouble(), left: leftMargin + cursorPosLeft.toDouble(), child: FadeTransition(opacity: animationController, child: Text("|", style: cursorStyle)));
|
var cursorPos =
|
||||||
|
Positioned(top: cursorPosTop.toDouble(), left: leftMargin + cursorPosLeft.toDouble(), child: FadeTransition(opacity: animationController, child: Text("|", style: cursorStyle)));
|
||||||
stackElements.add(cursorPos);
|
stackElements.add(cursorPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
var cursorOverlay = Stack(children: stackElements);
|
var cursorOverlay = Stack(children: stackElements);
|
||||||
print("rebuilding line $lineNumber $idealHeight");
|
//print("rebuilding line $lineNumber $idealHeight");
|
||||||
// print("$charWidth $lineHeight $usableSize $idealHeight ${constraints.maxWidth}");
|
// print("$charWidth $lineHeight $usableSize $idealHeight ${constraints.maxWidth}");
|
||||||
|
|
||||||
return Listener(
|
return Listener(
|
||||||
|
@ -104,6 +105,24 @@ class LineInfo extends ChangeNotifier {
|
||||||
if (event.down && !inMargin) {
|
if (event.down && !inMargin) {
|
||||||
file.mouseDownUpdate(lineNumber, c.round());
|
file.mouseDownUpdate(lineNumber, c.round());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var set = false;
|
||||||
|
|
||||||
|
if (event.buttons == kSecondaryMouseButton) {
|
||||||
|
for (var annot in annotations) {
|
||||||
|
if (annot.tool == "spellcheck") {
|
||||||
|
if (annot.sourceLink.colStart != null && annot.sourceLink.colEnd != null) {
|
||||||
|
if (annot.sourceLink.colStart! <= c && annot.sourceLink.colEnd! > c) {
|
||||||
|
set = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (set) {
|
||||||
|
Provider.of<LineFile>(context).showSuggestionsFor(lineNumber, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onPointerMove: (event) {
|
onPointerMove: (event) {
|
||||||
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
|
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
|
||||||
|
@ -129,6 +148,7 @@ class LineInfo extends ChangeNotifier {
|
||||||
onHover: (event) {
|
onHover: (event) {
|
||||||
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
|
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
|
||||||
var file = Provider.of<LineFile>(context, listen: false);
|
var file = Provider.of<LineFile>(context, listen: false);
|
||||||
|
|
||||||
if (event.down) {
|
if (event.down) {
|
||||||
// note should never happen because flutter only fires hover events when the mouse it up...
|
// note should never happen because flutter only fires hover events when the mouse it up...
|
||||||
file.mouseDownUpdate(lineNumber, c.round());
|
file.mouseDownUpdate(lineNumber, c.round());
|
||||||
|
@ -136,8 +156,7 @@ class LineInfo extends ChangeNotifier {
|
||||||
file.mouseDownEnd();
|
file.mouseDownEnd();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onExit: (event) {
|
onExit: (event) {},
|
||||||
},
|
|
||||||
child: Container(
|
child: Container(
|
||||||
color: backgroundStyle.backgroundColor,
|
color: backgroundStyle.backgroundColor,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:subwrite/file.dart';
|
import 'package:subwrite/file.dart';
|
||||||
import 'package:subwrite/theme.dart';
|
import 'package:subwrite/theme.dart';
|
||||||
|
import 'package:subwrite/view.dart';
|
||||||
|
|
||||||
class Outline extends StatefulWidget {
|
class Outline extends StatefulWidget {
|
||||||
const Outline({super.key});
|
const Outline({super.key});
|
||||||
|
@ -11,22 +14,59 @@ class Outline extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Outline extends State<Outline> {
|
class _Outline extends State<Outline> {
|
||||||
|
final ScrollController controller = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var sections = Provider.of<LineFile>(context).sections;
|
var sections = Provider.of<LineFile>(context).sections;
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: tabs,
|
color: tabs,
|
||||||
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
|
return Scrollbar(
|
||||||
|
controller: controller,
|
||||||
|
thumbVisibility: false,
|
||||||
|
trackVisibility: false,
|
||||||
child: ReorderableListView.builder(
|
child: ReorderableListView.builder(
|
||||||
|
scrollController: controller,
|
||||||
|
physics: AlwaysScrollableScrollPhysics(),
|
||||||
header: Padding(
|
header: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
|
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
|
||||||
child: Column(children: [
|
child: Column(children: [
|
||||||
Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [
|
Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
Icon(
|
// Icon(
|
||||||
Icons.edit,
|
// Icons.edit,
|
||||||
color: foreground,
|
// color: foreground,
|
||||||
|
// ),
|
||||||
|
Image(
|
||||||
|
image: AssetImage("assets/subwrite-logo.png"),
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
width: constraints.maxWidth / 3.0,
|
||||||
|
height: constraints.maxWidth / 3.0,
|
||||||
|
)
|
||||||
|
//Text("subwrite", style: TextStyle(fontSize: 16.0, color: foreground, fontWeight: FontWeight.bold, fontFamily: "Iosevka"))
|
||||||
|
]),
|
||||||
|
SizedBox(
|
||||||
|
height: 20,
|
||||||
),
|
),
|
||||||
Text("subwrite", style: TextStyle(fontSize: 16.0, color: foreground, fontWeight: FontWeight.bold, fontFamily: "Iosevka"))
|
Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
LineFile doc = Provider.of<LineFile>(context);
|
||||||
|
newFile(doc);
|
||||||
|
},
|
||||||
|
child: Text("New", style: TextStyle(fontSize: 10.0, color: foreground, fontWeight: FontWeight.bold, fontFamily: "Iosevka"))),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
LineFile doc = Provider.of<LineFile>(context);
|
||||||
|
openFile(doc);
|
||||||
|
},
|
||||||
|
child: Text("Open", style: TextStyle(fontSize: 10.0, color: foreground, fontWeight: FontWeight.bold, fontFamily: "Iosevka"))),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
exit(0);
|
||||||
|
},
|
||||||
|
child: Text("Exit", style: TextStyle(fontSize: 10.0, color: foreground, fontWeight: FontWeight.bold, fontFamily: "Iosevka"))),
|
||||||
]),
|
]),
|
||||||
Divider(
|
Divider(
|
||||||
color: foreground,
|
color: foreground,
|
||||||
|
@ -75,5 +115,6 @@ class _Outline extends State<Outline> {
|
||||||
},
|
},
|
||||||
itemCount: sections.length,
|
itemCount: sections.length,
|
||||||
));
|
));
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import 'dart:collection';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class SpellChecker {
|
||||||
|
HashMap<String, HashSet<String>> dictionary;
|
||||||
|
HashSet<String> known;
|
||||||
|
|
||||||
|
SpellChecker(this.dictionary, this.known);
|
||||||
|
|
||||||
|
static SpellChecker load(String dictPath) {
|
||||||
|
HashMap<String, HashSet<String>> dictionary = HashMap();
|
||||||
|
HashSet<String> known = HashSet();
|
||||||
|
File dict = File(dictPath);
|
||||||
|
print("building dictionary...");
|
||||||
|
dict.readAsLinesSync().forEach((word) {
|
||||||
|
var lower = word.toLowerCase().trim();
|
||||||
|
dictionary.putIfAbsent(lower, () => HashSet.from([lower]));
|
||||||
|
known.add(lower);
|
||||||
|
|
||||||
|
mutations1(lower).forEach((element) {
|
||||||
|
dictionary.putIfAbsent(element, () => HashSet());
|
||||||
|
dictionary[element]!.add(lower);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
print("builing dictionary...${dictionary.length}");
|
||||||
|
return SpellChecker(dictionary, known);
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<String>? candidates(String word) {
|
||||||
|
return dictionary[word];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<String> mutations1(String word) {
|
||||||
|
// "All edits that are one edit away from `word`."
|
||||||
|
var letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
|
||||||
|
HashSet<String> mutations = HashSet();
|
||||||
|
|
||||||
|
// // delete a letter
|
||||||
|
for (int i = 0; i < word.length; i++) {
|
||||||
|
mutations.add(word.substring(0, i) + word.substring(i + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// transpositions
|
||||||
|
for (int i = 0; i < word.length - 1; i++) {
|
||||||
|
mutations.add(word.substring(0, i) + word.substring(i + 1, i + 2) + word.substring(i, i + 1) + word.substring(i + 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert a letter
|
||||||
|
for (int i = 0; i < word.length; i++) {
|
||||||
|
for (var c = 0; c < 25; c++) {
|
||||||
|
mutations.add(word.substring(0, i) + letters.substring(c, c + 1) + word.substring(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace a letter
|
||||||
|
for (int i = 0; i < word.length; i++) {
|
||||||
|
for (var c = 0; c < 25; c++) {
|
||||||
|
mutations.add(word.substring(0, i) + letters.substring(c, c + 1) + word.substring(i + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mutations;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<String> mutations2(String word) {
|
||||||
|
HashSet<String> mutations = HashSet();
|
||||||
|
for (var e2 in mutations1(word)) {
|
||||||
|
mutations.addAll(mutations1(e2));
|
||||||
|
}
|
||||||
|
return mutations;
|
||||||
|
}
|
112
lib/view.dart
112
lib/view.dart
|
@ -6,6 +6,8 @@ import 'package:provider/provider.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'package:subwrite/theme.dart';
|
import 'package:subwrite/theme.dart';
|
||||||
import 'file.dart';
|
import 'file.dart';
|
||||||
|
import 'ghostsense.dart';
|
||||||
|
import 'highlighter.dart';
|
||||||
import 'line_info.dart';
|
import 'line_info.dart';
|
||||||
|
|
||||||
class View extends StatefulWidget {
|
class View extends StatefulWidget {
|
||||||
|
@ -31,6 +33,9 @@ class _View extends State<View> with SingleTickerProviderStateMixin {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double x = 0.0;
|
||||||
|
double y = 0.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
LineFile doc = Provider.of<LineFile>(context);
|
LineFile doc = Provider.of<LineFile>(context);
|
||||||
|
@ -43,15 +48,7 @@ class _View extends State<View> with SingleTickerProviderStateMixin {
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
String? result = await FilePicker.platform.saveFile();
|
newFile(doc);
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
File(result).createSync(recursive: false);
|
|
||||||
doc.openFile(result, 0);
|
|
||||||
doc.focus.requestFocus();
|
|
||||||
} else {
|
|
||||||
// User canceled the picker
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.create, color: foreground),
|
icon: Icon(Icons.create, color: foreground),
|
||||||
label: const Text("New Document"),
|
label: const Text("New Document"),
|
||||||
|
@ -61,16 +58,7 @@ class _View extends State<View> with SingleTickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
openFile(doc);
|
||||||
allowMultiple: false,
|
|
||||||
type: FileType.custom,
|
|
||||||
allowedExtensions: ['md', 'txt'],
|
|
||||||
);
|
|
||||||
if (result != null) {
|
|
||||||
doc.openFile(result.files.single.path!, 0);
|
|
||||||
} else {
|
|
||||||
// User canceled the picker
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.file_open, color: foreground),
|
icon: Icon(Icons.file_open, color: foreground),
|
||||||
label: const Text("Open Document"),
|
label: const Text("Open Document"),
|
||||||
|
@ -79,8 +67,18 @@ class _View extends State<View> with SingleTickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
return MouseRegion(
|
||||||
return ScrollablePositionedList.builder(
|
onHover: (PointerEvent details) {
|
||||||
|
if (!doc.showSuggestions) {
|
||||||
|
setState(() {
|
||||||
|
x = details.localPosition.dx;
|
||||||
|
y = details.localPosition.dy;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
ScrollablePositionedList.builder(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
itemScrollController: doc.scrollController,
|
itemScrollController: doc.scrollController,
|
||||||
itemCount: doc.lines(),
|
itemCount: doc.lines(),
|
||||||
|
@ -93,7 +91,77 @@ class _View extends State<View> with SingleTickerProviderStateMixin {
|
||||||
builder: (lcontext, lineinfo) {
|
builder: (lcontext, lineinfo) {
|
||||||
return Provider.of<LineInfo>(lcontext).build(context, _animationController);
|
return Provider.of<LineInfo>(lcontext).build(context, _animationController);
|
||||||
});
|
});
|
||||||
});
|
}),
|
||||||
|
Visibility(
|
||||||
|
visible: doc.showSuggestions,
|
||||||
|
maintainInteractivity: false,
|
||||||
|
maintainState: false,
|
||||||
|
child: Positioned(
|
||||||
|
width: 400,
|
||||||
|
height: 200,
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
child: Container(width: 300, decoration: BoxDecoration(color: sidebarAlt, border: Border.all(color: sidebar, width: 1.0)), child: Ghostsense(doc.suggestions))),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> openFile(LineFile doc) async {
|
||||||
|
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||||
|
allowMultiple: false,
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ['md', 'txt'],
|
||||||
|
);
|
||||||
|
if (result != null) {
|
||||||
|
doc.openFile(result.files.single.path!, 0);
|
||||||
|
} else {
|
||||||
|
// User canceled the picker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> newFile(LineFile doc) async {
|
||||||
|
String? result = await FilePicker.platform.saveFile();
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
File(result).createSync(recursive: false);
|
||||||
|
doc.openFile(result, 0);
|
||||||
|
doc.focus.requestFocus();
|
||||||
|
} else {
|
||||||
|
// User canceled the picker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Suggestion {
|
||||||
|
String classType;
|
||||||
|
String name;
|
||||||
|
|
||||||
|
Suggestion(
|
||||||
|
this.classType,
|
||||||
|
this.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is Suggestion && hashCode == other.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => classType.hashCode + name.hashCode;
|
||||||
|
|
||||||
|
Widget getWidget() {
|
||||||
|
var color = function;
|
||||||
|
switch (classType) {
|
||||||
|
case "spelling":
|
||||||
|
color = header;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
|
RichText(text: TextSpan(text: name, style: TextStyle(color: color, fontFamily: "Iosevka", fontSize: 11.0, fontWeight: FontWeight.bold))),
|
||||||
|
RichText(text: TextSpan(text: "dictionary", style: TextStyle(color: foreground, fontFamily: "Iosevka", fontSize: 10.0))),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ static void my_application_activate(GApplication* application) {
|
||||||
|
|
||||||
gtk_window_set_default_size(window, 1280, 720);
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
gtk_widget_show(GTK_WIDGET(window));
|
gtk_widget_show(GTK_WIDGET(window));
|
||||||
|
gtk_window_set_icon_from_file(window, "./subwrite-icon.png", NULL);
|
||||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,8 @@ flutter:
|
||||||
# the material Icons class.
|
# the material Icons class.
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
|
assets:
|
||||||
|
- assets/subwrite-logo.png
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
- family: Iosevka
|
- family: Iosevka
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:subwrite/spellcheck/spellchecker.dart';
|
||||||
|
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Define the test
|
||||||
|
test("test dictionary",(){
|
||||||
|
|
||||||
|
var dict = SpellChecker("dicts/english.txt");
|
||||||
|
|
||||||
|
for (var candidate in dict.candidates("wella")) {
|
||||||
|
print("Candidate: $candidate");
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue