mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2025-01-11 23:57:21 -08:00
refactor: rewrite OpenAI integration
- use `OpenAI` and `Azure.AI.OpenAI` - use streaming response - re-design `AIAssistant`
This commit is contained in:
parent
f06b1d5d51
commit
3c5b9a353e
14 changed files with 375 additions and 322 deletions
|
@ -140,11 +140,11 @@ This software supports using OpenAI or other AI service that has an OpenAI comap
|
||||||
|
|
||||||
For `OpenAI`:
|
For `OpenAI`:
|
||||||
|
|
||||||
* `Server` must be `https://api.openai.com/v1/chat/completions`
|
* `Server` must be `https://api.openai.com/v1`
|
||||||
|
|
||||||
For other AI service:
|
For other AI service:
|
||||||
|
|
||||||
* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1/chat/completions`. For example, when using `Ollama`, it should be `http://localhost:11434/v1/chat/completions` instead of `http://localhost:11434/api/generate`
|
* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1`. For example, when using `Ollama`, it should be `http://localhost:11434/v1` instead of `http://localhost:11434/api/generate`
|
||||||
* The `API Key` is optional that depends on the service
|
* The `API Key` is optional that depends on the service
|
||||||
|
|
||||||
## External Tools
|
## External Tools
|
||||||
|
|
|
@ -46,8 +46,6 @@ namespace SourceGit
|
||||||
[JsonSerializable(typeof(Models.ExternalToolPaths))]
|
[JsonSerializable(typeof(Models.ExternalToolPaths))]
|
||||||
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
|
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
|
||||||
[JsonSerializable(typeof(Models.JetBrainsState))]
|
[JsonSerializable(typeof(Models.JetBrainsState))]
|
||||||
[JsonSerializable(typeof(Models.OpenAIChatRequest))]
|
|
||||||
[JsonSerializable(typeof(Models.OpenAIChatResponse))]
|
|
||||||
[JsonSerializable(typeof(Models.ThemeOverrides))]
|
[JsonSerializable(typeof(Models.ThemeOverrides))]
|
||||||
[JsonSerializable(typeof(Models.Version))]
|
[JsonSerializable(typeof(Models.Version))]
|
||||||
[JsonSerializable(typeof(Models.RepositorySettings))]
|
[JsonSerializable(typeof(Models.RepositorySettings))]
|
||||||
|
|
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
namespace SourceGit.Commands
|
namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -20,82 +22,75 @@ namespace SourceGit.Commands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onProgress)
|
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onResponse)
|
||||||
{
|
{
|
||||||
_service = service;
|
_service = service;
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_changes = changes;
|
_changes = changes;
|
||||||
_cancelToken = cancelToken;
|
_cancelToken = cancelToken;
|
||||||
_onProgress = onProgress;
|
_onResponse = onResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Result()
|
public void Exec()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var summarybuilder = new StringBuilder();
|
var responseBuilder = new StringBuilder();
|
||||||
var bodyBuilder = new StringBuilder();
|
var summaryBuilder = new StringBuilder();
|
||||||
foreach (var change in _changes)
|
foreach (var change in _changes)
|
||||||
{
|
{
|
||||||
if (_cancelToken.IsCancellationRequested)
|
if (_cancelToken.IsCancellationRequested)
|
||||||
return "";
|
return;
|
||||||
|
|
||||||
_onProgress?.Invoke($"Analyzing {change.Path}...");
|
responseBuilder.Append("- ");
|
||||||
|
summaryBuilder.Append("- ");
|
||||||
|
|
||||||
var summary = GenerateChangeSummary(change);
|
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
|
||||||
summarybuilder.Append("- ");
|
if (rs.IsSuccess)
|
||||||
summarybuilder.Append(summary);
|
{
|
||||||
summarybuilder.Append("(file: ");
|
_service.Chat(
|
||||||
summarybuilder.Append(change.Path);
|
_service.AnalyzeDiffPrompt,
|
||||||
summarybuilder.Append(")");
|
$"Here is the `git diff` output: {rs.StdOut}",
|
||||||
summarybuilder.AppendLine();
|
_cancelToken,
|
||||||
|
update =>
|
||||||
|
{
|
||||||
|
responseBuilder.Append(update);
|
||||||
|
summaryBuilder.Append(update);
|
||||||
|
_onResponse?.Invoke("Waiting for pre-file analyzing to complated...\n\n" + responseBuilder.ToString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bodyBuilder.Append("- ");
|
responseBuilder.Append("\n");
|
||||||
bodyBuilder.Append(summary);
|
summaryBuilder.Append("(file: ");
|
||||||
bodyBuilder.AppendLine();
|
summaryBuilder.Append(change.Path);
|
||||||
|
summaryBuilder.Append(")\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_cancelToken.IsCancellationRequested)
|
if (_cancelToken.IsCancellationRequested)
|
||||||
return "";
|
return;
|
||||||
|
|
||||||
_onProgress?.Invoke($"Generating commit message...");
|
var responseBody = responseBuilder.ToString();
|
||||||
|
var subjectBuilder = new StringBuilder();
|
||||||
var body = bodyBuilder.ToString();
|
_service.Chat(
|
||||||
var subject = GenerateSubject(summarybuilder.ToString());
|
_service.GenerateSubjectPrompt,
|
||||||
return string.Format("{0}\n\n{1}", subject, body);
|
$"Here are the summaries changes:\n{summaryBuilder}",
|
||||||
|
_cancelToken,
|
||||||
|
update =>
|
||||||
|
{
|
||||||
|
subjectBuilder.Append(update);
|
||||||
|
_onResponse?.Invoke($"{subjectBuilder}\n\n{responseBody}");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
App.RaiseException(_repo, $"Failed to generate commit message: {e}");
|
Dispatcher.UIThread.Post(() => App.RaiseException(_repo, $"Failed to generate commit message: {e}"));
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GenerateChangeSummary(Models.Change change)
|
|
||||||
{
|
|
||||||
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
|
|
||||||
var diff = rs.IsSuccess ? rs.StdOut : "unknown change";
|
|
||||||
|
|
||||||
var rsp = _service.Chat(_service.AnalyzeDiffPrompt, $"Here is the `git diff` output: {diff}", _cancelToken);
|
|
||||||
if (rsp != null && rsp.Choices.Count > 0)
|
|
||||||
return rsp.Choices[0].Message.Content;
|
|
||||||
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GenerateSubject(string summary)
|
|
||||||
{
|
|
||||||
var rsp = _service.Chat(_service.GenerateSubjectPrompt, $"Here are the summaries changes:\n{summary}", _cancelToken);
|
|
||||||
if (rsp != null && rsp.Choices.Count > 0)
|
|
||||||
return rsp.Choices[0].Message.Content;
|
|
||||||
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Models.OpenAIService _service;
|
private Models.OpenAIService _service;
|
||||||
private string _repo;
|
private string _repo;
|
||||||
private List<Models.Change> _changes;
|
private List<Models.Change> _changes;
|
||||||
private CancellationToken _cancelToken;
|
private CancellationToken _cancelToken;
|
||||||
private Action<string> _onProgress;
|
private Action<string> _onResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,81 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.ClientModel;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Azure.AI.OpenAI;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using OpenAI;
|
||||||
|
using OpenAI.Chat;
|
||||||
|
|
||||||
namespace SourceGit.Models
|
namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
public class OpenAIChatMessage
|
|
||||||
{
|
|
||||||
[JsonPropertyName("role")]
|
|
||||||
public string Role
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonPropertyName("content")]
|
|
||||||
public string Content
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OpenAIChatChoice
|
|
||||||
{
|
|
||||||
[JsonPropertyName("index")]
|
|
||||||
public int Index
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonPropertyName("message")]
|
|
||||||
public OpenAIChatMessage Message
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OpenAIChatResponse
|
|
||||||
{
|
|
||||||
[JsonPropertyName("choices")]
|
|
||||||
public List<OpenAIChatChoice> Choices
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
} = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OpenAIChatRequest
|
|
||||||
{
|
|
||||||
[JsonPropertyName("model")]
|
|
||||||
public string Model
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonPropertyName("messages")]
|
|
||||||
public List<OpenAIChatMessage> Messages
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
} = [];
|
|
||||||
|
|
||||||
public void AddMessage(string role, string content)
|
|
||||||
{
|
|
||||||
Messages.Add(new OpenAIChatMessage { Role = role, Content = content });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OpenAIService : ObservableObject
|
public class OpenAIService : ObservableObject
|
||||||
{
|
{
|
||||||
public string Name
|
public string Name
|
||||||
|
@ -147,45 +79,39 @@ namespace SourceGit.Models
|
||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
public OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation)
|
public void Chat(string prompt, string question, CancellationToken cancellation, Action<string> onUpdate)
|
||||||
{
|
{
|
||||||
var chat = new OpenAIChatRequest() { Model = Model };
|
Uri server = new(Server);
|
||||||
chat.AddMessage("user", prompt);
|
ApiKeyCredential key = new(ApiKey);
|
||||||
chat.AddMessage("user", question);
|
ChatClient client = null;
|
||||||
|
if (Server.Contains("openai.azure.com/", StringComparison.Ordinal))
|
||||||
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) };
|
|
||||||
if (!string.IsNullOrEmpty(ApiKey))
|
|
||||||
{
|
{
|
||||||
if (Server.Contains("openai.azure.com/", StringComparison.Ordinal))
|
var azure = new AzureOpenAIClient(server, key);
|
||||||
client.DefaultRequestHeaders.Add("api-key", ApiKey);
|
client = azure.GetChatClient(Model);
|
||||||
else
|
}
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
|
else
|
||||||
|
{
|
||||||
|
var openai = new OpenAIClient(key, new() { Endpoint = server });
|
||||||
|
client = openai.GetChatClient(Model);
|
||||||
}
|
}
|
||||||
|
|
||||||
var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest), Encoding.UTF8, "application/json");
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var task = client.PostAsync(Server, req, cancellation);
|
var updates = client.CompleteChatStreaming([
|
||||||
task.Wait(cancellation);
|
new UserChatMessage(prompt),
|
||||||
|
new UserChatMessage(question),
|
||||||
|
], null, cancellation);
|
||||||
|
|
||||||
var rsp = task.Result;
|
foreach (var update in updates)
|
||||||
var reader = rsp.Content.ReadAsStringAsync(cancellation);
|
|
||||||
reader.Wait(cancellation);
|
|
||||||
|
|
||||||
var body = reader.Result;
|
|
||||||
if (!rsp.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
throw new Exception($"AI service returns error code {rsp.StatusCode}. Body: {body ?? string.Empty}");
|
if (update.ContentUpdate.Count > 0)
|
||||||
|
onUpdate.Invoke(update.ContentUpdate[0].Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
if (cancellation.IsCancellationRequested)
|
if (!cancellation.IsCancellationRequested)
|
||||||
return null;
|
throw;
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">Track Branch:</x:String>
|
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">Track Branch:</x:String>
|
||||||
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">Tracking remote branch</x:String>
|
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">Tracking remote branch</x:String>
|
||||||
<x:String x:Key="Text.AIAssistant" xml:space="preserve">AI Assistant</x:String>
|
<x:String x:Key="Text.AIAssistant" xml:space="preserve">AI Assistant</x:String>
|
||||||
|
<x:String x:Key="Text.AIAssistant.Regen" xml:space="preserve">RE-GENERATE</x:String>
|
||||||
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">Use AI to generate commit message</x:String>
|
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">Use AI to generate commit message</x:String>
|
||||||
|
<x:String x:Key="Text.AIAssistant.Use" xml:space="preserve">APPLY AS COMMIT MESSAGE</x:String>
|
||||||
<x:String x:Key="Text.Apply" xml:space="preserve">Patch</x:String>
|
<x:String x:Key="Text.Apply" xml:space="preserve">Patch</x:String>
|
||||||
<x:String x:Key="Text.Apply.Error" xml:space="preserve">Error</x:String>
|
<x:String x:Key="Text.Apply.Error" xml:space="preserve">Error</x:String>
|
||||||
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">Raise errors and refuses to apply the patch</x:String>
|
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">Raise errors and refuses to apply the patch</x:String>
|
||||||
|
|
|
@ -22,7 +22,9 @@
|
||||||
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">跟踪分支</x:String>
|
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">跟踪分支</x:String>
|
||||||
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">设置上游跟踪分支</x:String>
|
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">设置上游跟踪分支</x:String>
|
||||||
<x:String x:Key="Text.AIAssistant" xml:space="preserve">AI助手</x:String>
|
<x:String x:Key="Text.AIAssistant" xml:space="preserve">AI助手</x:String>
|
||||||
|
<x:String x:Key="Text.AIAssistant.Regen" xml:space="preserve">重新生成</x:String>
|
||||||
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用AI助手生成提交信息</x:String>
|
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用AI助手生成提交信息</x:String>
|
||||||
|
<x:String x:Key="Text.AIAssistant.Use" xml:space="preserve">应用本次生成</x:String>
|
||||||
<x:String x:Key="Text.Apply" xml:space="preserve">应用补丁(apply)</x:String>
|
<x:String x:Key="Text.Apply" xml:space="preserve">应用补丁(apply)</x:String>
|
||||||
<x:String x:Key="Text.Apply.Error" xml:space="preserve">错误</x:String>
|
<x:String x:Key="Text.Apply.Error" xml:space="preserve">错误</x:String>
|
||||||
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">输出错误,并终止应用补丁</x:String>
|
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">输出错误,并终止应用补丁</x:String>
|
||||||
|
|
|
@ -22,7 +22,9 @@
|
||||||
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">追蹤分支</x:String>
|
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">追蹤分支</x:String>
|
||||||
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">設定遠端追蹤分支</x:String>
|
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">設定遠端追蹤分支</x:String>
|
||||||
<x:String x:Key="Text.AIAssistant" xml:space="preserve">AI 助理</x:String>
|
<x:String x:Key="Text.AIAssistant" xml:space="preserve">AI 助理</x:String>
|
||||||
|
<x:String x:Key="Text.AIAssistant.Regen" xml:space="preserve">重新產生</x:String>
|
||||||
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用 AI 產生提交訊息</x:String>
|
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用 AI 產生提交訊息</x:String>
|
||||||
|
<x:String x:Key="Text.AIAssistant.Use" xml:space="preserve">套用為提交訊息</x:String>
|
||||||
<x:String x:Key="Text.Apply" xml:space="preserve">套用修補檔 (apply patch)</x:String>
|
<x:String x:Key="Text.Apply" xml:space="preserve">套用修補檔 (apply patch)</x:String>
|
||||||
<x:String x:Key="Text.Apply.Error" xml:space="preserve">錯誤</x:String>
|
<x:String x:Key="Text.Apply.Error" xml:space="preserve">錯誤</x:String>
|
||||||
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">輸出錯誤,並中止套用修補檔</x:String>
|
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">輸出錯誤,並中止套用修補檔</x:String>
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
<PublishAot>true</PublishAot>
|
<PublishAot>true</PublishAot>
|
||||||
<PublishTrimmed>true</PublishTrimmed>
|
<PublishTrimmed>true</PublishTrimmed>
|
||||||
<TrimMode>link</TrimMode>
|
<TrimMode>link</TrimMode>
|
||||||
|
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(DisableUpdateDetection)' == 'true'">
|
<PropertyGroup Condition="'$(DisableUpdateDetection)' == 'true'">
|
||||||
|
@ -48,8 +49,10 @@
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.3" Condition="'$(Configuration)' == 'Debug'" />
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.3" Condition="'$(Configuration)' == 'Debug'" />
|
||||||
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.1.0" />
|
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.1.0" />
|
||||||
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
|
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
|
||||||
|
<PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
|
||||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-rc4.5" />
|
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-rc4.5" />
|
||||||
|
<PackageReference Include="OpenAI" Version="2.1.0" />
|
||||||
<PackageReference Include="TextMateSharp" Version="1.0.65" />
|
<PackageReference Include="TextMateSharp" Version="1.0.65" />
|
||||||
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.65" />
|
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.65" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -1162,6 +1162,25 @@ namespace SourceGit.ViewModels
|
||||||
App.GetLauncer()?.OpenRepositoryInTab(node, null);
|
App.GetLauncer()?.OpenRepositoryInTab(node, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AvaloniaList<Models.OpenAIService> GetPreferedOpenAIServices()
|
||||||
|
{
|
||||||
|
var services = Preference.Instance.OpenAIServices;
|
||||||
|
if (services == null || services.Count == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
if (services.Count == 1)
|
||||||
|
return services;
|
||||||
|
|
||||||
|
var prefered = _settings.PreferedOpenAIService;
|
||||||
|
foreach (var service in services)
|
||||||
|
{
|
||||||
|
if (service.Name.Equals(prefered, StringComparison.Ordinal))
|
||||||
|
return [service];
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
public ContextMenu CreateContextMenuForGitFlow()
|
public ContextMenu CreateContextMenuForGitFlow()
|
||||||
{
|
{
|
||||||
var menu = new ContextMenu();
|
var menu = new ContextMenu();
|
||||||
|
|
|
@ -1056,7 +1056,7 @@ namespace SourceGit.ViewModels
|
||||||
var menu = new ContextMenu();
|
var menu = new ContextMenu();
|
||||||
|
|
||||||
var ai = null as MenuItem;
|
var ai = null as MenuItem;
|
||||||
var services = GetPreferedOpenAIServices();
|
var services = _repo.GetPreferedOpenAIServices();
|
||||||
if (services.Count > 0)
|
if (services.Count > 0)
|
||||||
{
|
{
|
||||||
ai = new MenuItem();
|
ai = new MenuItem();
|
||||||
|
@ -1067,7 +1067,7 @@ namespace SourceGit.ViewModels
|
||||||
{
|
{
|
||||||
ai.Click += (_, e) =>
|
ai.Click += (_, e) =>
|
||||||
{
|
{
|
||||||
var dialog = new Views.AIAssistant(services[0], _repo.FullPath, _selectedStaged, generated => CommitMessage = generated);
|
var dialog = new Views.AIAssistant(services[0], _repo.FullPath, this, _selectedStaged);
|
||||||
App.OpenDialog(dialog);
|
App.OpenDialog(dialog);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
};
|
};
|
||||||
|
@ -1082,7 +1082,7 @@ namespace SourceGit.ViewModels
|
||||||
item.Header = service.Name;
|
item.Header = service.Name;
|
||||||
item.Click += (_, e) =>
|
item.Click += (_, e) =>
|
||||||
{
|
{
|
||||||
var dialog = new Views.AIAssistant(dup, _repo.FullPath, _selectedStaged, generated => CommitMessage = generated);
|
var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _selectedStaged);
|
||||||
App.OpenDialog(dialog);
|
App.OpenDialog(dialog);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
};
|
};
|
||||||
|
@ -1447,7 +1447,7 @@ namespace SourceGit.ViewModels
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var services = GetPreferedOpenAIServices();
|
var services = _repo.GetPreferedOpenAIServices();
|
||||||
if (services.Count == 0)
|
if (services.Count == 0)
|
||||||
{
|
{
|
||||||
App.RaiseException(_repo.FullPath, "Bad configuration for OpenAI");
|
App.RaiseException(_repo.FullPath, "Bad configuration for OpenAI");
|
||||||
|
@ -1456,7 +1456,7 @@ namespace SourceGit.ViewModels
|
||||||
|
|
||||||
if (services.Count == 1)
|
if (services.Count == 1)
|
||||||
{
|
{
|
||||||
var dialog = new Views.AIAssistant(services[0], _repo.FullPath, _staged, generated => CommitMessage = generated);
|
var dialog = new Views.AIAssistant(services[0], _repo.FullPath, this, _staged);
|
||||||
App.OpenDialog(dialog);
|
App.OpenDialog(dialog);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1472,7 +1472,7 @@ namespace SourceGit.ViewModels
|
||||||
item.Header = service.Name;
|
item.Header = service.Name;
|
||||||
item.Click += (_, e) =>
|
item.Click += (_, e) =>
|
||||||
{
|
{
|
||||||
var dialog = new Views.AIAssistant(dup, _repo.FullPath, _staged, generated => CommitMessage = generated);
|
var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _staged);
|
||||||
App.OpenDialog(dialog);
|
App.OpenDialog(dialog);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
};
|
};
|
||||||
|
@ -1588,25 +1588,6 @@ namespace SourceGit.ViewModels
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IList<Models.OpenAIService> GetPreferedOpenAIServices()
|
|
||||||
{
|
|
||||||
var services = Preference.Instance.OpenAIServices;
|
|
||||||
if (services == null || services.Count == 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
if (services.Count == 1)
|
|
||||||
return services;
|
|
||||||
|
|
||||||
var prefered = _repo.Settings.PreferedOpenAIService;
|
|
||||||
foreach (var service in services)
|
|
||||||
{
|
|
||||||
if (service.Name.Equals(prefered, StringComparison.Ordinal))
|
|
||||||
return [service];
|
|
||||||
}
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Repository _repo = null;
|
private Repository _repo = null;
|
||||||
private bool _isLoadingData = false;
|
private bool _isLoadingData = false;
|
||||||
private bool _isStaging = false;
|
private bool _isStaging = false;
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
x:Name="ThisControl"
|
x:Name="ThisControl"
|
||||||
Icon="/App.ico"
|
Icon="/App.ico"
|
||||||
Title="{DynamicResource Text.AIAssistant}"
|
Title="{DynamicResource Text.AIAssistant}"
|
||||||
Width="400" SizeToContent="Height"
|
Width="520" SizeToContent="Height"
|
||||||
CanResize="False"
|
CanResize="False"
|
||||||
WindowStartupLocation="CenterOwner">
|
WindowStartupLocation="CenterOwner">
|
||||||
<Grid RowDefinitions="Auto,Auto,Auto">
|
<Grid RowDefinitions="Auto,Auto,Auto">
|
||||||
|
@ -36,18 +36,33 @@
|
||||||
IsVisible="{OnPlatform True, macOS=False}"/>
|
IsVisible="{OnPlatform True, macOS=False}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Animated Icon -->
|
<!-- AI response -->
|
||||||
<v:LoadingIcon Grid.Row="1"
|
<v:AIResponseView Grid.Row="1"
|
||||||
Width="24" Height="24"
|
x:Name="TxtResponse"
|
||||||
Margin="0,16,0,0"/>
|
Margin="8"
|
||||||
|
Height="320"
|
||||||
|
BorderThickness="1"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border2}"
|
||||||
|
Background="{DynamicResource Brush.Contents}"/>
|
||||||
|
|
||||||
<!-- Message -->
|
<!-- Options -->
|
||||||
<TextBlock Grid.Row="2"
|
<Border Grid.Row="2" Margin="0,0,0,8">
|
||||||
x:Name="ProgressMessage"
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
Margin="16"
|
<v:LoadingIcon x:Name="IconInProgress" Width="14" Height="14" Margin="0,0,8,0"/>
|
||||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Decrease}}"
|
<Button Classes="flat"
|
||||||
HorizontalAlignment="Center"
|
x:Name="BtnGenerateCommitMessage"
|
||||||
Text="Generating commit message... Please wait!"
|
Height="28"
|
||||||
TextTrimming="CharacterEllipsis"/>
|
Margin="0,0,8,0"
|
||||||
|
Padding="12,0"
|
||||||
|
Content="{DynamicResource Text.AIAssistant.Use}"
|
||||||
|
Click="OnGenerateCommitMessage"/>
|
||||||
|
<Button Classes="flat"
|
||||||
|
x:Name="BtnRegenerate"
|
||||||
|
Height="28"
|
||||||
|
Padding="12,0"
|
||||||
|
Content="{DynamicResource Text.AIAssistant.Regen}"
|
||||||
|
Click="OnRegen"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</v:ChromelessWindow>
|
</v:ChromelessWindow>
|
||||||
|
|
|
@ -3,26 +3,112 @@ using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
using AvaloniaEdit.Document;
|
||||||
|
using AvaloniaEdit.Editing;
|
||||||
|
using AvaloniaEdit.TextMate;
|
||||||
|
using AvaloniaEdit;
|
||||||
|
|
||||||
namespace SourceGit.Views
|
namespace SourceGit.Views
|
||||||
{
|
{
|
||||||
|
public class AIResponseView : TextEditor
|
||||||
|
{
|
||||||
|
protected override Type StyleKeyOverride => typeof(TextEditor);
|
||||||
|
|
||||||
|
public AIResponseView() : base(new TextArea(), new TextDocument())
|
||||||
|
{
|
||||||
|
IsReadOnly = true;
|
||||||
|
ShowLineNumbers = false;
|
||||||
|
WordWrap = true;
|
||||||
|
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
|
||||||
|
VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
|
||||||
|
|
||||||
|
TextArea.TextView.Margin = new Thickness(4, 0);
|
||||||
|
TextArea.TextView.Options.EnableHyperlinks = false;
|
||||||
|
TextArea.TextView.Options.EnableEmailHyperlinks = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLoaded(RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnLoaded(e);
|
||||||
|
|
||||||
|
TextArea.TextView.ContextRequested += OnTextViewContextRequested;
|
||||||
|
|
||||||
|
if (_textMate == null)
|
||||||
|
{
|
||||||
|
_textMate = Models.TextMateHelper.CreateForEditor(this);
|
||||||
|
Models.TextMateHelper.SetGrammarByFileName(_textMate, "README.md");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUnloaded(RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnUnloaded(e);
|
||||||
|
|
||||||
|
TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
|
||||||
|
|
||||||
|
if (_textMate != null)
|
||||||
|
{
|
||||||
|
_textMate.Dispose();
|
||||||
|
_textMate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.Collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e)
|
||||||
|
{
|
||||||
|
var selected = SelectedText;
|
||||||
|
if (string.IsNullOrEmpty(selected))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var copy = new MenuItem() { Header = App.Text("Copy") };
|
||||||
|
copy.Click += (_, ev) =>
|
||||||
|
{
|
||||||
|
App.CopyText(selected);
|
||||||
|
ev.Handled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.FindResource("Icons.Copy") is Geometry geo)
|
||||||
|
{
|
||||||
|
copy.Icon = new Avalonia.Controls.Shapes.Path()
|
||||||
|
{
|
||||||
|
Width = 10,
|
||||||
|
Height = 10,
|
||||||
|
Stretch = Stretch.Uniform,
|
||||||
|
Data = geo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var menu = new ContextMenu();
|
||||||
|
menu.Items.Add(copy);
|
||||||
|
menu.Open(TextArea.TextView);
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextMate.Installation _textMate = null;
|
||||||
|
}
|
||||||
|
|
||||||
public partial class AIAssistant : ChromelessWindow
|
public partial class AIAssistant : ChromelessWindow
|
||||||
{
|
{
|
||||||
public AIAssistant()
|
public AIAssistant()
|
||||||
{
|
{
|
||||||
_cancel = new CancellationTokenSource();
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public AIAssistant(Models.OpenAIService service, string repo, List<Models.Change> changes, Action<string> onDone)
|
public AIAssistant(Models.OpenAIService service, string repo, ViewModels.WorkingCopy wc, List<Models.Change> changes)
|
||||||
{
|
{
|
||||||
_service = service;
|
_service = service;
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
|
_wc = wc;
|
||||||
_changes = changes;
|
_changes = changes;
|
||||||
_onDone = onDone;
|
|
||||||
_cancel = new CancellationTokenSource();
|
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
@ -30,39 +116,63 @@ namespace SourceGit.Views
|
||||||
protected override void OnOpened(EventArgs e)
|
protected override void OnOpened(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnOpened(e);
|
base.OnOpened(e);
|
||||||
|
Generate();
|
||||||
if (string.IsNullOrEmpty(_repo))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
var message = new Commands.GenerateCommitMessage(_service, _repo, _changes, _cancel.Token, SetDescription).Result();
|
|
||||||
if (_cancel.IsCancellationRequested)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Dispatcher.UIThread.Invoke(() =>
|
|
||||||
{
|
|
||||||
_onDone?.Invoke(message);
|
|
||||||
Close();
|
|
||||||
});
|
|
||||||
}, _cancel.Token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnClosing(WindowClosingEventArgs e)
|
protected override void OnClosing(WindowClosingEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnClosing(e);
|
base.OnClosing(e);
|
||||||
_cancel.Cancel();
|
_cancel?.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetDescription(string message)
|
private void OnGenerateCommitMessage(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Invoke(() => ProgressMessage.Text = message);
|
if (_wc != null)
|
||||||
|
_wc.CommitMessage = TxtResponse.Text;
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRegen(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
TxtResponse.Text = string.Empty;
|
||||||
|
Generate();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Generate()
|
||||||
|
{
|
||||||
|
if (_repo == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IconInProgress.IsVisible = true;
|
||||||
|
BtnGenerateCommitMessage.IsEnabled = false;
|
||||||
|
BtnRegenerate.IsEnabled = false;
|
||||||
|
|
||||||
|
_cancel = new CancellationTokenSource();
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
new Commands.GenerateCommitMessage(_service, _repo, _changes, _cancel.Token, message =>
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Invoke(() => TxtResponse.Text = message);
|
||||||
|
}).Exec();
|
||||||
|
|
||||||
|
if (!_cancel.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Invoke(() =>
|
||||||
|
{
|
||||||
|
IconInProgress.IsVisible = false;
|
||||||
|
BtnGenerateCommitMessage.IsEnabled = true;
|
||||||
|
BtnRegenerate.IsEnabled = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, _cancel.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Models.OpenAIService _service;
|
private Models.OpenAIService _service;
|
||||||
private string _repo;
|
private string _repo;
|
||||||
|
private ViewModels.WorkingCopy _wc;
|
||||||
private List<Models.Change> _changes;
|
private List<Models.Change> _changes;
|
||||||
private Action<string> _onDone;
|
|
||||||
private CancellationTokenSource _cancel;
|
private CancellationTokenSource _cancel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,117 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
using AvaloniaEdit;
|
||||||
|
using AvaloniaEdit.Document;
|
||||||
|
using AvaloniaEdit.Editing;
|
||||||
|
using AvaloniaEdit.TextMate;
|
||||||
|
|
||||||
namespace SourceGit.Views
|
namespace SourceGit.Views
|
||||||
{
|
{
|
||||||
|
public class RevisionTextFileView : TextEditor
|
||||||
|
{
|
||||||
|
protected override Type StyleKeyOverride => typeof(TextEditor);
|
||||||
|
|
||||||
|
public RevisionTextFileView() : base(new TextArea(), new TextDocument())
|
||||||
|
{
|
||||||
|
IsReadOnly = true;
|
||||||
|
ShowLineNumbers = true;
|
||||||
|
WordWrap = false;
|
||||||
|
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
|
||||||
|
VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
|
||||||
|
|
||||||
|
TextArea.LeftMargins[0].Margin = new Thickness(8, 0);
|
||||||
|
TextArea.TextView.Margin = new Thickness(4, 0);
|
||||||
|
TextArea.TextView.Options.EnableHyperlinks = false;
|
||||||
|
TextArea.TextView.Options.EnableEmailHyperlinks = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLoaded(RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnLoaded(e);
|
||||||
|
|
||||||
|
TextArea.TextView.ContextRequested += OnTextViewContextRequested;
|
||||||
|
UpdateTextMate();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUnloaded(RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnUnloaded(e);
|
||||||
|
|
||||||
|
TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
|
||||||
|
|
||||||
|
if (_textMate != null)
|
||||||
|
{
|
||||||
|
_textMate.Dispose();
|
||||||
|
_textMate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.Collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
|
||||||
|
if (DataContext is Models.RevisionTextFile source)
|
||||||
|
{
|
||||||
|
UpdateTextMate();
|
||||||
|
Text = source.Content;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Text = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e)
|
||||||
|
{
|
||||||
|
var selected = SelectedText;
|
||||||
|
if (string.IsNullOrEmpty(selected))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var copy = new MenuItem() { Header = App.Text("Copy") };
|
||||||
|
copy.Click += (_, ev) =>
|
||||||
|
{
|
||||||
|
App.CopyText(selected);
|
||||||
|
ev.Handled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.FindResource("Icons.Copy") is Geometry geo)
|
||||||
|
{
|
||||||
|
copy.Icon = new Avalonia.Controls.Shapes.Path()
|
||||||
|
{
|
||||||
|
Width = 10,
|
||||||
|
Height = 10,
|
||||||
|
Stretch = Stretch.Uniform,
|
||||||
|
Data = geo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var menu = new ContextMenu();
|
||||||
|
menu.Items.Add(copy);
|
||||||
|
menu.Open(TextArea.TextView);
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTextMate()
|
||||||
|
{
|
||||||
|
if (_textMate == null)
|
||||||
|
_textMate = Models.TextMateHelper.CreateForEditor(this);
|
||||||
|
|
||||||
|
if (DataContext is Models.RevisionTextFile file)
|
||||||
|
Models.TextMateHelper.SetGrammarByFileName(_textMate, file.FileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextMate.Installation _textMate = null;
|
||||||
|
}
|
||||||
|
|
||||||
public partial class RevisionFileContentViewer : UserControl
|
public partial class RevisionFileContentViewer : UserControl
|
||||||
{
|
{
|
||||||
public RevisionFileContentViewer()
|
public RevisionFileContentViewer()
|
||||||
|
|
|
@ -1,118 +1,8 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Primitives;
|
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Media;
|
|
||||||
|
|
||||||
using AvaloniaEdit;
|
|
||||||
using AvaloniaEdit.Document;
|
|
||||||
using AvaloniaEdit.Editing;
|
|
||||||
using AvaloniaEdit.TextMate;
|
|
||||||
|
|
||||||
namespace SourceGit.Views
|
namespace SourceGit.Views
|
||||||
{
|
{
|
||||||
public class RevisionTextFileView : TextEditor
|
|
||||||
{
|
|
||||||
protected override Type StyleKeyOverride => typeof(TextEditor);
|
|
||||||
|
|
||||||
public RevisionTextFileView() : base(new TextArea(), new TextDocument())
|
|
||||||
{
|
|
||||||
IsReadOnly = true;
|
|
||||||
ShowLineNumbers = true;
|
|
||||||
WordWrap = false;
|
|
||||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
|
|
||||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
|
|
||||||
|
|
||||||
TextArea.LeftMargins[0].Margin = new Thickness(8, 0);
|
|
||||||
TextArea.TextView.Margin = new Thickness(4, 0);
|
|
||||||
TextArea.TextView.Options.EnableHyperlinks = false;
|
|
||||||
TextArea.TextView.Options.EnableEmailHyperlinks = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnLoaded(RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnLoaded(e);
|
|
||||||
|
|
||||||
TextArea.TextView.ContextRequested += OnTextViewContextRequested;
|
|
||||||
UpdateTextMate();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnUnloaded(RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnUnloaded(e);
|
|
||||||
|
|
||||||
TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
|
|
||||||
|
|
||||||
if (_textMate != null)
|
|
||||||
{
|
|
||||||
_textMate.Dispose();
|
|
||||||
_textMate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
GC.Collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDataContextChanged(EventArgs e)
|
|
||||||
{
|
|
||||||
base.OnDataContextChanged(e);
|
|
||||||
|
|
||||||
if (DataContext is Models.RevisionTextFile source)
|
|
||||||
{
|
|
||||||
UpdateTextMate();
|
|
||||||
Text = source.Content;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Text = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e)
|
|
||||||
{
|
|
||||||
var selected = SelectedText;
|
|
||||||
if (string.IsNullOrEmpty(selected))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var copy = new MenuItem() { Header = App.Text("Copy") };
|
|
||||||
copy.Click += (_, ev) =>
|
|
||||||
{
|
|
||||||
App.CopyText(selected);
|
|
||||||
ev.Handled = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.FindResource("Icons.Copy") is Geometry geo)
|
|
||||||
{
|
|
||||||
copy.Icon = new Avalonia.Controls.Shapes.Path()
|
|
||||||
{
|
|
||||||
Width = 10,
|
|
||||||
Height = 10,
|
|
||||||
Stretch = Stretch.Uniform,
|
|
||||||
Data = geo,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var menu = new ContextMenu();
|
|
||||||
menu.Items.Add(copy);
|
|
||||||
menu.Open(TextArea.TextView);
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateTextMate()
|
|
||||||
{
|
|
||||||
if (_textMate == null)
|
|
||||||
_textMate = Models.TextMateHelper.CreateForEditor(this);
|
|
||||||
|
|
||||||
if (DataContext is Models.RevisionTextFile file)
|
|
||||||
Models.TextMateHelper.SetGrammarByFileName(_textMate, file.FileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TextMate.Installation _textMate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class RevisionFiles : UserControl
|
public partial class RevisionFiles : UserControl
|
||||||
{
|
{
|
||||||
public RevisionFiles()
|
public RevisionFiles()
|
||||||
|
|
Loading…
Reference in a new issue