diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..b674033 --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + - platform: linux + create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/fonts/iosevka-light.ttf b/fonts/iosevka-light.ttf new file mode 100644 index 0000000..16f6cea Binary files /dev/null and b/fonts/iosevka-light.ttf differ diff --git a/lib/annotation.dart b/lib/annotation.dart new file mode 100644 index 0000000..da39752 --- /dev/null +++ b/lib/annotation.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:subwrite/sourcelink.dart'; +import 'package:subwrite/theme.dart'; + +enum AnnotationType { + highlight, + error, + todo, + note, +} + +class LineAnnotation { + SourceLink sourceLink; + AnnotationType annotationType; + String type; + SourceLink? reflink; + String? description; + + // the tool that generated this annotation + String tool; + + @override + bool operator ==(Object other) { + return other is LineAnnotation && other.annotationType == annotationType && other.description == description && other.sourceLink == sourceLink; + } + + @override + int get hashCode { + var hash = Object.hash(sourceLink, annotationType, description); + return hash; + } + + LineAnnotation(this.sourceLink, this.tool, this.annotationType, this.type, {this.reflink, this.description}); + + Icon getIcon(double size) { + if (type == "note") { + return Icon( + Icons.note, + color: literal, + size: size, + ); + } + if (type == "todo") { + return Icon( + Icons.details, + color: variable, + size: size, + ); + } + if (type == "incomplete") { + return Icon( + Icons.label, + color: constant, + size: size, + ); + } + if (type == "edit") { + return Icon( + Icons.label, + color: literal, + size: size, + ); + } + return Icon( + Icons.warning_amber_outlined, + color: Colors.red, + size: size, + ); + } +} diff --git a/lib/cursor.dart b/lib/cursor.dart new file mode 100644 index 0000000..d3d061e --- /dev/null +++ b/lib/cursor.dart @@ -0,0 +1,28 @@ +class Cursor { + Cursor({this.line = 0, this.column = 0, this.anchorLine = 0, this.anchorColumn = 0}); + + int line = 0; + int column = 0; + int anchorLine = 0; + int anchorColumn = 0; + + Cursor copy() { + return Cursor(line: line, column: column, anchorLine: anchorLine, anchorColumn: anchorColumn); + } + + Cursor normalized() { + Cursor res = copy(); + if (line > anchorLine || (line == anchorLine && column > anchorColumn)) { + res.line = anchorLine; + res.column = anchorColumn; + res.anchorLine = line; + res.anchorColumn = column; + return res; + } + return res; + } + + bool hasSelection() { + return line != anchorLine || column != anchorColumn; + } +} diff --git a/lib/cursor_provider.dart b/lib/cursor_provider.dart new file mode 100644 index 0000000..68d55e4 --- /dev/null +++ b/lib/cursor_provider.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; + +import 'cursor.dart'; +import 'line_info.dart'; + +class CursorProvider extends ChangeNotifier { + Cursor cursor = Cursor(line: 1, column: 0); + bool inMouseSelection = false; + + int get line => cursor.line; + int get column => cursor.column; + + void publishCursor(List elements) { + var ncursor = cursor.normalized(); + elements.forEach((element) { + element.setCursor(ncursor); + }); + } + + void moveCursorToDocStart() { + clearSelection(); + cursor.line = 0; + cursor.column = 0; + cursor = cursor.normalized(); + notifyListeners(); + } + + void moveCursorToLineStart() { + cursor.column = 0; + notifyListeners(); + } + + void clearSelection() { + inMouseSelection = false; + cursor.anchorLine = cursor.line; + cursor.anchorColumn = cursor.column; + notifyListeners(); + } + + void moveCursorToLineEnd(int lineLength) { + cursor.column = lineLength; + notifyListeners(); + } + + void moveCursor(int dx, int dy, List backingLines, {bool keepAnchor = false}) { + //print(cursor.line.toString() + " " + cursor.column.toString() + " " + dx.toString() + " " + dy.toString() + " " + backingLines[cursor.line - 1].text.length.toString()); + + if (dx != 0) { + if (cursor.column + dx < 0) { + moveCursor(0, -1, backingLines, keepAnchor: keepAnchor); + if (cursor.line > 1) { + moveCursorToLineEnd(backingLines[cursor.line - 1].text.length); + } + } else if (cursor.column + dx > backingLines[cursor.line - 1].text.length) { + moveCursor(0, 1, backingLines, keepAnchor: keepAnchor); + moveCursorToLineStart(); + } else { + cursor.column += dx; + } + } + + if (dy != 0) { + // Only allow the cursor line to change if in bounds + if (cursor.line + dy > 0 && cursor.line + dy <= backingLines.length) { + cursor.line += dy; + // Change cursor to line end + if (cursor.column > backingLines[cursor.line - 1].text.length) { + moveCursorToLineEnd(backingLines[cursor.line - 1].text.length); + } + } + } + + if (!keepAnchor) { + clearSelection(); + } + + notifyListeners(); + } + + void mouseDownEnd() { + inMouseSelection = false; + } + + void mouseDownMoveUpdate(int col, List backingLines) { + if (cursor.column != col) { + var lineLength = backingLines[cursor.line - 1].text.length; + cursor.column = col.clamp(0, lineLength); + if (inMouseSelection == false) { + clearSelection(); + } + publishCursor(backingLines); + notifyListeners(); + } else if (inMouseSelection == false) { + clearSelection(); + publishCursor(backingLines); + notifyListeners(); + } + inMouseSelection = true; + } + + void mouseDownUpdate(int line, int col, List backingLines) { + if (cursor.line != line || cursor.column != col) { + cursor.line = line; + var lineLength = backingLines[cursor.line - 1].text.length; + cursor.column = col.clamp(0, lineLength); + if (inMouseSelection == false) { + clearSelection(); + } + publishCursor(backingLines); + notifyListeners(); + } else if (inMouseSelection == false) { + clearSelection(); + publishCursor(backingLines); + notifyListeners(); + } + inMouseSelection = true; + } + + bool hasSelection() { + return cursor.hasSelection(); + } + + Cursor normalized() { + return cursor.normalized(); + } +} diff --git a/lib/editor.dart b/lib/editor.dart new file mode 100644 index 0000000..6c455fa --- /dev/null +++ b/lib/editor.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:subwrite/view.dart'; + +import 'input.dart'; + +class Editor extends StatefulWidget { + const Editor({super.key}); + + @override + State createState() => _Editor(); +} + +class _Editor extends State { + @override + Widget build(BuildContext context) { + return InputListener(LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { + //print("opening document " + path + " " + startLine.toString()); + return View(); + })); + } +} diff --git a/lib/file.dart b/lib/file.dart new file mode 100644 index 0000000..2b601d2 --- /dev/null +++ b/lib/file.dart @@ -0,0 +1,472 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:subwrite/annotation.dart'; + +import 'package:subwrite/section.dart'; +import 'package:subwrite/sourcelink.dart'; +import 'package:subwrite/suggestion.dart'; +import 'cursor_provider.dart'; +import 'line_info.dart'; + +class LineFile extends ChangeNotifier { + List backingLines = List.empty(growable: true); + + CursorProvider cursor = CursorProvider(); + FocusNode focus = FocusNode(); + bool showSuggestions = false; + late String projectPath; + late String path; + + ScrollController scrollController = ScrollController(); + + void openFile(String path, int startLine) { + this.path = 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; + }); + + // handle the empty file case + if (backingLines.isEmpty) { + backingLines.insert(0, LineInfo(path, 1, "")); + } + parse(); + cursor.moveCursorToDocStart(); + focus.requestFocus(); + scrollController = ScrollController(initialScrollOffset: (startLine * 17.0 - (17.0 * 10.0)).clamp(0.0, double.infinity)); + notifyListeners(); + } + + void cancelSuggestions() { + if (showSuggestions) { + suggestionIndex = 0; + showSuggestions = false; + notifyListeners(); + } + } + + ScrollController suggestionListController = ScrollController(initialScrollOffset: 0.0); + int suggestionIndex = 0; + List suggestions = List.empty(); + + // Future> loadSuggestions(IDE ide) async { + // return ide.getSuggestions(projectPath, path, cursor.line, cursor.column, base64()).then((list) { + // suggestions = list; + // return list; + // }); + // } + + 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() { + suggestionIndex = (suggestionIndex) % suggestions.length; + for (var char = suggestions[suggestionIndex].partialLen; char < suggestions[suggestionIndex].name.length; char++) { + insertChar(suggestions[suggestionIndex].name[char]); + } + cursor.publishCursor(backingLines); + 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.clearSelection(); + cursor.moveCursorToLineStart(); + cursor.publishCursor(backingLines); + } else if (event.logicalKey == LogicalKeyboardKey.end) { + cancelSuggestions(); + cursor.clearSelection(); + cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length); + cursor.publishCursor(backingLines); + } else if (event.logicalKey == LogicalKeyboardKey.enter) { + if (showSuggestions == false) { + insertLine(); + cancelSuggestions(); + cursor.clearSelection(); + cursor.publishCursor(backingLines); + } else { + suggestListInsert(); + } + } else if (event.logicalKey == LogicalKeyboardKey.tab) { + insertChar(" "); + cancelSuggestions(); + cursor.publishCursor(backingLines); + } else if (event.logicalKey == LogicalKeyboardKey.delete) { + handleDelete(); + cancelSuggestions(); + cursor.clearSelection(); + cursor.publishCursor(backingLines); + } else if (event.logicalKey == LogicalKeyboardKey.backspace) { + handleBackspace(); + cancelSuggestions(); + cursor.clearSelection(); + cursor.publishCursor(backingLines); + } else if (event.logicalKey == LogicalKeyboardKey.escape) { + showSuggestions = false; + notifyListeners(); + } else { + if (event.isControlPressed) { + if (event.logicalKey == LogicalKeyboardKey.space) { + showSuggestions = true; + 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(); + return KeyEventResult.handled; + } + + void saveFile() { + String content = ''; + for (var l in backingLines) { + content += l.text + '\n'; + } + File(path).writeAsStringSync(content); + } + + 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) { + print("deleting $line ${backingLines[line].text}"); + 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) { + 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() { + // 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) { + 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, + ); + + List
sections = List.empty(growable: true); + + // 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")); + } + + _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(); + } + + String base64() { + String content = ''; + backingLines.forEach((l) { + content += l.text + '\n'; + }); + return base64Encode(utf8.encode(content)); + } + + 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) { + cancelSuggestions(); + cursor.mouseDownUpdate(line, col, backingLines); + } + + void mouseDownMoveUpdate(int col) { + cancelSuggestions(); + cursor.mouseDownMoveUpdate(col, backingLines); + } + + void mouseDownEnd() { + cursor.mouseDownEnd(); + } + + void moveLines(int startLine, int endLine, int newLine) { + print("want to move from $startLine (: $endLine) to $newLine\n"); + List 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) { + + var notetype = "note"; + if (note.startsWith("[incomplete]")) { + notetype = "incomplete"; + } else if (note.startsWith("[edit]")) { + notetype = "edit"; + } + + backingLines[cursor.line-1].annotations.add( + LineAnnotation( + SourceLink( + path, + lineStart: cursor.line, + ), + "notes", + AnnotationType.note, + notetype, + description: note)); + notifyListeners(); + } +} diff --git a/lib/highlighter.dart b/lib/highlighter.dart new file mode 100644 index 0000000..487e840 --- /dev/null +++ b/lib/highlighter.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:subwrite/sourcelink.dart'; +import 'package:subwrite/theme.dart'; + +import 'annotation.dart'; + +double fontSize = 14; +double gutterFontSize = 12; + +class LineDecoration { + int start = 0; + int end = 0; + Color color = Colors.white; + Color background = Colors.white; + bool underline = false; + bool italic = false; + bool bold = false; + + SourceLink? message; +} + +class Highlighter { + Highlighter() {} + + List run(BuildContext context, String text, int line, bool highlightRisk, TextStyle style, List? insights, int? cursorStart, int? cursorEnd, bool lineSelected) { + TextStyle defaultStyle = style.copyWith(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, height: 1.2); + List res = []; + List decors = []; + + insights + ?.where( + (element) => element.annotationType == AnnotationType.highlight, + ) + .forEach((element) { + LineDecoration d = LineDecoration(); + //print("${element.sourceLink.colStart} ${element.sourceLink.colEnd}"); + d.start = element.sourceLink.colStart! - 1; + d.end = element.sourceLink.colEnd! - 2; + d.background = Colors.transparent; + switch (element.type) { + case "header": + d.color = header; + break; + case "bold": + d.color = keyword; + d.bold = true; + break; + case "code": + d.color = function; + break; + } + + decors.add(d); + }); + + // Assume that decorators do not overlap... + decors.sort((a, b) { + return a.start.compareTo(b.start); + }); + + text += ' '; + + for (int i = 0; i < text.length; i++) { + String current = text[i]; + TextStyle style = defaultStyle.copyWith(); + + // decorate + for (var d in decors) { + // if we are in this decoration... + if (i >= d.start && i <= d.end) { + style = style.copyWith(color: d.color); + if (d.bold = true) { + style = style.copyWith(fontWeight: FontWeight.bold); + } + } + } + + // is within selection + if (lineSelected) { + style = style.copyWith(backgroundColor: selection); + } else if (cursorStart != null && cursorEnd != null) { + if (i >= cursorStart && i < cursorEnd) { + style = style.copyWith(backgroundColor: selection); + } + } else if (cursorStart != null && cursorEnd == null) { + if (i >= cursorStart) { + style = style.copyWith(backgroundColor: selection); + } + } else if (cursorStart == null && cursorEnd != null) { + if (i < cursorEnd) { + style = style.copyWith(backgroundColor: selection); + } + } + + if (highlightRisk) { + style = style.copyWith( + backgroundColor: risk.withOpacity(0.50), + ); + } + + if (current == " ") { + res.add(TextSpan(text: "_", style: style.copyWith(color: style.backgroundColor), semanticsLabel: " ", mouseCursor: SystemMouseCursors.text)); + } else { + // we need to add a '\ufeff' (Zero Width No-Break Space) here to avoid RichText doing ridiculous + // breaking logic with hyphens / slashes etc. + res.add(TextSpan(text: '$current\ufeff', style: style, mouseCursor: SystemMouseCursors.text)); + } + } + return res; + } +} diff --git a/lib/input.dart b/lib/input.dart new file mode 100644 index 0000000..f50a692 --- /dev/null +++ b/lib/input.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:subwrite/theme.dart'; + +import 'file.dart'; + +class InputListener extends StatefulWidget { + late Widget child; + InputListener(this.child); + @override + _InputListener createState() => _InputListener(); +} + +class _InputListener extends State { + late FocusNode focusNode; + + @override + void initState() { + super.initState(); + focusNode = FocusNode(); + } + + @override + void dispose() { + super.dispose(); + focusNode.dispose(); + } + + void newTextDialog(context, String title, String hint, Function(String) callback) { + // show the dialog + showDialog( + context: context, + builder: (BuildContext context) { + TextEditingController controller = TextEditingController(); + return AlertDialog( + shape: ContinuousRectangleBorder(side: BorderSide(color: sidebarAlt)), + backgroundColor: sidebar, + title: Align(alignment: Alignment.topCenter, child: Text(title)), + titleTextStyle: TextStyle(fontSize: 10.0), + titlePadding: EdgeInsets.all(5.0), + content: TextField( + autofocus: true, + controller: controller, + onSubmitted: (newValue) { + callback(newValue); + Navigator.of(context).pop(); + }, + style: TextStyle(color: Colors.white, fontSize: 10.0), + cursorColor: Colors.white, + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: Colors.white38), + isDense: true, + fillColor: sidebarHighlight, + focusColor: sidebarHighlight, + labelStyle: TextStyle(color: Colors.white), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(width: 1, color: sidebarAlt), + borderRadius: BorderRadius.zero, + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(width: 1, color: sidebarHighlight), + borderRadius: BorderRadius.zero, + )), + ), + contentPadding: EdgeInsets.symmetric(vertical: 1.0, horizontal: 5.0), + actions: [ + + ElevatedButton.icon(icon: Icon(Icons.label, color: literal,), onPressed: () { + controller.text = "[edit]${controller.text}"; + callback(controller.text); + Navigator.of(context).pop(); + }, label: Text("Edit"),style: ButtonStyle(backgroundColor: MaterialStateProperty.all(tabs), foregroundColor: MaterialStateProperty.all(foreground))), + + + ElevatedButton.icon(icon: Icon(Icons.label, color: constant,), onPressed: () { + controller.text = "[incomplete]${controller.text}"; + callback(controller.text); + Navigator.of(context).pop(); + }, label: Text("Incomplete"),style: ButtonStyle(backgroundColor: MaterialStateProperty.all(tabs), foregroundColor: MaterialStateProperty.all(foreground))), + ], + ); + }, + ).then((value) => null); + } + + @override + Widget build(BuildContext context) { + if (!focusNode.hasFocus) { + focusNode.requestFocus(); + } + + LineFile doc = Provider.of(context); + + return GestureDetector( + child: Focus( + child: widget.child, + focusNode: focusNode, + autofocus: true, + onKey: (FocusNode node, RawKeyEvent event) { + doc.handleKey(event); + if (event.logicalKey == LogicalKeyboardKey.escape) { + newTextDialog(context, "New Note", "Note", (note) => {doc.addNote(note)}); + } + return KeyEventResult.handled; + })); + } +} diff --git a/lib/line_info.dart b/lib/line_info.dart new file mode 100644 index 0000000..30ceee4 --- /dev/null +++ b/lib/line_info.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:subwrite/theme.dart'; + +import 'annotation.dart'; +import 'cursor.dart'; +import 'file.dart'; +import 'highlighter.dart'; + +double gutterWidth = 64; +double rightMargin = 32; + +class LineInfo extends ChangeNotifier { + String path; + String text; + int lineNumber; + int? cursorStart; + int? cursorEnd; + bool lineSelected = false; + List annotations = List.empty(growable: true); + + + LineInfo(this.path, this.lineNumber, this.text); + + Widget build(BuildContext context) { + bool highlightRisk = false; + + var backgroundStyle = TextStyle(backgroundColor: cursorStart != null ? sidebarHighlight : background); + + Highlighter hl = Highlighter(); + List spans = hl.run(context, text, lineNumber, highlightRisk, backgroundStyle, annotations, cursorStart, cursorEnd, lineSelected); + + final gutterStyle = TextStyle(fontFamily: 'Iosevka', decoration: null, decorationColor: null, fontSize: gutterFontSize, color: comment, backgroundColor: sidebarAlt, height: 1.0); + + return LayoutBuilder( + builder: (context, constraints) { + var highlightedLine = RichText( + text: TextSpan( + children: spans, + style: backgroundStyle, + ), + ); + + List stackElements = List.empty(growable: true); + stackElements.add(highlightedLine); + + var charWidth = (fontSize / 2.0); + var lineHeight = (fontSize * 1.2); + var usableSize = (constraints.maxWidth - gutterWidth - rightMargin); + + var sidebarContent = List.empty(growable: true); + sidebarContent.add(Text('$lineNumber'.padLeft(5), style: gutterStyle)); + + var icons = annotations.where((element) => element.annotationType == AnnotationType.note).map((e) { + highlightRisk = true; + return Padding(padding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: Tooltip(message: e.description!, child: e.getIcon(15.0))); + }).toList(); + + if (icons != null) { + sidebarContent.addAll(icons); + } + + icons = annotations.where((element) => element.annotationType == AnnotationType.todo).map((e) { + highlightRisk = true; + return Padding(padding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: e.getIcon(15.0)); + }).toList(); + + if (icons != null) { + sidebarContent.addAll(icons); + } + + if (cursorStart != null) { + var maxCharsBeforeWrapping = (usableSize / charWidth).floorToDouble(); + var cursorPosTop = (cursorStart! / maxCharsBeforeWrapping).floorToDouble() * lineHeight; + var cursorPosLeft = ((cursorStart! % maxCharsBeforeWrapping) * charWidth) - charWidth / 2.0; + TextStyle cusrsorStyle = TextStyle(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, backgroundColor: Colors.transparent, letterSpacing: -2.0); + var cursorPos = Positioned(top: cursorPosTop.toDouble(), left: cursorPosLeft.toDouble(), child: Text("|", style: cusrsorStyle)); + stackElements.add(cursorPos); + } + + var idealHeight = (lineHeight * (1.0 + ((text.length * charWidth) / usableSize).floor())).ceilToDouble(); + + var cursorOverlay = Stack(children: stackElements); + + return Listener( + onPointerDown: (event) { + //var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0); + var inMargin = (event.localPosition.dx - gutterWidth) > usableSize; + var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); + var file = Provider.of(context, listen: false); + if (event.down && !inMargin) { + file.mouseDownUpdate(lineNumber, c.round()); + } + }, + onPointerMove: (event) { + //var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0) + var inMargin = (event.localPosition.dx - gutterWidth) > usableSize; + var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); + var file = Provider.of(context, listen: false); + if (event.down && !inMargin) { + file.mouseDownMoveUpdate(c.round()); + } else { + file.mouseDownEnd(); + } + }, + child: MouseRegion( + onEnter: (event) { + //var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0); + var inMargin = (event.localPosition.dx - gutterWidth) > usableSize; + var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); + var file = Provider.of(context, listen: false); + if (event.down && !inMargin) { + file.mouseDownUpdate(lineNumber, c.round()); + } else { + file.mouseDownEnd(); + } + }, + onHover: (event) { + //var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0); + var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); + var file = Provider.of(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()); + } else { + file.mouseDownEnd(); + } + }, + onExit: (event) { + notifyListeners(); + }, + child: Container( + color: backgroundStyle.backgroundColor, + padding: EdgeInsets.zero, + margin: EdgeInsets.zero, + height: idealHeight, + width: constraints.maxWidth, + child: Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Container( + padding: EdgeInsets.zero, + decoration: BoxDecoration( + color: sidebarAlt, + ), + width: gutterWidth - 1, + height: idealHeight, + margin: EdgeInsets.only(right: 1, top: 0, bottom: 0, left: 0), + alignment: Alignment.centerLeft, + child: Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.start, children: sidebarContent)), + Container( + padding: EdgeInsets.zero, + margin: EdgeInsets.only(right: rightMargin, top: 0, bottom: 0, left: 0), + decoration: BoxDecoration( + color: backgroundStyle.backgroundColor, + ), + width: constraints.maxWidth - rightMargin - gutterWidth, + height: idealHeight, + child: cursorOverlay, + ), + ])))); + }, + ); + } + + void setCursor(Cursor cursor) { + if (cursor.hasSelection()) { + // if we are part of a selection then act like it + if (cursor.line < lineNumber && cursor.anchorLine > lineNumber) { + if (lineSelected == false) { + lineSelected = true; + cursorStart = null; + cursorEnd = null; + notifyListeners(); + } + } else if (cursor.line == lineNumber && cursor.anchorLine > lineNumber) { + // else if the selection starts on this line... + // obliterate selection + if (lineSelected == true) { + lineSelected = false; + } + cursorStart = cursor.column; + cursorEnd = null; + notifyListeners(); + } else if (cursor.line < lineNumber && cursor.anchorLine == lineNumber) { + // else if the selection ends on this line... + // obliterate selection + if (lineSelected == true) { + lineSelected = false; + } + cursorStart = null; + cursorEnd = cursor.anchorColumn; + notifyListeners(); + } else if (cursor.line == cursor.line && cursor.anchorLine == lineNumber) { + if (lineSelected == true) { + lineSelected = false; + } + cursorStart = cursor.column; + cursorEnd = cursor.anchorColumn; + notifyListeners(); + } else { + // obliterate selection + if (lineSelected == true) { + lineSelected = false; + notifyListeners(); + } + if (cursorStart != null) { + cursorStart = null; + notifyListeners(); + } + if (cursorEnd != null) { + cursorEnd = null; + notifyListeners(); + } + } + } else { + // obliterate selection + if (lineSelected == true) { + lineSelected = false; + cursorStart = null; + cursorEnd = null; + notifyListeners(); + } + if (cursor.line == lineNumber) { + cursorStart = cursor.column; + cursorEnd = cursor.anchorColumn; + notifyListeners(); + } else if (cursorStart != null || cursorEnd != null) { + cursorStart = null; + cursorEnd = null; + notifyListeners(); + } + } + } + + void updateText(String line) { + text = line; + notifyListeners(); + } + + void notify() { + notifyListeners(); + } +} + +int cursorFromMouse(double usableSize, double mx, double my) { + var charWidth = (fontSize / 2.0); + var lineHeight = (fontSize * 1.2); + var maxCharsBeforeWrapping = (usableSize / charWidth).floorToDouble(); + var prevLines = (my / lineHeight).floorToDouble() * maxCharsBeforeWrapping; + var charsInThisLine = ((mx) / charWidth).floorToDouble(); + return (prevLines + charsInThisLine).floor(); +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..405f07d --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:subwrite/file.dart'; +import 'package:subwrite/theme.dart'; + +import 'editor.dart'; +import 'outline.dart'; + +void main(List args) { + var docfile = LineFile(); + docfile.openFile("./example.md", 0); + var doc = ChangeNotifierProvider.value(value: docfile); + + + runApp(MultiProvider( + providers: [doc], + builder: (context, child) { + return MyApp(); + })); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'subwrite', + theme: ThemeData(scrollbarTheme: ScrollbarThemeData(thumbColor: MaterialStateProperty.all(header))), + home: const SarahDownApp(), + ); + } +} + +class SarahDownApp extends StatefulWidget { + const SarahDownApp({super.key}); + + @override + State createState() => _SarahDownApp(); +} + +class _SarahDownApp extends State { + @override + Widget build(BuildContext context) { + return Material( + color: sidebarAlt, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.start, + children: [Expanded(child: Outline()), Container(color: comment)])), + Flexible(flex: 5, child: Editor()) + ], + )); + } +} diff --git a/lib/outline.dart b/lib/outline.dart new file mode 100644 index 0000000..fba3d84 --- /dev/null +++ b/lib/outline.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:subwrite/file.dart'; +import 'package:subwrite/theme.dart'; + +class Outline extends StatefulWidget { + const Outline({super.key}); + + @override + State createState() => _Outline(); +} + +class _Outline extends State { + @override + Widget build(BuildContext context) { + var sections = Provider.of(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(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 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(context, listen: false).scrollController.jumpTo(sections[index].lineNumber * 17.0); + }, + trailing: ReorderableDragStartListener( + index: index, + child: Icon( + Icons.drag_handle, + color: foreground, + ), + ), + ); + }, + itemCount: sections.length, + )); + } +} diff --git a/lib/section.dart b/lib/section.dart new file mode 100644 index 0000000..d4ea1b9 --- /dev/null +++ b/lib/section.dart @@ -0,0 +1,7 @@ +class Section { + int lineNumber; + int level; + String title; + + Section(this.lineNumber, this.level, this.title); +} diff --git a/lib/sourcelink.dart b/lib/sourcelink.dart new file mode 100644 index 0000000..5946905 --- /dev/null +++ b/lib/sourcelink.dart @@ -0,0 +1,51 @@ +class SourceLink { + late final String file; + final int? lineStart; + final int? colStart; + final int? lineEnd; + final int? colEnd; + + SourceLink(this.file, {this.lineStart, this.colStart, this.lineEnd, this.colEnd}); + + String lineLink() { + return file + ":" + lineStart.toString(); + } + + @override + bool operator ==(Object other) { + return other is SourceLink && other.hashCode == hashCode; + } + + @override + int get hashCode { + var hash = Object.hash(file, lineStart, colStart, lineEnd, colEnd); + return hash; + } +} + +SourceLink? parseSourceLink(String line) { + String? file; + int? lineStart; + int? colStart; + int? lineEnd; + int? colEnd; + var parts = line.split(":"); + if (parts.isNotEmpty && parts[0].isNotEmpty) { + file = parts[0]; + } else { + return null; + } + if (parts.length > 1 && parts[1].trim().isNotEmpty) { + lineStart = int.parse(parts[1]); + } + if (parts.length > 2 && parts[2].trim().isNotEmpty) { + colStart = int.parse(parts[2]); + } + if (parts.length > 3 && parts[3].trim().isNotEmpty) { + lineEnd = int.parse(parts[3]); + } + if (parts.length > 4 && parts[4].trim().isNotEmpty) { + colEnd = int.parse(parts[4]); + } + return SourceLink(file, lineStart: lineStart, lineEnd: lineEnd, colStart: colStart, colEnd: colEnd); +} diff --git a/lib/suggestion.dart b/lib/suggestion.dart new file mode 100644 index 0000000..032550f --- /dev/null +++ b/lib/suggestion.dart @@ -0,0 +1,42 @@ +import 'package:flutter/cupertino.dart'; +import 'package:subwrite/theme.dart'; + +class Suggestion { + String classType; + String name; + String typeSignature; + int partialLen; + + Suggestion(this.classType, this.name, this.partialLen, this.typeSignature); + + @override + bool operator ==(Object other) { + return other is Suggestion && hashCode == other.hashCode; + } + + @override + int get hashCode => classType.hashCode + name.hashCode + typeSignature.hashCode; + + Widget getWidget() { + var color = function; + switch (classType) { + case "const": + color = constant; + break; + case "var": + color = variable; + break; + case "package": + color = keyword; + break; + case "type": + 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: typeSignature, style: const TextStyle(fontFamily: "Iosevka", fontSize: 10.0))), + ]); + } +} diff --git a/lib/theme.dart b/lib/theme.dart new file mode 100644 index 0000000..11df891 --- /dev/null +++ b/lib/theme.dart @@ -0,0 +1,21 @@ +import 'dart:ui'; + +Color foreground = const Color(0xfff8f8f2); +Color background = const Color(0xff000000); //Color(0xFF1A0F1C); +Color comment = const Color(0xffd684e8); +Color selection = const Color(0xff777889); + +Color constant = const Color(0xff8be9fd); +Color header = const Color(0xffbd93f9); +Color keyword = const Color(0xffffb86c); +Color variable = const Color(0xffff79c6); +Color function = const Color(0xfff1fa8c); +Color literal = const Color(0xff50fa7b); +Color field = header; + +Color risk = const Color(0xff45353b); + +Color sidebarAlt = const Color(0xFF110F15); +Color sidebar = const Color(0xff202023); +Color tabs = const Color(0xFF342036); +Color sidebarHighlight = const Color(0xFF38213D); diff --git a/lib/view.dart b/lib/view.dart new file mode 100644 index 0000000..ad6ee0d --- /dev/null +++ b/lib/view.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'file.dart'; +import 'line_info.dart'; + +class View extends StatefulWidget { + View({Key? key}) : super(key: key); + + @override + _View createState() => _View(); +} + +class _View extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + LineFile doc = Provider.of(context); + + return Scrollbar( + controller: doc.scrollController, + trackVisibility: true, + thumbVisibility: true, + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + controller: doc.scrollController, + itemCount: doc.lines(), + padding: EdgeInsets.zero, + shrinkWrap: true, + itemBuilder: (BuildContext bcontext, int index) { + return ChangeNotifierProvider.value( + value: doc.backingLines[index], + builder: (lcontext, lineinfo) { + return Provider.of(lcontext).build(lcontext); + }); + })); + } +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..724236f --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "sarahdown") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.sarahdown") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 0000000..a8a3689 --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "subwrite"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "subwrite"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..29872f3 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,175 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.12" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.18.0 <3.0.0" + flutter: ">=1.16.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..4677470 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,98 @@ +name: subwrite +description: A Ridiculously Personalized Markdown Editor + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=2.18.0 <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + provider: ^6.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + + fonts: + - family: Iosevka + fonts: + - asset: fonts/iosevka-light.ttf + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..94a842a --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:subwrite/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}