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
37 changes: 12 additions & 25 deletions .github/workflows/ci-dev.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: CI Develop

on:
workflow_run:
workflows: ["Auto Fix Format"]
types:
- completed # 在工作流完成时触发
pull_request:
branches: [dev]
paths-ignore:
Expand All @@ -10,15 +14,9 @@ on:
- 'LICENSE'
- 'README*'
- 'CHANGELOG*'
push:
merge_group:
branches: [dev]
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/**/*.md'
- 'LICENSE'
- 'README*'
- 'CHANGELOG*'
types: [checks_requested]

concurrency:
group: ci-dev-${{ github.ref }}
Expand All @@ -34,26 +32,11 @@ env:

jobs:
# ========================================
# 1. 代码格式检查
# ========================================
format:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6

- name: 安装 .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: 检查代码格式
run: dotnet format --verify-no-changes --verbosity diagnostic

# ========================================
# 2. 构建 + 测试 (Debug)
# 1. 构建 + 测试 (Debug)
# ========================================
build-and-test:
runs-on: windows-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
needs: format
steps:
- uses: actions/checkout@v6
Expand All @@ -62,6 +45,7 @@ jobs:
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
ref: ${{ github.event.workflow_run.head_branch }}

- name: 缓存 NuGet 包
uses: actions/cache@v4
Expand All @@ -73,6 +57,9 @@ jobs:
- name: 还原 NuGet 包
run: dotnet restore

- name: Build (Debug)
run: dotnet build --no-restore --configuration Debug

- name: 运行 EasyCon 测试
shell: cmd
run: ci\test.bat Debug
8 changes: 8 additions & 0 deletions src/EasyCon2.Avalonia.Core/AvaloniaRuntime.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Themes.Fluent;

namespace EasyCon2.Avalonia.Core;
Expand All @@ -16,6 +17,13 @@ public static void EnsureInitialized()
.LogToTrace()
.SetupWithoutStarting();
app.Instance.Styles.Add(new FluentTheme());

var editStyle = new StyleInclude(new Uri("avares://EasyCon2.Avalonia.Core"))
{
Source = new Uri("avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml")
};
app.Instance.Styles.Add(editStyle);

_initialized = true;
}
}
5 changes: 5 additions & 0 deletions src/EasyCon2.Avalonia.Core/EasyCon2.Avalonia.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
<PackageReference Include="CommunityToolkit.Mvvm" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia.AvaloniaEdit" />
</ItemGroup>

<ItemGroup>
<AvaloniaResource Include="VPad\Resources\*.png" />
<EmbeddedResource Include="Resources\Scripts\*.xshd" />
</ItemGroup>
</Project>
125 changes: 125 additions & 0 deletions src/EasyCon2.Avalonia.Core/Editor/CodeCompletionController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using Avalonia.Input;
using AvaloniaEdit;
using AvaloniaEdit.CodeCompletion;
using System.Diagnostics;

namespace EasyCon2.Avalonia.Core.Editor;

internal class CodeCompletionController : IDisposable
{
private readonly TextEditor _editor;
private readonly ICompletionProvider _completionProvider;
private bool _enableAutoCompletion;
private CompletionWindow _completionWindow;
private bool _isDisposed;

public bool EnableAutoCompletion
{
get => _enableAutoCompletion;
set => _enableAutoCompletion = value;
}

public CodeCompletionController(TextEditor editor, ICompletionProvider completionProvider, bool enableAutoCompletion = true)
{
_editor = editor ?? throw new ArgumentNullException(nameof(editor));
_completionProvider = completionProvider;
_enableAutoCompletion = enableAutoCompletion;

_editor.TextArea.TextEntering += OnTextEntering;
_editor.TextArea.TextEntered += OnTextEntered;
_editor.TextArea.KeyDown += OnKeyDown;
_editor.LostFocus += (_, _) => CloseCompletionWindow();
}

private async void OnTextEntered(object sender, TextInputEventArgs e)
{
if (!_enableAutoCompletion || string.IsNullOrEmpty(e.Text)) return;

var line = _editor.TextArea.Document.GetLineByNumber(_editor.TextArea.Caret.Line);
if (_completionProvider.ShouldTriggerCompletion(e.Text[0],
_editor.TextArea.Document.GetText(line.Offset, line.Length),
_editor.TextArea.Caret.Column))
{
await ShowCompletionWindow();
}
}

private void OnTextEntering(object sender, TextInputEventArgs e)
{
if (_completionWindow != null && e.Text is " " or "\t" or "\n" or "\r")
CloseCompletionWindow();
}

private void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Space && e.KeyModifiers.HasFlag(KeyModifiers.Control))
{
e.Handled = true;
_ = ShowCompletionWindow();
}
else if (e.Key == Key.Escape && _completionWindow != null)
{
CloseCompletionWindow();
e.Handled = true;
}
}

private async Task ShowCompletionWindow()
{
CloseCompletionWindow();

try
{
var currentWord = _completionProvider.GetCurrentWord(
_editor.Document,
_editor.TextArea.Caret.Offset
);

var completions = await _completionProvider.GetCompletions(
_editor.Document,
_editor.TextArea.Caret.Offset,
currentWord);

if (!completions.Any()) return;

_completionWindow = new CompletionWindow(_editor.TextArea)
{
CloseWhenCaretAtBeginning = true,
CloseAutomatically = true,
};

var data = _completionWindow.CompletionList.CompletionData;
foreach (var c in completions)
data.Add(c);

if (!string.IsNullOrEmpty(currentWord))
_completionWindow.StartOffset = _editor.TextArea.Caret.Offset - currentWord.Length;

_completionWindow.Closed += (_, _) => _completionWindow = null;
_completionWindow.Show();

if (!string.IsNullOrEmpty(currentWord))
_completionWindow.CompletionList.SelectItem(currentWord);
}
catch (Exception ex)
{
Debug.WriteLine($"显示代码提示时出错: {ex.Message}");
CloseCompletionWindow();
}
}

public void CloseCompletionWindow()
{
_completionWindow?.Close();
_completionWindow = null;
}

public void Dispose()
{
if (!_isDisposed)
{
CloseCompletionWindow();
_isDisposed = true;
}
}
}
26 changes: 26 additions & 0 deletions src/EasyCon2.Avalonia.Core/Editor/EcpCompletionData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Avalonia.Media;
using AvaloniaEdit.CodeCompletion;
using AvaloniaEdit.Document;
using AvaloniaEdit.Editing;

namespace EasyCon2.Avalonia.Core.Editor;

internal class EcpCompletionData : ICompletionData
{
public EcpCompletionData(string text) { Text = text; }

public IImage Image => null;

public string Text { get; }

public object Content => Text;

public object Description => $"描述:{Text}";

public double Priority => 0.9;

public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment, Text);
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
using AvaloniaEdit;
using AvaloniaEdit.CodeCompletion;
using AvaloniaEdit.Document;
using EasyCon.Core;
using EasyCon2.Models;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.CodeCompletion;
using ICSharpCode.AvalonEdit.Document;

namespace EasyCon2.Helper;
namespace EasyCon2.Avalonia.Core.Editor;

public delegate IEnumerable<string> GetImgLabel();
public interface ICompletionProvider
{
Task<IEnumerable<ICompletionData>> GetCompletions(ITextSource textSource, int offset, string cur);
bool ShouldTriggerCompletion(char triggerChar, string currentLineText, int caretIndex);
string GetCurrentWord(TextDocument document, int offset);
}

internal class EcpCompletionProvider(TextEditor textEditor) : ICompletionProvider
{
private readonly TextEditor Editor = textEditor;
public GetImgLabel GetImgLabel;
public Func<IEnumerable<string>> GetImgLabel;

private readonly List<string> _keywords = [
private readonly List<string> _keywords =
[
"IMPORT",
"IF", "ELIF", "ELSE", "ENDIF",
"FOR", "TO", "NEXT", "BREAK", "CONTINUE",
Expand All @@ -26,52 +31,43 @@ internal class EcpCompletionProvider(TextEditor textEditor) : ICompletionProvide
"LEFT", "RIGHT", "UP", "DOWN",
"UPLEFT", "UPRIGHT", "DOWNLEFT", "DOWNRIGHT",
"RAND", "AMIIBO", "BEEP",
];
public async Task<IEnumerable<ICompletionData>> GetCompletions(ITextSource textSource, int offset, string cur)
];

public Task<IEnumerable<ICompletionData>> GetCompletions(ITextSource textSource, int offset, string cur)
{
var completions = new List<ICompletionData>();

if (cur.StartsWith('@'))
{
var ilnames = GetImgLabel?.Invoke() ?? [];
foreach (var name in ilnames)
{
completions.Add(new EcpCompletionData($"@{name}"));
}
}
else if (cur.StartsWith('_') || cur.StartsWith('$'))
{
var tok = Scripter.GetTokens(Editor.Text, cur);
foreach (var name in tok)
{
completions.Add(new EcpCompletionData(name));
}
}
else if (char.IsLetter(cur[0]))
else if (cur.Length > 0 && char.IsLetter(cur[0]))
{
var kw = _keywords.Where(ch => ch.StartsWith(cur, StringComparison.OrdinalIgnoreCase));
foreach (var item in kw)
{
completions.Add(new EcpCompletionData(item));
}
}
return completions;
}

private bool IsWordPart(char c)
{
return char.IsLetterOrDigit(c) || "@$_".IndexOf(c) != -1;
return Task.FromResult<IEnumerable<ICompletionData>>(completions);
}

public string GetCurrentWord(TextDocument document, int offset)
{
int start = offset;
while (start > 0 && IsWordPart(document.GetCharAt(start - 1)))
{
start--;
}
return document.GetText(start, offset - start);
}

public bool ShouldTriggerCompletion(char triggerChar, string currentLineText, int caretIndex) => true;

private static bool IsWordPart(char c) => char.IsLetterOrDigit(c) || "@$_".IndexOf(c) != -1;
}
Loading