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
235
lib/file.dart
235
lib/file.dart
|
@ -1,6 +1,9 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
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';
|
||||
|
@ -8,6 +11,8 @@ 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';
|
||||
|
||||
|
@ -16,16 +21,93 @@ class LineFile extends ChangeNotifier {
|
|||
|
||||
CursorProvider cursor = CursorProvider();
|
||||
FocusNode focus = FocusNode();
|
||||
bool showSuggestions = false;
|
||||
late String projectPath;
|
||||
late String path;
|
||||
|
||||
ItemScrollController scrollController = ItemScrollController();
|
||||
|
||||
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) {
|
||||
// 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) {
|
||||
|
@ -59,45 +141,67 @@ class LineFile extends ChangeNotifier {
|
|||
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) {
|
||||
cursor.moveCursor(0, -1, backingLines, keepAnchor: event.isShiftPressed);
|
||||
cursor.publishCursor(backingLines);
|
||||
if (showSuggestions == false) {
|
||||
cursor.moveCursor(0, -1, backingLines, keepAnchor: event.isShiftPressed);
|
||||
cursor.publishCursor(backingLines);
|
||||
} else {
|
||||
suggestListUp();
|
||||
}
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
cursor.moveCursor(0, 1, backingLines, keepAnchor: event.isShiftPressed);
|
||||
cursor.publishCursor(backingLines);
|
||||
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) {
|
||||
insertLine();
|
||||
cursor.clearSelection();
|
||||
cursor.publishCursor(backingLines);
|
||||
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) {
|
||||
spellCheckAll().then((value) {
|
||||
status = "spellcheck complete";
|
||||
});
|
||||
} else {
|
||||
if (event.isControlPressed) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.space) {
|
||||
showSuggestions = true;
|
||||
notifyListeners();
|
||||
}
|
||||
if (event.character != null && event.character == 's') {
|
||||
|
@ -133,7 +237,7 @@ class LineFile extends ChangeNotifier {
|
|||
for (var l in backingLines) {
|
||||
content += '${l.text}\n';
|
||||
for (var annot in l.annotations) {
|
||||
if (annot.description != null) {
|
||||
if (annot.description != null && annot.tool != "spellcheck") {
|
||||
notes += '${annot.sourceLink.lineStart}:${annot.description}\n';
|
||||
}
|
||||
}
|
||||
|
@ -278,6 +382,94 @@ class LineFile extends ChangeNotifier {
|
|||
|
||||
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
|
||||
void parse() {
|
||||
sections.clear();
|
||||
|
@ -372,7 +564,9 @@ class LineFile extends ChangeNotifier {
|
|||
|
||||
void mouseDownUpdate(int line, int col) {
|
||||
focus.requestFocus();
|
||||
showSuggestions = false;
|
||||
cursor.mouseDownUpdate(line, col, backingLines);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void mouseDownMoveUpdate(int col) {
|
||||
|
@ -440,4 +634,21 @@ class LineFile extends ChangeNotifier {
|
|||
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);
|
||||
}
|
||||
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":
|
||||
d.color = function;
|
||||
break;
|
||||
case "spellcheck":
|
||||
d.color = variable;
|
||||
d.underline = true;
|
||||
}
|
||||
|
||||
decors.add(d);
|
||||
|
@ -72,6 +75,9 @@ class Highlighter {
|
|||
if (d.bold = true) {
|
||||
style = style.copyWith(fontWeight: FontWeight.bold);
|
||||
}
|
||||
if (d.underline = true) {
|
||||
style = style.copyWith(fontStyle: FontStyle.italic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:subwrite/theme.dart';
|
||||
|
@ -37,7 +38,6 @@ class LineInfo extends ChangeNotifier {
|
|||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
|
||||
var highlightedLine = RichText(
|
||||
text: TextSpan(
|
||||
children: spans,
|
||||
|
@ -88,12 +88,13 @@ class LineInfo extends ChangeNotifier {
|
|||
var cursorPosTop = ((cursorStart! / maxCharsBeforeWrapping).floorToDouble() * lineHeight).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);
|
||||
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);
|
||||
}
|
||||
|
||||
var cursorOverlay = Stack(children: stackElements);
|
||||
print("rebuilding line $lineNumber $idealHeight");
|
||||
//print("rebuilding line $lineNumber $idealHeight");
|
||||
// print("$charWidth $lineHeight $usableSize $idealHeight ${constraints.maxWidth}");
|
||||
|
||||
return Listener(
|
||||
|
@ -104,6 +105,24 @@ class LineInfo extends ChangeNotifier {
|
|||
if (event.down && !inMargin) {
|
||||
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) {
|
||||
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
|
||||
|
@ -129,6 +148,7 @@ class LineInfo extends ChangeNotifier {
|
|||
onHover: (event) {
|
||||
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
|
||||
var file = Provider.of<LineFile>(context, listen: false);
|
||||
|
||||
if (event.down) {
|
||||
// note should never happen because flutter only fires hover events when the mouse it up...
|
||||
file.mouseDownUpdate(lineNumber, c.round());
|
||||
|
@ -136,8 +156,7 @@ class LineInfo extends ChangeNotifier {
|
|||
file.mouseDownEnd();
|
||||
}
|
||||
},
|
||||
onExit: (event) {
|
||||
},
|
||||
onExit: (event) {},
|
||||
child: Container(
|
||||
color: backgroundStyle.backgroundColor,
|
||||
padding: EdgeInsets.zero,
|
||||
|
|
151
lib/outline.dart
151
lib/outline.dart
|
@ -1,7 +1,10 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:subwrite/file.dart';
|
||||
import 'package:subwrite/theme.dart';
|
||||
import 'package:subwrite/view.dart';
|
||||
|
||||
class Outline extends StatefulWidget {
|
||||
const Outline({super.key});
|
||||
|
@ -11,69 +14,107 @@ class Outline extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _Outline extends State<Outline> {
|
||||
final ScrollController controller = ScrollController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var sections = Provider.of<LineFile>(context).sections;
|
||||
|
||||
return Material(
|
||||
color: tabs,
|
||||
child: ReorderableListView.builder(
|
||||
header: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
|
||||
child: Column(children: [
|
||||
Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(
|
||||
Icons.edit,
|
||||
color: foreground,
|
||||
),
|
||||
Text("subwrite", style: TextStyle(fontSize: 16.0, color: foreground, fontWeight: FontWeight.bold, fontFamily: "Iosevka"))
|
||||
]),
|
||||
Divider(
|
||||
color: foreground,
|
||||
)
|
||||
])),
|
||||
footer: Padding(padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 10.0), child: Divider(color: foreground, thickness: 4.0)),
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
var doc = Provider.of<LineFile>(context, listen: false);
|
||||
var startLine = sections[oldIndex].lineNumber;
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return Scrollbar(
|
||||
controller: controller,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
child: ReorderableListView.builder(
|
||||
scrollController: controller,
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
header: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
|
||||
child: Column(children: [
|
||||
Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
// Icon(
|
||||
// Icons.edit,
|
||||
// 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,
|
||||
),
|
||||
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(
|
||||
color: foreground,
|
||||
)
|
||||
])),
|
||||
footer: Padding(padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 10.0), child: Divider(color: foreground, thickness: 4.0)),
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
var doc = Provider.of<LineFile>(context, listen: false);
|
||||
var startLine = sections[oldIndex].lineNumber;
|
||||
|
||||
var endLine = doc.lines();
|
||||
for (var index = oldIndex + 1; index < sections.length; index++) {
|
||||
print("${sections[oldIndex].level} > ${sections[index].level}");
|
||||
if (sections[oldIndex].level < sections[index].level) {
|
||||
// sub section...skipping
|
||||
} else {
|
||||
endLine = sections[index].lineNumber;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var endLine = doc.lines();
|
||||
for (var index = oldIndex + 1; index < sections.length; index++) {
|
||||
print("${sections[oldIndex].level} > ${sections[index].level}");
|
||||
if (sections[oldIndex].level < sections[index].level) {
|
||||
// sub section...skipping
|
||||
} else {
|
||||
endLine = sections[index].lineNumber;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var newLine = newIndex >= sections.length ? doc.lines() : sections[newIndex].lineNumber;
|
||||
var newLine = newIndex >= sections.length ? doc.lines() : sections[newIndex].lineNumber;
|
||||
|
||||
doc.moveLines(startLine, endLine, newLine);
|
||||
},
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ListTile(
|
||||
key: ValueKey(index),
|
||||
tileColor: tabs,
|
||||
title: Text(
|
||||
sections[index].title.padLeft(sections[index].title.length + sections[index].level - 1, ' '),
|
||||
style: TextStyle(color: foreground, fontSize: 14.0 - sections[index].level, fontFamily: "Iosevka"),
|
||||
),
|
||||
onTap: () {
|
||||
Provider.of<LineFile>(context, listen: false).scrollController.jumpTo(index: sections[index].lineNumber - 1);
|
||||
},
|
||||
trailing: ReorderableDragStartListener(
|
||||
index: index,
|
||||
child: Icon(
|
||||
Icons.drag_handle,
|
||||
color: foreground,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: sections.length,
|
||||
));
|
||||
doc.moveLines(startLine, endLine, newLine);
|
||||
},
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ListTile(
|
||||
key: ValueKey(index),
|
||||
tileColor: tabs,
|
||||
title: Text(
|
||||
sections[index].title.padLeft(sections[index].title.length + sections[index].level - 1, ' '),
|
||||
style: TextStyle(color: foreground, fontSize: 14.0 - sections[index].level, fontFamily: "Iosevka"),
|
||||
),
|
||||
onTap: () {
|
||||
Provider.of<LineFile>(context, listen: false).scrollController.jumpTo(index: sections[index].lineNumber - 1);
|
||||
},
|
||||
trailing: ReorderableDragStartListener(
|
||||
index: index,
|
||||
child: Icon(
|
||||
Icons.drag_handle,
|
||||
color: foreground,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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;
|
||||
}
|
136
lib/view.dart
136
lib/view.dart
|
@ -6,6 +6,8 @@ import 'package:provider/provider.dart';
|
|||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:subwrite/theme.dart';
|
||||
import 'file.dart';
|
||||
import 'ghostsense.dart';
|
||||
import 'highlighter.dart';
|
||||
import 'line_info.dart';
|
||||
|
||||
class View extends StatefulWidget {
|
||||
|
@ -31,6 +33,9 @@ class _View extends State<View> with SingleTickerProviderStateMixin {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
LineFile doc = Provider.of<LineFile>(context);
|
||||
|
@ -43,15 +48,7 @@ class _View extends State<View> with SingleTickerProviderStateMixin {
|
|||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () 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
|
||||
}
|
||||
newFile(doc);
|
||||
},
|
||||
icon: Icon(Icons.create, color: foreground),
|
||||
label: const Text("New Document"),
|
||||
|
@ -61,16 +58,7 @@ class _View extends State<View> with SingleTickerProviderStateMixin {
|
|||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () 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
|
||||
}
|
||||
openFile(doc);
|
||||
},
|
||||
icon: Icon(Icons.file_open, color: foreground),
|
||||
label: const Text("Open Document"),
|
||||
|
@ -79,21 +67,101 @@ class _View extends State<View> with SingleTickerProviderStateMixin {
|
|||
),
|
||||
);
|
||||
} else {
|
||||
|
||||
return ScrollablePositionedList.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemScrollController: doc.scrollController,
|
||||
itemCount: doc.lines(),
|
||||
padding: EdgeInsets.zero,
|
||||
minCacheExtent: 10,
|
||||
addRepaintBoundaries: true,
|
||||
itemBuilder: (BuildContext bcontext, int index) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: doc.backingLines[index],
|
||||
builder: (lcontext, lineinfo) {
|
||||
return Provider.of<LineInfo>(lcontext).build(context, _animationController);
|
||||
});
|
||||
});
|
||||
return MouseRegion(
|
||||
onHover: (PointerEvent details) {
|
||||
if (!doc.showSuggestions) {
|
||||
setState(() {
|
||||
x = details.localPosition.dx;
|
||||
y = details.localPosition.dy;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
ScrollablePositionedList.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemScrollController: doc.scrollController,
|
||||
itemCount: doc.lines(),
|
||||
padding: EdgeInsets.zero,
|
||||
minCacheExtent: 10,
|
||||
addRepaintBoundaries: true,
|
||||
itemBuilder: (BuildContext bcontext, int index) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: doc.backingLines[index],
|
||||
builder: (lcontext, lineinfo) {
|
||||
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_widget_show(GTK_WIDGET(window));
|
||||
|
||||
gtk_window_set_icon_from_file(window, "./subwrite-icon.png", NULL);
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||
|
||||
|
|
|
@ -62,6 +62,8 @@ flutter:
|
|||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- assets/subwrite-logo.png
|
||||
|
||||
fonts:
|
||||
- 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