Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions example/lib/ide/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ class _IdeEditorState extends State<IdeEditor> {

final globalOffset = (context.findRenderObject() as RenderBox).localToGlobal(event.localPosition);
final codeLines = _linesKey.asCodeLines;
final hoverPosition = codeLines.findCodePositionNearestGlobalOffset(globalOffset);
final hoverPosition = codeLines.findCodePositionNearestGlobalOffset(globalOffset).$1;

final hoverWordRange = codeLines.findWordBoundaryAtGlobalOffset(globalOffset);
if (hoverWordRange == null) {
Expand Down Expand Up @@ -285,7 +285,7 @@ class _IdeEditorState extends State<IdeEditor> {
}

final codeLines = _linesKey.asCodeLines;
final codePosition = codeLines.findCodePositionNearestGlobalOffset(details.globalPosition);
final codePosition = codeLines.findCodePositionNearestGlobalOffset(details.globalPosition).$1;

_currentSelectedPosition = Position(
line: codePosition.line,
Expand Down
7 changes: 3 additions & 4 deletions lib/inception.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ export 'src/editor/code_editor.dart';
export 'src/editor/code_layout.dart';
export 'src/editor/theme.dart';

export 'src/languages/dart/dart_contextualizer.dart';
export 'src/languages/dart/dart_lexer.dart';
export 'src/languages/dart/dart_syntax_highlighter.dart';
export 'src/languages/dart/dart_theme.dart';
export 'src/infrastructure/text/code_comment_selection_rules.dart';

export 'src/lsp/lsp_client.dart';
export 'src/lsp/messages/initialize.dart';
Expand All @@ -29,6 +26,8 @@ export 'src/lsp/messages/rename_files_params.dart';
export 'src/lsp/messages/type_hierarchy.dart';
export 'src/lsp/messages/did_open_text_document.dart';

export 'src/test/code_editor/code_editor_presenters_for_tests.dart';
export 'src/test/code_editor/code_editor_test_inspector.dart';
export 'src/test/code_layout/code_layout_finders.dart';
export 'src/test/code_layout/code_layout_test_inspector.dart';
export 'src/test/code_layout/code_layout_test_interactor.dart';
6 changes: 6 additions & 0 deletions lib/language_dart.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export 'src/languages/dart/dart_code_editor_presenter.dart';
export 'src/languages/dart/dart_contextualizer.dart';
export 'src/languages/dart/dart_lexer.dart';
export 'src/languages/dart/dart_syntax_highlighter.dart';
export 'src/languages/dart/dart_theme.dart';
export 'src/languages/dart/dart_theme_pineapple.dart';
107 changes: 104 additions & 3 deletions lib/src/document/code_document.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:inception/inception.dart';
import 'package:inception/src/document/lexing.dart';
import 'package:inception/src/document/piece_table.dart';

Expand All @@ -22,6 +23,15 @@ class CodeDocument {
String get text => _pieceTable.getText();
int get length => _pieceTable.length;

/// Extracts and returns a list of substrings for every line in the document.
List<String> computeLines() {
final lines = <String>[];
for (int i = 0; i < lineCount; i += 1) {
lines.add(getLine(i)!);
}
return lines;
}

String? getLine(int lineIndex) {
if (lineIndex < 0 || lineIndex >= lineCount) {
return null;
Expand All @@ -30,6 +40,21 @@ class CodeDocument {
return text.substring(getLineStart(lineIndex), getLineEnd(lineIndex));
}

/// Returns `true` if the given [position] points to a location in this document
/// that exists, or `false` otherwise.
///
/// A [position] can be invalid in one of four ways:
/// 1. Points to a negative line index.
/// 2. Points to a line beyond the end of this document.
/// 3. Points to a negative character offset.
/// 4. Points to a character offset beyond the end of the line.
bool containsPosition(CodePosition position) {
return position.line >= 0 &&
position.line < lineCount &&
position.characterOffset >= 0 &&
position.characterOffset <= getLine(position.line)!.length;
}

// ----- START TOKENS --------
Lexer lexer;
List<LexerToken> _tokens = const [];
Expand Down Expand Up @@ -57,6 +82,80 @@ class CodeDocument {
}
}

LexerToken? findTokenAt(CodePosition position) {
final textOffset = lineColumnToOffset(position.line, position.characterOffset);
final tokens = _tokens.where((t) => textOffset == t.start || (t.start < textOffset && textOffset < t.end));

if (tokens.length > 1) {
throw Exception(
"Something went wrong when finding the token at $position - we expect to find either 0 or 1, but we found ${tokens.length}",
);
}
if (tokens.isEmpty) {
return null;
}

return tokens.first;
}

LexerToken? findTokenToTheLeftOnSameLine(
CodePosition from, {
TokenFilter? filter,
}) {
if (!containsPosition(from)) {
return null;
}

// FIXME: This logic probably needs to use a character iterator, rather than
// an index integer. Look into it and make the switch if necessary.
var tokenAtStartingPoint = findTokenAt(from);
var textIndex = lineColumnToOffset(from.line, from.characterOffset) - 1;
while (textIndex >= 0 && offsetToCodePosition(textIndex).line == from.line) {
final nextPosition = offsetToCodePosition(textIndex);
final nextToken = findTokenAt(nextPosition);
if (nextToken != tokenAtStartingPoint &&
nextToken != null &&
(filter == null || filter(nextToken, nextPosition))) {
// This is the nearest token to the left, that we're looking for.
return nextToken;
}

// Move one character to the left.
textIndex -= 1;
}

return null;
}

LexerToken? findTokenToTheRightOnSameLine(
CodePosition from, {
TokenFilter? filter,
}) {
if (!containsPosition(from)) {
return null;
}

// FIXME: This logic probably needs to use a character iterator, rather than
// an index integer. Look into it and make the switch if necessary.
var tokenAtStartingPoint = findTokenAt(from);
var textIndex = lineColumnToOffset(from.line, from.characterOffset) + 1;
while (textIndex < length && offsetToCodePosition(textIndex).line == from.line) {
final nextPosition = offsetToCodePosition(textIndex);
final nextToken = findTokenAt(nextPosition);
if (nextToken != tokenAtStartingPoint &&
nextToken != null &&
(filter == null || filter(nextToken, nextPosition))) {
// This is the nearest token to the left, that we're looking for.
return nextToken;
}

// Move one character to the right.
textIndex += 1;
}

return null;
}

/// Returns all tokens that overlap the current selection or cursor position.
/// If there is no selection, this returns an empty list.
List<LexerToken> tokensInSelection() {
Expand Down Expand Up @@ -368,16 +467,16 @@ class CodeDocument {
return null;
}

final startLine = offsetToLineColumn(selectionStart).$1;
final startLine = offsetToCodePosition(selectionStart).line;
final startOffset = getLineStart(startLine);
final endOffset = startLine + 1 < lineCount ? getLineStart(startLine + 1) : length;

return (start: startOffset, end: endOffset);
}

(int line, int column) offsetToLineColumn(int offset) {
CodePosition offsetToCodePosition(int offset) {
final lineIndex = _findLineIndex(offset);
return (lineIndex, offset - _lineStarts[lineIndex]);
return CodePosition(lineIndex, offset - _lineStarts[lineIndex]);
}

int lineColumnToOffset(int line, int column) => _lineStarts[line] + column;
Expand Down Expand Up @@ -668,6 +767,8 @@ class CodeDocument {
}
}

typedef TokenFilter = bool Function(LexerToken token, CodePosition position);

class _InsertAction implements _EditAction {
_InsertAction(this.offset, this.text);

Expand Down
28 changes: 28 additions & 0 deletions lib/src/document/selection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,26 @@ class CodeSelection {
final CodePosition base;
final CodePosition extent;

/// Returns the [CodePosition] at the most upstream point in the selection, which might be the
/// [base] or the [extent], depending on the direction of the selection.
CodePosition get start => base <= extent ? base : extent;

/// Returns the [CodePosition] at the most downstream point in the selection, which might be the
/// [base] or the [extent], depending on the direction of the selection.
CodePosition get end => base <= extent ? extent : base;

bool get isCollapsed => base == extent;

bool get isExpanded => base != extent;

/// Returns `true` if the selection points from upstream to downstream, e.g., the
/// base of the selection comes before the extent.
bool get isDownstream => extent >= base;

/// Returns `true` if the selection points from downstream to upstream, e.g., the
/// extent of the selection comes before the base.
bool get isUpstream => base > extent;

CodeRange toRange() {
final affinity = extent.line > base.line || extent.characterOffset >= base.characterOffset
? TextAffinity.downstream
Expand All @@ -27,6 +43,18 @@ class CodeSelection {
affinity == TextAffinity.downstream ? extent : base,
);
}

@override
String toString() =>
"[CodeRange]: L${base.line}:C${base.characterOffset} -> L${extent.line}:C${extent.characterOffset}";

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CodeSelection && runtimeType == other.runtimeType && base == other.base && extent == other.extent;

@override
int get hashCode => base.hashCode ^ extent.hashCode;
}

/// A range of code, from a starting line and offset, to an ending line and offset.
Expand Down
Loading
Loading