logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
101
crates/edit_prediction/Cargo.toml
Normal file
101
crates/edit_prediction/Cargo.toml
Normal file
@@ -0,0 +1,101 @@
|
||||
[package]
|
||||
name = "edit_prediction"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/edit_prediction.rs"
|
||||
|
||||
[features]
|
||||
cli-support = []
|
||||
|
||||
[dependencies]
|
||||
ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
heapless.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
cloud_api_client.workspace = true
|
||||
cloud_api_types.workspace = true
|
||||
cloud_llm_client = { workspace = true, features = ["predict-edits"] }
|
||||
collections.workspace = true
|
||||
copilot.workspace = true
|
||||
copilot_ui.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
db.workspace = true
|
||||
edit_prediction_types.workspace = true
|
||||
edit_prediction_context.workspace = true
|
||||
edit_prediction_metrics.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
open_ai.workspace = true
|
||||
postage.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
project.workspace = true
|
||||
pulldown-cmark.workspace = true
|
||||
rand.workspace = true
|
||||
regex.workspace = true
|
||||
release_channel.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
toml.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
worktree.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_credentials_provider.workspace = true
|
||||
zeta_prompt.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
criterion.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
languages = { workspace = true, features = ["load-grammars"] }
|
||||
node_runtime.workspace = true
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
cloud_llm_client = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
lsp.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
zlog.workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "kept_rate"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "ts_error_count"
|
||||
harness = false
|
||||
1
crates/edit_prediction/LICENSE-GPL
Symbolic link
1
crates/edit_prediction/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
128
crates/edit_prediction/benches/kept_rate.rs
Normal file
128
crates/edit_prediction/benches/kept_rate.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
|
||||
use edit_prediction::metrics::compute_kept_rate;
|
||||
|
||||
fn repeated_function_lines(line_count: usize) -> String {
|
||||
let mut text = String::with_capacity(line_count * 32);
|
||||
for index in 0..line_count {
|
||||
text.push_str("fn helper_");
|
||||
text.push_str(&(index % 16).to_string());
|
||||
text.push_str("() { value += old_name + 1; }\n");
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
fn localized_rename_inputs(line_count: usize) -> (String, String, String) {
|
||||
let base = repeated_function_lines(line_count);
|
||||
let mut predicted = base.clone();
|
||||
let mut final_text = base.clone();
|
||||
|
||||
let needle = "value += old_name + 1;";
|
||||
let prediction = "value += very_long_predicted_name + 1;";
|
||||
let accepted = "value += new_name + 1;";
|
||||
|
||||
let offset = base
|
||||
.rfind(needle)
|
||||
.expect("expected needle in synthetic input");
|
||||
let end = offset + needle.len();
|
||||
|
||||
predicted.replace_range(offset..end, prediction);
|
||||
final_text.replace_range(offset..end, accepted);
|
||||
|
||||
(base, predicted, final_text)
|
||||
}
|
||||
|
||||
fn identical_new_content_inputs(line_count: usize) -> (String, String, String) {
|
||||
let predicted = repeated_function_lines(line_count);
|
||||
(String::new(), predicted.clone(), predicted)
|
||||
}
|
||||
|
||||
fn repetitive_token_inputs(token_repetitions: usize) -> (String, String, String) {
|
||||
let repeated_old = "foo + foo + foo + foo + foo\n".repeat(token_repetitions);
|
||||
let repeated_predicted = "foo + foo + prediction_token + foo + foo\n".repeat(token_repetitions);
|
||||
let repeated_final = "foo + foo + kept_token + foo + foo\n".repeat(token_repetitions);
|
||||
(repeated_old, repeated_predicted, repeated_final)
|
||||
}
|
||||
|
||||
fn kept_rate_benchmark(c: &mut Criterion) {
|
||||
let mut no_change_group = c.benchmark_group("kept_rate/no_change");
|
||||
for line_count in [128usize, 512, 2048] {
|
||||
let text = repeated_function_lines(line_count);
|
||||
no_change_group.bench_with_input(
|
||||
BenchmarkId::new("lines", line_count),
|
||||
&text,
|
||||
|bench, text| {
|
||||
bench.iter(|| {
|
||||
black_box(compute_kept_rate(
|
||||
black_box(text),
|
||||
black_box(text),
|
||||
black_box(text),
|
||||
));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
no_change_group.finish();
|
||||
|
||||
let mut localized_group = c.benchmark_group("kept_rate/localized_rename");
|
||||
for line_count in [128usize, 512, 2048] {
|
||||
let inputs = localized_rename_inputs(line_count);
|
||||
localized_group.bench_with_input(
|
||||
BenchmarkId::new("lines", line_count),
|
||||
&inputs,
|
||||
|bench, inputs| {
|
||||
let (base, predicted, final_text) = inputs;
|
||||
bench.iter(|| {
|
||||
black_box(compute_kept_rate(
|
||||
black_box(base),
|
||||
black_box(predicted),
|
||||
black_box(final_text),
|
||||
));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
localized_group.finish();
|
||||
|
||||
let mut addition_group = c.benchmark_group("kept_rate/identical_addition");
|
||||
for line_count in [128usize, 512, 2048] {
|
||||
let inputs = identical_new_content_inputs(line_count);
|
||||
addition_group.bench_with_input(
|
||||
BenchmarkId::new("lines", line_count),
|
||||
&inputs,
|
||||
|bench, inputs| {
|
||||
let (base, predicted, final_text) = inputs;
|
||||
bench.iter(|| {
|
||||
black_box(compute_kept_rate(
|
||||
black_box(base),
|
||||
black_box(predicted),
|
||||
black_box(final_text),
|
||||
));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
addition_group.finish();
|
||||
|
||||
let mut repetitive_group = c.benchmark_group("kept_rate/repetitive_tokens");
|
||||
for token_repetitions in [64usize, 256, 1024] {
|
||||
let inputs = repetitive_token_inputs(token_repetitions);
|
||||
repetitive_group.bench_with_input(
|
||||
BenchmarkId::new("repetitions", token_repetitions),
|
||||
&inputs,
|
||||
|bench, inputs| {
|
||||
let (base, predicted, final_text) = inputs;
|
||||
bench.iter(|| {
|
||||
black_box(compute_kept_rate(
|
||||
black_box(base),
|
||||
black_box(predicted),
|
||||
black_box(final_text),
|
||||
));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
repetitive_group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, kept_rate_benchmark);
|
||||
criterion_main!(benches);
|
||||
454
crates/edit_prediction/benches/ts_error_count.rs
Normal file
454
crates/edit_prediction/benches/ts_error_count.rs
Normal file
@@ -0,0 +1,454 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
|
||||
use edit_prediction::metrics::count_tree_sitter_errors;
|
||||
use fs::FakeFs;
|
||||
use gpui::{AppContext as _, TestAppContext};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry};
|
||||
use languages::init as init_languages;
|
||||
use node_runtime::NodeRuntime;
|
||||
use settings::SettingsStore;
|
||||
|
||||
struct ParsedCase {
|
||||
label: String,
|
||||
bytes: usize,
|
||||
error_count: usize,
|
||||
snapshot: BufferSnapshot,
|
||||
}
|
||||
|
||||
fn replace_nth_occurrences(
|
||||
source: &mut String,
|
||||
needle: &str,
|
||||
replacement: &str,
|
||||
every: usize,
|
||||
max_replacements: usize,
|
||||
) {
|
||||
let mut rebuilt = String::with_capacity(source.len());
|
||||
let mut cursor = 0;
|
||||
let mut seen = 0;
|
||||
let mut replaced = 0;
|
||||
|
||||
while let Some(relative_index) = source[cursor..].find(needle) {
|
||||
let start = cursor + relative_index;
|
||||
let end = start + needle.len();
|
||||
rebuilt.push_str(&source[cursor..start]);
|
||||
|
||||
if seen % every == 0 && replaced < max_replacements {
|
||||
rebuilt.push_str(replacement);
|
||||
replaced += 1;
|
||||
} else {
|
||||
rebuilt.push_str(needle);
|
||||
}
|
||||
|
||||
seen += 1;
|
||||
cursor = end;
|
||||
}
|
||||
|
||||
rebuilt.push_str(&source[cursor..]);
|
||||
*source = rebuilt;
|
||||
}
|
||||
|
||||
fn rust_source(function_count: usize) -> String {
|
||||
let mut source = String::from(
|
||||
"pub struct Counter {\n value: usize,\n}\n\nimpl Counter {\n pub fn new() -> Self {\n Self { value: 0 }\n }\n}\n\n",
|
||||
);
|
||||
for index in 0..function_count {
|
||||
source.push_str(&format!(
|
||||
"pub fn compute_value_{index}(input: usize) -> usize {{\n let mut total = input;\n for offset in 0..32 {{\n total += offset + {index};\n }}\n if total % 2 == 0 {{\n total / 2\n }} else {{\n total * 3 + 1\n }}\n}}\n\n"
|
||||
));
|
||||
}
|
||||
source
|
||||
}
|
||||
|
||||
fn rust_source_with_errors(function_count: usize) -> String {
|
||||
let mut source = rust_source(function_count);
|
||||
replace_nth_occurrences(
|
||||
&mut source,
|
||||
" if total % 2 == 0 {\n",
|
||||
" if total % 2 == 0 \n",
|
||||
17,
|
||||
48,
|
||||
);
|
||||
source
|
||||
}
|
||||
|
||||
fn python_source(function_count: usize) -> String {
|
||||
let mut source = String::from(
|
||||
"class Counter:\n def __init__(self) -> None:\n self.value = 0\n\n\n",
|
||||
);
|
||||
for index in 0..function_count {
|
||||
source.push_str(&format!(
|
||||
"def compute_value_{index}(input_value: int) -> int:\n total = input_value\n for offset in range(32):\n total += offset + {index}\n if total % 2 == 0:\n return total // 2\n return total * 3 + 1\n\n"
|
||||
));
|
||||
}
|
||||
source
|
||||
}
|
||||
|
||||
fn python_source_with_errors(function_count: usize) -> String {
|
||||
let mut source = python_source(function_count);
|
||||
replace_nth_occurrences(
|
||||
&mut source,
|
||||
" if total % 2 == 0:\n",
|
||||
" if total % 2 == 0\n",
|
||||
19,
|
||||
48,
|
||||
);
|
||||
source
|
||||
}
|
||||
|
||||
fn go_source(function_count: usize) -> String {
|
||||
let mut source = String::from(
|
||||
"package bench\n\ntype Counter struct {\n\tvalue int\n}\n\nfunc NewCounter() Counter {\n\treturn Counter{value: 0}\n}\n\n",
|
||||
);
|
||||
for index in 0..function_count {
|
||||
source.push_str(&format!(
|
||||
"func ComputeValue{index}(inputValue int) int {{\n\ttotal := inputValue\n\tfor offset := 0; offset < 32; offset++ {{\n\t\ttotal += offset + {index}\n\t}}\n\tif total%2 == 0 {{\n\t\treturn total / 2\n\t}}\n\treturn total*3 + 1\n}}\n\n"
|
||||
));
|
||||
}
|
||||
source
|
||||
}
|
||||
|
||||
fn go_source_with_errors(function_count: usize) -> String {
|
||||
let mut source = go_source(function_count);
|
||||
replace_nth_occurrences(
|
||||
&mut source,
|
||||
"\tfor offset := 0; offset < 32; offset++ {\n",
|
||||
"\tfor offset := 0; offset < 32; offset++ \n",
|
||||
17,
|
||||
48,
|
||||
);
|
||||
source
|
||||
}
|
||||
|
||||
fn typescript_source(function_count: usize) -> String {
|
||||
let mut source = String::from(
|
||||
"export type Counter = { value: number };\n\nexport function newCounter(): Counter {\n return { value: 0 };\n}\n\n",
|
||||
);
|
||||
for index in 0..function_count {
|
||||
source.push_str(&format!(
|
||||
"export function computeValue{index}(inputValue: number): number {{\n let total = inputValue;\n for (let offset = 0; offset < 32; offset += 1) {{\n total += offset + {index};\n }}\n return total % 2 === 0 ? total / 2 : total * 3 + 1;\n}}\n\n"
|
||||
));
|
||||
}
|
||||
source
|
||||
}
|
||||
|
||||
fn typescript_source_with_errors(function_count: usize) -> String {
|
||||
let mut source = typescript_source(function_count);
|
||||
replace_nth_occurrences(
|
||||
&mut source,
|
||||
" return total % 2 === 0 ? total / 2 : total * 3 + 1;\n",
|
||||
" return total % 2 === 0 ? total / 2 : ;\n",
|
||||
17,
|
||||
64,
|
||||
);
|
||||
source
|
||||
}
|
||||
|
||||
fn tsx_source(component_count: usize) -> String {
|
||||
let mut source = String::from(
|
||||
"type ItemProps = { index: number; label: string };\n\nfunction Item({ index, label }: ItemProps) {\n return <li data-index={index}>{label}</li>;\n}\n\nexport function App() {\n return <section><ul>{[0, 1, 2].map((value) => <Item key={value} index={value} label={`item-${value}`} />)}</ul></section>;\n}\n\n",
|
||||
);
|
||||
for index in 0..component_count {
|
||||
source.push_str(&format!(
|
||||
"export function Widget{index}(): JSX.Element {{\n const items = Array.from({{ length: 16 }}, (_, value) => value + {index});\n return (\n <div className=\"widget-{index}\">\n <h2>Widget {index}</h2>\n <ul>\n {{items.map((value) => (\n <Item key={{value}} index={{value}} label={{`widget-{index}-${{value}}`}} />\n ))}}\n </ul>\n </div>\n );\n}}\n\n"
|
||||
));
|
||||
}
|
||||
source
|
||||
}
|
||||
|
||||
fn tsx_source_with_errors(component_count: usize) -> String {
|
||||
let mut source = tsx_source(component_count);
|
||||
replace_nth_occurrences(
|
||||
&mut source,
|
||||
" const items = Array.from({ length: 16 }, (_, value) => value + ",
|
||||
" const items = Array.from({ length: 16 }, (_, value) => ); // ",
|
||||
11,
|
||||
32,
|
||||
);
|
||||
source
|
||||
}
|
||||
|
||||
fn json_source(object_count: usize) -> String {
|
||||
let mut source = String::from("{\n \"items\": [\n");
|
||||
for index in 0..object_count {
|
||||
let suffix = if index + 1 == object_count { "" } else { "," };
|
||||
source.push_str(&format!(
|
||||
" {{\n \"id\": {index},\n \"name\": \"item-{index}\",\n \"enabled\": true,\n \"tags\": [\"alpha\", \"beta\", \"gamma\"],\n \"metrics\": {{ \"count\": {}, \"ratio\": {} }}\n }}{suffix}\n",
|
||||
index * 3 + 1,
|
||||
index as f64 / 10.0,
|
||||
));
|
||||
}
|
||||
source.push_str(" ]\n}\n");
|
||||
source
|
||||
}
|
||||
|
||||
fn json_source_with_errors(object_count: usize) -> String {
|
||||
let mut source = json_source(object_count);
|
||||
replace_nth_occurrences(
|
||||
&mut source,
|
||||
" \"enabled\": true,\n",
|
||||
" \"enabled\": ,\n",
|
||||
23,
|
||||
64,
|
||||
);
|
||||
source
|
||||
}
|
||||
|
||||
fn yaml_source(document_count: usize) -> String {
|
||||
let mut source = String::new();
|
||||
for index in 0..document_count {
|
||||
source.push_str(&format!(
|
||||
"- id: {index}\n name: item-{index}\n enabled: true\n tags:\n - alpha\n - beta\n - gamma\n metrics:\n count: {}\n ratio: {}\n",
|
||||
index * 3 + 1,
|
||||
index as f64 / 10.0,
|
||||
));
|
||||
}
|
||||
source
|
||||
}
|
||||
|
||||
fn yaml_source_with_errors(document_count: usize) -> String {
|
||||
let mut source = yaml_source(document_count);
|
||||
replace_nth_occurrences(&mut source, " count: ", " count ", 23, 64);
|
||||
source
|
||||
}
|
||||
|
||||
fn css_source(rule_count: usize) -> String {
|
||||
let mut source = String::new();
|
||||
for index in 0..rule_count {
|
||||
source.push_str(&format!(
|
||||
".widget-{index} {{\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: 12px;\n padding: 8px;\n color: rgb({}, {}, {});\n}}\n\n.widget-{index} > .item-{index} {{\n border: 1px solid rgba(0, 0, 0, 0.15);\n background: linear-gradient(90deg, #fff, #eef);\n}}\n\n",
|
||||
(index * 17) % 255,
|
||||
(index * 31) % 255,
|
||||
(index * 47) % 255,
|
||||
));
|
||||
}
|
||||
source
|
||||
}
|
||||
|
||||
fn css_source_with_errors(rule_count: usize) -> String {
|
||||
let mut source = css_source(rule_count);
|
||||
replace_nth_occurrences(&mut source, " gap: 12px;\n", " gap 12px;\n", 29, 64);
|
||||
source
|
||||
}
|
||||
|
||||
fn build_case(
|
||||
context: &mut TestAppContext,
|
||||
languages: &Arc<LanguageRegistry>,
|
||||
language_name: &'static str,
|
||||
variant_name: &'static str,
|
||||
source: String,
|
||||
expect_errors: bool,
|
||||
) -> ParsedCase {
|
||||
let language_task = context.background_spawn({
|
||||
let languages = languages.clone();
|
||||
async move { languages.language_for_name(language_name).await }
|
||||
});
|
||||
while !language_task.is_ready() {
|
||||
context.run_until_parked();
|
||||
}
|
||||
let language = futures::executor::block_on(language_task)
|
||||
.unwrap_or_else(|error| panic!("failed to load {language_name}: {error}"));
|
||||
|
||||
let buffer = context.new(|cx| Buffer::local(source, cx).with_language(language, cx));
|
||||
context.run_until_parked();
|
||||
while buffer.read_with(context, |buffer, _| buffer.is_parsing()) {
|
||||
context.run_until_parked();
|
||||
}
|
||||
|
||||
let snapshot = buffer.read_with(context, |buffer, _| buffer.snapshot());
|
||||
let full_range = 0..snapshot.text.len();
|
||||
let error_count = count_tree_sitter_errors(snapshot.syntax_layers());
|
||||
if expect_errors {
|
||||
assert!(
|
||||
error_count > 0,
|
||||
"expected tree-sitter errors for {language_name}/{variant_name}",
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
error_count, 0,
|
||||
"expected no tree-sitter errors for {language_name}/{variant_name}",
|
||||
);
|
||||
}
|
||||
|
||||
let label = format!(
|
||||
"{}/{}_{}kb_{}e",
|
||||
language_name.to_lowercase(),
|
||||
variant_name,
|
||||
full_range.end / 1024,
|
||||
error_count,
|
||||
);
|
||||
ParsedCase {
|
||||
label,
|
||||
bytes: full_range.end,
|
||||
error_count,
|
||||
snapshot,
|
||||
}
|
||||
}
|
||||
|
||||
fn parsed_cases() -> Vec<ParsedCase> {
|
||||
let mut context = TestAppContext::single();
|
||||
context.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
|
||||
let languages = Arc::new(LanguageRegistry::new(context.executor()));
|
||||
let fs = FakeFs::new(context.executor());
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
context.update(|cx| init_languages(languages.clone(), fs, node_runtime, cx));
|
||||
|
||||
vec![
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"Rust",
|
||||
"valid",
|
||||
rust_source(900),
|
||||
false,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"Rust",
|
||||
"error_heavy",
|
||||
rust_source_with_errors(900),
|
||||
true,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"Python",
|
||||
"valid",
|
||||
python_source(1100),
|
||||
false,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"Python",
|
||||
"error_heavy",
|
||||
python_source_with_errors(1100),
|
||||
true,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"Go",
|
||||
"valid",
|
||||
go_source(1000),
|
||||
false,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"Go",
|
||||
"error_heavy",
|
||||
go_source_with_errors(1000),
|
||||
true,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"TypeScript",
|
||||
"valid",
|
||||
typescript_source(1000),
|
||||
false,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"TypeScript",
|
||||
"error_heavy",
|
||||
typescript_source_with_errors(1000),
|
||||
true,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"TSX",
|
||||
"valid",
|
||||
tsx_source(350),
|
||||
false,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"TSX",
|
||||
"error_heavy",
|
||||
tsx_source_with_errors(350),
|
||||
true,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"JSON",
|
||||
"valid",
|
||||
json_source(2200),
|
||||
false,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"JSON",
|
||||
"error_heavy",
|
||||
json_source_with_errors(2200),
|
||||
true,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"YAML",
|
||||
"valid",
|
||||
yaml_source(2200),
|
||||
false,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"YAML",
|
||||
"error_heavy",
|
||||
yaml_source_with_errors(2200),
|
||||
true,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"CSS",
|
||||
"valid",
|
||||
css_source(2400),
|
||||
false,
|
||||
),
|
||||
build_case(
|
||||
&mut context,
|
||||
&languages,
|
||||
"CSS",
|
||||
"error_heavy",
|
||||
css_source_with_errors(2400),
|
||||
true,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn ts_error_count_benchmark(c: &mut Criterion) {
|
||||
let cases = parsed_cases();
|
||||
let mut group = c.benchmark_group("ts_error_count/full_file");
|
||||
|
||||
for case in &cases {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(&case.label),
|
||||
case,
|
||||
|bench, case| {
|
||||
bench.iter(|| {
|
||||
black_box(case.bytes);
|
||||
black_box(case.error_count);
|
||||
black_box(count_tree_sitter_errors(case.snapshot.syntax_layers()))
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, ts_error_count_benchmark);
|
||||
criterion_main!(benches);
|
||||
13
crates/edit_prediction/license_examples/0bsd.txt
Normal file
13
crates/edit_prediction/license_examples/0bsd.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
Zero-Clause BSD
|
||||
=============
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for
|
||||
any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
|
||||
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
|
||||
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
174
crates/edit_prediction/license_examples/apache-2.0-ex0.txt
Normal file
174
crates/edit_prediction/license_examples/apache-2.0-ex0.txt
Normal file
@@ -0,0 +1,174 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
55
crates/edit_prediction/license_examples/apache-2.0-ex1.txt
Normal file
55
crates/edit_prediction/license_examples/apache-2.0-ex1.txt
Normal file
@@ -0,0 +1,55 @@
|
||||
Apache License
|
||||
|
||||
Version 2.0, January 2004
|
||||
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
189
crates/edit_prediction/license_examples/apache-2.0-ex2.txt
Normal file
189
crates/edit_prediction/license_examples/apache-2.0-ex2.txt
Normal file
@@ -0,0 +1,189 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
13
crates/edit_prediction/license_examples/apache-2.0-ex3.txt
Normal file
13
crates/edit_prediction/license_examples/apache-2.0-ex3.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
Copyright 2011 Someone
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
187
crates/edit_prediction/license_examples/apache-2.0-ex4.txt
Normal file
187
crates/edit_prediction/license_examples/apache-2.0-ex4.txt
Normal file
@@ -0,0 +1,187 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright (c) 2017, The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
20
crates/edit_prediction/license_examples/bsd-1-clause.txt
Normal file
20
crates/edit_prediction/license_examples/bsd-1-clause.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2024 John Doe
|
||||
Some Organization
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list
|
||||
of conditions and the following disclaimer.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY [Name of Organization] “AS IS” AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
|
||||
SHALL [Name of Organisation] BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
|
||||
OF SUCH DAMAGE.
|
||||
26
crates/edit_prediction/license_examples/bsd-2-clause-ex0.txt
Normal file
26
crates/edit_prediction/license_examples/bsd-2-clause-ex0.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2024
|
||||
|
||||
John Doe (john.doe@gmail.com)
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
29
crates/edit_prediction/license_examples/bsd-3-clause-ex0.txt
Normal file
29
crates/edit_prediction/license_examples/bsd-3-clause-ex0.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2025, John Doe
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
27
crates/edit_prediction/license_examples/bsd-3-clause-ex1.txt
Normal file
27
crates/edit_prediction/license_examples/bsd-3-clause-ex1.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2024 (this is copy modified from chromium)
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of da company nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
31
crates/edit_prediction/license_examples/bsd-3-clause-ex2.txt
Normal file
31
crates/edit_prediction/license_examples/bsd-3-clause-ex2.txt
Normal file
@@ -0,0 +1,31 @@
|
||||
The Glasgow Haskell Compiler License
|
||||
|
||||
Copyright 2002, The University Court of the University of Glasgow.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
- Neither name of the University nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF
|
||||
GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
30
crates/edit_prediction/license_examples/bsd-3-clause-ex3.txt
Normal file
30
crates/edit_prediction/license_examples/bsd-3-clause-ex3.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
Copyright (c) 2019 Someone
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* Neither the name of Someone nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
27
crates/edit_prediction/license_examples/bsd-3-clause-ex4.txt
Normal file
27
crates/edit_prediction/license_examples/bsd-3-clause-ex4.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009-2011, Mozilla Foundation and contributors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the names of the Mozilla Foundation nor the names of project
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
15
crates/edit_prediction/license_examples/isc.txt
Normal file
15
crates/edit_prediction/license_examples/isc.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2024, John Doe
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
21
crates/edit_prediction/license_examples/mit-ex0.txt
Normal file
21
crates/edit_prediction/license_examples/mit-ex0.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 John Doe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
26
crates/edit_prediction/license_examples/mit-ex1.txt
Normal file
26
crates/edit_prediction/license_examples/mit-ex1.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2006-2009 Someone
|
||||
Copyright (c) 2009-2013 Some organization
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
22
crates/edit_prediction/license_examples/mit-ex2.txt
Normal file
22
crates/edit_prediction/license_examples/mit-ex2.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) someone
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
21
crates/edit_prediction/license_examples/mit-ex3.txt
Normal file
21
crates/edit_prediction/license_examples/mit-ex3.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Someone.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
35
crates/edit_prediction/license_examples/upl-1.0.txt
Normal file
35
crates/edit_prediction/license_examples/upl-1.0.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
Copyright (c) 2025, John Doe
|
||||
|
||||
The Universal Permissive License (UPL), Version 1.0
|
||||
|
||||
Subject to the condition set forth below, permission is hereby granted to any person
|
||||
obtaining a copy of this software, associated documentation and/or data (collectively
|
||||
the "Software"), free of charge and under any and all copyright rights in the
|
||||
Software, and any and all patent rights owned or freely licensable by each licensor
|
||||
hereunder covering either (i) the unmodified Software as contributed to or provided
|
||||
by such licensor, or (ii) the Larger Works (as defined below), to deal in both
|
||||
|
||||
(a) the Software, and
|
||||
|
||||
(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is
|
||||
included with the Software (each a "Larger Work" to which the Software is
|
||||
contributed by such licensors),
|
||||
|
||||
without restriction, including without limitation the rights to copy, create
|
||||
derivative works of, display, perform, and distribute the Software and make, use,
|
||||
sell, offer for sale, import, export, have made, and have sold the Software and the
|
||||
Larger Work(s), and to sublicense the foregoing rights on either these or other
|
||||
terms.
|
||||
|
||||
This license is subject to the following condition:
|
||||
|
||||
The above copyright notice and either this complete permission notice or at a minimum
|
||||
a reference to the UPL must be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
19
crates/edit_prediction/license_examples/zlib-ex0.txt
Normal file
19
crates/edit_prediction/license_examples/zlib-ex0.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2021 Someone
|
||||
|
||||
This software is provided 'as-is', without any express or implied warranty. In
|
||||
no event will the authors be held liable for any damages arising from the use of
|
||||
this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose, including
|
||||
commercial applications, and to alter it and redistribute it freely, subject to
|
||||
the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not claim
|
||||
that you wrote the original software. If you use this software in a product,
|
||||
an acknowledgment in the product documentation would be appreciated but is
|
||||
not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
11
crates/edit_prediction/license_patterns/0bsd-pattern
Normal file
11
crates/edit_prediction/license_patterns/0bsd-pattern
Normal file
@@ -0,0 +1,11 @@
|
||||
-- 0..512
|
||||
Permission to use, copy, modify, and/or distribute this software for
|
||||
any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
|
||||
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
|
||||
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
109
crates/edit_prediction/license_patterns/apache-2.0-pattern
Normal file
109
crates/edit_prediction/license_patterns/apache-2.0-pattern
Normal file
@@ -0,0 +1,109 @@
|
||||
-- 0..512
|
||||
-- 0..0 optional:
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http
|
||||
-- 0..1 optional:
|
||||
://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
-- 0..5
|
||||
Apache License
|
||||
|
||||
Version 2.0, January 2004
|
||||
|
||||
http
|
||||
-- 0..1
|
||||
://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
-- 1..5
|
||||
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
-- 1..5
|
||||
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
-- 1..5
|
||||
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
-- 1..5
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
-- 1..1 optional:
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
-- 1..1 optional:
|
||||
APPENDIX How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work attach the following
|
||||
boilerplate notice with the fields enclosed by brackets
|
||||
replaced with your own identifying information Dont include
|
||||
the brackets The text should be enclosed in the appropriate
|
||||
comment syntax for the file format We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same printed page as the copyright notice for easier
|
||||
identification within thirdparty archives
|
||||
|
||||
-- 1..512 optional:
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
-- 1..5 optional:
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http
|
||||
-- 0..1 optional:
|
||||
://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
-- 1..5 optional:
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 0..512
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http
|
||||
-- 0..1
|
||||
://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
32
crates/edit_prediction/license_patterns/bsd-pattern
Normal file
32
crates/edit_prediction/license_patterns/bsd-pattern
Normal file
@@ -0,0 +1,32 @@
|
||||
-- 0..512
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
-- 1..5
|
||||
Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
-- 1..5 optional:
|
||||
Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
-- 1..128 optional:
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
-- 1..5
|
||||
THIS SOFTWARE IS PROVIDED
|
||||
-- 1..128
|
||||
“AS IS” AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL
|
||||
-- 1..128
|
||||
BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
12
crates/edit_prediction/license_patterns/isc-pattern
Normal file
12
crates/edit_prediction/license_patterns/isc-pattern
Normal file
@@ -0,0 +1,12 @@
|
||||
-- 0..512
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
18
crates/edit_prediction/license_patterns/mit-pattern
Normal file
18
crates/edit_prediction/license_patterns/mit-pattern
Normal file
@@ -0,0 +1,18 @@
|
||||
-- 0..512
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
32
crates/edit_prediction/license_patterns/upl-1.0-pattern
Normal file
32
crates/edit_prediction/license_patterns/upl-1.0-pattern
Normal file
@@ -0,0 +1,32 @@
|
||||
-- 0..512
|
||||
Subject to the condition set forth below, permission is hereby granted to any person
|
||||
obtaining a copy of this software, associated documentation and/or data (collectively
|
||||
the "Software"), free of charge and under any and all copyright rights in the
|
||||
Software, and any and all patent rights owned or freely licensable by each licensor
|
||||
hereunder covering either (i) the unmodified Software as contributed to or provided
|
||||
by such licensor, or (ii) the Larger Works (as defined below), to deal in both
|
||||
|
||||
(a) the Software, and
|
||||
|
||||
(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is
|
||||
included with the Software (each a "Larger Work" to which the Software is
|
||||
contributed by such licensors),
|
||||
|
||||
without restriction, including without limitation the rights to copy, create
|
||||
derivative works of, display, perform, and distribute the Software and make, use,
|
||||
sell, offer for sale, import, export, have made, and have sold the Software and the
|
||||
Larger Work(s), and to sublicense the foregoing rights on either these or other
|
||||
terms.
|
||||
|
||||
This license is subject to the following condition:
|
||||
|
||||
The above copyright notice and either this complete permission notice or at a minimum
|
||||
a reference to the UPL must be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
21
crates/edit_prediction/license_patterns/zlib-pattern
Normal file
21
crates/edit_prediction/license_patterns/zlib-pattern
Normal file
@@ -0,0 +1,21 @@
|
||||
-- 0..512
|
||||
This software is provided 'as-is', without any express or implied warranty. In
|
||||
no event will the authors be held liable for any damages arising from the use of
|
||||
this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose, including
|
||||
commercial applications, and to alter it and redistribute it freely, subject to
|
||||
the following restrictions:
|
||||
|
||||
-- 1..5
|
||||
The origin of this software must not be misrepresented; you must not claim
|
||||
that you wrote the original software. If you use this software in a product,
|
||||
an acknowledgment in the product documentation would be appreciated but is
|
||||
not required.
|
||||
|
||||
-- 1..5
|
||||
Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
-- 1..5
|
||||
This notice may not be removed or altered from any source distribution.
|
||||
557
crates/edit_prediction/src/capture_example.rs
Normal file
557
crates/edit_prediction/src/capture_example.rs
Normal file
@@ -0,0 +1,557 @@
|
||||
use crate::{StoredEvent, example_spec::ExampleSpec};
|
||||
use anyhow::Result;
|
||||
use buffer_diff::BufferDiffSnapshot;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::Buffer;
|
||||
use project::{Project, WorktreeId};
|
||||
use std::{collections::hash_map, fmt::Write as _, ops::Range, path::Path, sync::Arc};
|
||||
use text::{BufferSnapshot as TextBufferSnapshot, Point};
|
||||
|
||||
pub fn capture_example(
|
||||
project: Entity<Project>,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_anchor: language::Anchor,
|
||||
mut events: Vec<StoredEvent>,
|
||||
populate_expected_patch: bool,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<ExampleSpec>>> {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let file = snapshot.file()?;
|
||||
let worktree_id = file.worktree_id(cx);
|
||||
let repository = project.read(cx).active_repository(cx)?;
|
||||
let repository_snapshot = repository.read(cx).snapshot();
|
||||
let worktree = project.read(cx).worktree_for_id(worktree_id, cx)?;
|
||||
let root_name = worktree.read(cx).root_name_str().to_owned();
|
||||
let cursor_path: Arc<Path> = file.path().as_std_path().into();
|
||||
if worktree.read(cx).abs_path() != repository_snapshot.work_directory_abs_path {
|
||||
return None;
|
||||
}
|
||||
|
||||
let repository_url = repository_snapshot
|
||||
.remote_origin_url
|
||||
.clone()
|
||||
.or_else(|| repository_snapshot.remote_upstream_url.clone())?;
|
||||
let revision = repository_snapshot.head_commit.as_ref()?.sha.to_string();
|
||||
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
|
||||
Some(cx.spawn(async move |mut cx| {
|
||||
let snapshots_by_path =
|
||||
collect_snapshots(&project, &git_store, worktree_id, &events, &mut cx).await?;
|
||||
|
||||
events.retain(|stored_event| {
|
||||
let zeta_prompt::Event::BufferChange { path, .. } = stored_event.event.as_ref();
|
||||
let relative_path = strip_root_name(path, &root_name);
|
||||
snapshots_by_path.contains_key(relative_path)
|
||||
});
|
||||
|
||||
let line_comment_prefix = snapshot
|
||||
.language()
|
||||
.and_then(|lang| lang.config().line_comments.first())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let (cursor_excerpt, cursor_offset_in_excerpt, cursor_excerpt_range) = cx
|
||||
.background_executor()
|
||||
.spawn(async move { compute_cursor_excerpt(&snapshot, cursor_anchor) })
|
||||
.await;
|
||||
let uncommitted_diff = cx
|
||||
.background_executor()
|
||||
.spawn(async move { compute_uncommitted_diff(snapshots_by_path) })
|
||||
.await;
|
||||
|
||||
let mut edit_history = String::new();
|
||||
for stored_event in &events {
|
||||
write_event_with_relative_paths(&mut edit_history, &stored_event.event, &root_name);
|
||||
if !edit_history.ends_with('\n') {
|
||||
edit_history.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize an empty patch with context lines, to make it easy
|
||||
// to write the expected patch by hand.
|
||||
let mut expected_patches = Vec::new();
|
||||
let mut rejected_patch = None;
|
||||
if populate_expected_patch {
|
||||
let mut empty_patch = String::new();
|
||||
let start_row = cursor_excerpt_range.start.row + 1;
|
||||
let row_count = cursor_excerpt_range.end.row - cursor_excerpt_range.start.row + 1;
|
||||
writeln!(&mut empty_patch, "--- a/{}", cursor_path.display()).ok();
|
||||
writeln!(&mut empty_patch, "+++ b/{}", cursor_path.display()).ok();
|
||||
writeln!(
|
||||
&mut empty_patch,
|
||||
"@@ -{},{} +{},{} @@",
|
||||
start_row, row_count, start_row, row_count,
|
||||
)
|
||||
.ok();
|
||||
for line in cursor_excerpt.lines() {
|
||||
writeln!(&mut empty_patch, " {}", line).ok();
|
||||
}
|
||||
|
||||
expected_patches.push(empty_patch.clone());
|
||||
rejected_patch = Some(empty_patch);
|
||||
}
|
||||
|
||||
let mut spec = ExampleSpec {
|
||||
name: generate_timestamp_name(),
|
||||
repository_url,
|
||||
revision,
|
||||
tags: Vec::new(),
|
||||
reasoning: None,
|
||||
uncommitted_diff,
|
||||
cursor_path,
|
||||
cursor_position: String::new(),
|
||||
edit_history,
|
||||
expected_patches,
|
||||
rejected_patch,
|
||||
telemetry: None,
|
||||
human_feedback: Vec::new(),
|
||||
rating: None,
|
||||
};
|
||||
spec.set_cursor_excerpt(
|
||||
&cursor_excerpt,
|
||||
cursor_offset_in_excerpt,
|
||||
&line_comment_prefix,
|
||||
);
|
||||
Ok(spec)
|
||||
}))
|
||||
}
|
||||
|
||||
fn strip_root_name<'a>(path: &'a Path, root_name: &str) -> &'a Path {
|
||||
path.strip_prefix(root_name).unwrap_or(path)
|
||||
}
|
||||
|
||||
fn write_event_with_relative_paths(
|
||||
output: &mut String,
|
||||
event: &zeta_prompt::Event,
|
||||
root_name: &str,
|
||||
) {
|
||||
fn write_relative_path(output: &mut String, path: &Path, root_name: &str) {
|
||||
for component in strip_root_name(path, root_name).components() {
|
||||
output.push('/');
|
||||
write!(output, "{}", component.as_os_str().to_string_lossy()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
let zeta_prompt::Event::BufferChange {
|
||||
path,
|
||||
old_path,
|
||||
diff,
|
||||
..
|
||||
} = event;
|
||||
|
||||
output.push_str("--- a");
|
||||
write_relative_path(output, old_path.as_ref(), root_name);
|
||||
output.push_str("\n+++ b");
|
||||
write_relative_path(output, path.as_ref(), root_name);
|
||||
output.push('\n');
|
||||
output.push_str(diff);
|
||||
}
|
||||
|
||||
fn compute_cursor_excerpt(
|
||||
snapshot: &language::BufferSnapshot,
|
||||
cursor_anchor: language::Anchor,
|
||||
) -> (String, usize, Range<Point>) {
|
||||
use text::ToOffset as _;
|
||||
use text::ToPoint as _;
|
||||
|
||||
let cursor_offset = cursor_anchor.to_offset(snapshot);
|
||||
let (excerpt_point_range, excerpt_offset_range, cursor_offset_in_excerpt) =
|
||||
crate::cursor_excerpt::compute_cursor_excerpt(snapshot, cursor_offset);
|
||||
let syntax_ranges = crate::cursor_excerpt::compute_syntax_ranges(
|
||||
snapshot,
|
||||
cursor_offset,
|
||||
&excerpt_offset_range,
|
||||
);
|
||||
let excerpt_text: String = snapshot.text_for_range(excerpt_point_range).collect();
|
||||
let (_, context_range) = zeta_prompt::compute_editable_and_context_ranges(
|
||||
&excerpt_text,
|
||||
cursor_offset_in_excerpt,
|
||||
&syntax_ranges,
|
||||
100,
|
||||
50,
|
||||
);
|
||||
let context_text = excerpt_text[context_range.clone()].to_string();
|
||||
let cursor_in_context = cursor_offset_in_excerpt.saturating_sub(context_range.start);
|
||||
let context_buffer_start =
|
||||
(excerpt_offset_range.start + context_range.start).to_point(snapshot);
|
||||
let context_buffer_end = (excerpt_offset_range.start + context_range.end).to_point(snapshot);
|
||||
(
|
||||
context_text,
|
||||
cursor_in_context,
|
||||
context_buffer_start..context_buffer_end,
|
||||
)
|
||||
}
|
||||
|
||||
async fn collect_snapshots(
|
||||
project: &Entity<Project>,
|
||||
git_store: &Entity<project::git_store::GitStore>,
|
||||
worktree_id: WorktreeId,
|
||||
events: &[StoredEvent],
|
||||
cx: &mut gpui::AsyncApp,
|
||||
) -> Result<HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>> {
|
||||
let mut snapshots_by_path = HashMap::default();
|
||||
for stored_event in events {
|
||||
let zeta_prompt::Event::BufferChange { path, .. } = stored_event.event.as_ref();
|
||||
if let Some((project_path, relative_path)) = project.read_with(cx, |project, cx| {
|
||||
let project_path = project
|
||||
.find_project_path(path, cx)
|
||||
.filter(|path| path.worktree_id == worktree_id)?;
|
||||
let relative_path: Arc<Path> = project_path.path.as_std_path().into();
|
||||
Some((project_path, relative_path))
|
||||
}) {
|
||||
if let hash_map::Entry::Vacant(entry) = snapshots_by_path.entry(relative_path) {
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})
|
||||
.await?;
|
||||
let diff = git_store
|
||||
.update(cx, |git_store, cx| {
|
||||
git_store.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await?;
|
||||
let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx));
|
||||
entry.insert((stored_event.old_snapshot.clone(), diff_snapshot));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(snapshots_by_path)
|
||||
}
|
||||
|
||||
fn compute_uncommitted_diff(
|
||||
snapshots_by_path: HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>,
|
||||
) -> String {
|
||||
let mut uncommitted_diff = String::new();
|
||||
for (relative_path, (before_text, diff_snapshot)) in snapshots_by_path {
|
||||
if let Some(head_text) = &diff_snapshot.base_text_string() {
|
||||
let file_diff = language::unified_diff(head_text, &before_text.text());
|
||||
if !file_diff.is_empty() {
|
||||
let path_str = relative_path.to_string_lossy();
|
||||
writeln!(uncommitted_diff, "--- a/{path_str}").ok();
|
||||
writeln!(uncommitted_diff, "+++ b/{path_str}").ok();
|
||||
uncommitted_diff.push_str(&file_diff);
|
||||
if !uncommitted_diff.ends_with('\n') {
|
||||
uncommitted_diff.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
uncommitted_diff
|
||||
}
|
||||
|
||||
fn generate_timestamp_name() -> String {
|
||||
let format = time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]");
|
||||
match format {
|
||||
Ok(format) => {
|
||||
let now = time::OffsetDateTime::now_local()
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
|
||||
now.format(&format)
|
||||
.unwrap_or_else(|_| "unknown-time".to_string())
|
||||
}
|
||||
Err(_) => "unknown-time".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::EditPredictionStore;
|
||||
use client::RefreshLlmTokenListener;
|
||||
use client::{Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{AppContext as _, TestAppContext, http_client::FakeHttpClient};
|
||||
use indoc::indoc;
|
||||
use language::{Anchor, Point};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::path::Path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_capture_example(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
let committed_contents = indoc! {"
|
||||
fn main() {
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
four();
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
nine();
|
||||
}
|
||||
"};
|
||||
|
||||
let disk_contents = indoc! {"
|
||||
fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
four();
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"};
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
".git": {},
|
||||
"src": {
|
||||
"main.rs": disk_contents,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Create an external file outside the main project
|
||||
fs.insert_tree(
|
||||
"/external",
|
||||
json!({
|
||||
"external.rs": "fn external() {}\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_head_for_repo(
|
||||
Path::new("/project/.git"),
|
||||
&[("src/main.rs", committed_contents.to_string())],
|
||||
"abc123def456",
|
||||
);
|
||||
fs.set_remote_for_repo(
|
||||
Path::new("/project/.git"),
|
||||
"origin",
|
||||
"https://github.com/test/repo.git",
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), ["/project".as_ref()], cx).await;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/project/src/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let ep_store = cx.read(|cx| EditPredictionStore::try_global(cx).unwrap());
|
||||
ep_store.update(cx, |ep_store, cx| {
|
||||
ep_store.register_buffer(&buffer, &project, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let point = Point::new(6, 0);
|
||||
buffer.edit([(point..point, " // comment 3\n")], None, cx);
|
||||
let point = Point::new(4, 0);
|
||||
buffer.edit([(point..point, " // comment 4\n")], None, cx);
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
buffer.text(),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
// comment 4
|
||||
three();
|
||||
four();
|
||||
// comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Open and edit an external file (outside the main project's worktree)
|
||||
let external_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/external/external.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
ep_store.update(cx, |ep_store, cx| {
|
||||
ep_store.register_buffer(&external_buffer, &project, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
external_buffer.update(cx, |buffer, cx| {
|
||||
let point = Point::new(0, 0);
|
||||
buffer.edit([(point..point, "// external edit\n")], None, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify the external edit was recorded in events
|
||||
let events = ep_store.update(cx, |store, cx| store.edit_history_for_project(&project, cx));
|
||||
assert!(
|
||||
matches!(
|
||||
events
|
||||
.last()
|
||||
.unwrap()
|
||||
.event
|
||||
.as_ref(),
|
||||
zeta_prompt::Event::BufferChange { path, .. } if path.as_ref() == "/external/external.rs"
|
||||
),
|
||||
"external file edit should be in events"
|
||||
);
|
||||
|
||||
let mut example = cx
|
||||
.update(|cx| {
|
||||
capture_example(
|
||||
project.clone(),
|
||||
buffer.clone(),
|
||||
Anchor::min_for_buffer(buffer.read(cx).remote_id()),
|
||||
events,
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
example.name = "test".to_string();
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
example,
|
||||
ExampleSpec {
|
||||
name: "test".to_string(),
|
||||
repository_url: "https://github.com/test/repo.git".to_string(),
|
||||
revision: "abc123def456".to_string(),
|
||||
tags: Vec::new(),
|
||||
reasoning: None,
|
||||
uncommitted_diff: indoc! {"
|
||||
--- a/src/main.rs
|
||||
+++ b/src/main.rs
|
||||
@@ -1,4 +1,5 @@
|
||||
fn main() {
|
||||
+ // comment 1
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
@@ -7,5 +8,6 @@
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
+ // comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
.to_string(),
|
||||
cursor_path: Path::new("src/main.rs").into(),
|
||||
cursor_position: indoc! {"
|
||||
fn main() {
|
||||
^[CURSOR_POSITION]
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
// comment 4
|
||||
three();
|
||||
four();
|
||||
// comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
.to_string(),
|
||||
edit_history: indoc! {"
|
||||
--- a/src/main.rs
|
||||
+++ b/src/main.rs
|
||||
@@ -2,8 +2,10 @@
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
+ // comment 4
|
||||
three();
|
||||
four();
|
||||
+ // comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
"}
|
||||
.to_string(),
|
||||
expected_patches: vec![
|
||||
indoc! {"
|
||||
--- a/src/main.rs
|
||||
+++ b/src/main.rs
|
||||
@@ -1,16 +1,16 @@
|
||||
fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
// comment 4
|
||||
three();
|
||||
four();
|
||||
// comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
.to_string()
|
||||
],
|
||||
rejected_patch: Some(
|
||||
indoc! {"
|
||||
--- a/src/main.rs
|
||||
+++ b/src/main.rs
|
||||
@@ -1,16 +1,16 @@
|
||||
fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
// comment 4
|
||||
three();
|
||||
four();
|
||||
// comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
.to_string()
|
||||
),
|
||||
telemetry: None,
|
||||
human_feedback: Vec::new(),
|
||||
rating: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
zlog::init_test();
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(Arc::new(FakeSystemClock::new()), http_client, cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
language_model::init(cx);
|
||||
RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
|
||||
EditPredictionStore::global(&client, &user_store, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
569
crates/edit_prediction/src/cursor_excerpt.rs
Normal file
569
crates/edit_prediction/src/cursor_excerpt.rs
Normal file
@@ -0,0 +1,569 @@
|
||||
use language::{BufferSnapshot, Point, ToPoint as _};
|
||||
use std::ops::Range;
|
||||
use text::OffsetRangeExt as _;
|
||||
|
||||
const CURSOR_EXCERPT_TOKEN_BUDGET: usize = 8192;
|
||||
|
||||
/// Computes a cursor excerpt as the largest linewise symmetric region around
|
||||
/// the cursor that fits within an 8192-token budget. Returns the point range,
|
||||
/// byte offset range, and the cursor offset relative to the excerpt start.
|
||||
pub fn compute_cursor_excerpt(
|
||||
snapshot: &BufferSnapshot,
|
||||
cursor_offset: usize,
|
||||
) -> (Range<Point>, Range<usize>, usize) {
|
||||
let cursor_point = cursor_offset.to_point(snapshot);
|
||||
let cursor_row = cursor_point.row;
|
||||
let (start_row, end_row, _) =
|
||||
expand_symmetric_from_cursor(snapshot, cursor_row, CURSOR_EXCERPT_TOKEN_BUDGET);
|
||||
|
||||
let excerpt_range = Point::new(start_row, 0)..Point::new(end_row, snapshot.line_len(end_row));
|
||||
let excerpt_offset_range = excerpt_range.to_offset(snapshot);
|
||||
let cursor_offset_in_excerpt = cursor_offset - excerpt_offset_range.start;
|
||||
|
||||
(
|
||||
excerpt_range,
|
||||
excerpt_offset_range,
|
||||
cursor_offset_in_excerpt,
|
||||
)
|
||||
}
|
||||
|
||||
/// Expands symmetrically from cursor, one line at a time, alternating down then up.
|
||||
/// Returns (start_row, end_row, remaining_tokens).
|
||||
fn expand_symmetric_from_cursor(
|
||||
snapshot: &BufferSnapshot,
|
||||
cursor_row: u32,
|
||||
mut token_budget: usize,
|
||||
) -> (u32, u32, usize) {
|
||||
let mut start_row = cursor_row;
|
||||
let mut end_row = cursor_row;
|
||||
|
||||
let cursor_line_tokens = line_token_count(snapshot, cursor_row);
|
||||
token_budget = token_budget.saturating_sub(cursor_line_tokens);
|
||||
|
||||
loop {
|
||||
let can_expand_up = start_row > 0;
|
||||
let can_expand_down = end_row < snapshot.max_point().row;
|
||||
|
||||
if token_budget == 0 || (!can_expand_up && !can_expand_down) {
|
||||
break;
|
||||
}
|
||||
|
||||
if can_expand_down {
|
||||
let next_row = end_row + 1;
|
||||
let line_tokens = line_token_count(snapshot, next_row);
|
||||
if line_tokens <= token_budget {
|
||||
end_row = next_row;
|
||||
token_budget = token_budget.saturating_sub(line_tokens);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if can_expand_up && token_budget > 0 {
|
||||
let next_row = start_row - 1;
|
||||
let line_tokens = line_token_count(snapshot, next_row);
|
||||
if line_tokens <= token_budget {
|
||||
start_row = next_row;
|
||||
token_budget = token_budget.saturating_sub(line_tokens);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(start_row, end_row, token_budget)
|
||||
}
|
||||
|
||||
/// Typical number of string bytes per token for the purposes of limiting model input. This is
|
||||
/// intentionally low to err on the side of underestimating limits.
|
||||
pub(crate) const BYTES_PER_TOKEN_GUESS: usize = 3;
|
||||
|
||||
pub fn guess_token_count(bytes: usize) -> usize {
|
||||
bytes / BYTES_PER_TOKEN_GUESS
|
||||
}
|
||||
|
||||
fn line_token_count(snapshot: &BufferSnapshot, row: u32) -> usize {
|
||||
guess_token_count(snapshot.line_len(row) as usize).max(1)
|
||||
}
|
||||
|
||||
/// Computes the byte offset ranges of all syntax nodes containing the cursor,
|
||||
/// ordered from innermost to outermost. The offsets are relative to
|
||||
/// `excerpt_offset_range.start`.
|
||||
pub fn compute_syntax_ranges(
|
||||
snapshot: &BufferSnapshot,
|
||||
cursor_offset: usize,
|
||||
excerpt_offset_range: &Range<usize>,
|
||||
) -> Vec<Range<usize>> {
|
||||
let cursor_point = cursor_offset.to_point(snapshot);
|
||||
let range = cursor_point..cursor_point;
|
||||
let mut current = snapshot.syntax_ancestor(range);
|
||||
let mut ranges = Vec::new();
|
||||
let mut last_range: Option<(usize, usize)> = None;
|
||||
|
||||
while let Some(node) = current.take() {
|
||||
let node_start = node.start_byte();
|
||||
let node_end = node.end_byte();
|
||||
let key = (node_start, node_end);
|
||||
|
||||
current = node.parent();
|
||||
|
||||
if last_range == Some(key) {
|
||||
continue;
|
||||
}
|
||||
last_range = Some(key);
|
||||
|
||||
let start = node_start.saturating_sub(excerpt_offset_range.start);
|
||||
let end = node_end
|
||||
.min(excerpt_offset_range.end)
|
||||
.saturating_sub(excerpt_offset_range.start);
|
||||
ranges.push(start..end);
|
||||
}
|
||||
|
||||
ranges
|
||||
}
|
||||
|
||||
/// Expands context by first trying to reach syntax boundaries,
|
||||
/// then expanding line-wise only if no syntax expansion occurred.
|
||||
pub fn expand_context_syntactically_then_linewise(
|
||||
snapshot: &BufferSnapshot,
|
||||
editable_range: Range<Point>,
|
||||
context_token_limit: usize,
|
||||
) -> Range<Point> {
|
||||
let mut start_row = editable_range.start.row;
|
||||
let mut end_row = editable_range.end.row;
|
||||
let mut remaining_tokens = context_token_limit;
|
||||
let mut did_syntax_expand = false;
|
||||
|
||||
// Phase 1: Try to expand to containing syntax boundaries, picking the largest that fits.
|
||||
for (boundary_start, boundary_end) in containing_syntax_boundaries(snapshot, start_row, end_row)
|
||||
{
|
||||
let tokens_for_start = if boundary_start < start_row {
|
||||
estimate_tokens_for_rows(snapshot, boundary_start, start_row)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let tokens_for_end = if boundary_end > end_row {
|
||||
estimate_tokens_for_rows(snapshot, end_row + 1, boundary_end + 1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let total_needed = tokens_for_start + tokens_for_end;
|
||||
|
||||
if total_needed <= remaining_tokens {
|
||||
if boundary_start < start_row {
|
||||
start_row = boundary_start;
|
||||
}
|
||||
if boundary_end > end_row {
|
||||
end_row = boundary_end;
|
||||
}
|
||||
remaining_tokens = remaining_tokens.saturating_sub(total_needed);
|
||||
did_syntax_expand = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Only expand line-wise if no syntax expansion occurred.
|
||||
if !did_syntax_expand {
|
||||
(start_row, end_row, _) =
|
||||
expand_linewise_biased(snapshot, start_row, end_row, remaining_tokens, true);
|
||||
}
|
||||
|
||||
let start = Point::new(start_row, 0);
|
||||
let end = Point::new(end_row, snapshot.line_len(end_row));
|
||||
start..end
|
||||
}
|
||||
|
||||
/// Returns an iterator of (start_row, end_row) for successively larger syntax nodes
|
||||
/// containing the given row range. Smallest containing node first.
|
||||
fn containing_syntax_boundaries(
|
||||
snapshot: &BufferSnapshot,
|
||||
start_row: u32,
|
||||
end_row: u32,
|
||||
) -> impl Iterator<Item = (u32, u32)> {
|
||||
let range = Point::new(start_row, 0)..Point::new(end_row, snapshot.line_len(end_row));
|
||||
let mut current = snapshot.syntax_ancestor(range);
|
||||
let mut last_rows: Option<(u32, u32)> = None;
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
while let Some(node) = current.take() {
|
||||
let node_start_row = node.start_position().row as u32;
|
||||
let node_end_row = node.end_position().row as u32;
|
||||
let rows = (node_start_row, node_end_row);
|
||||
|
||||
current = node.parent();
|
||||
|
||||
// Skip nodes that don't extend beyond our range.
|
||||
if node_start_row >= start_row && node_end_row <= end_row {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if same as last returned (some nodes have same span).
|
||||
if last_rows == Some(rows) {
|
||||
continue;
|
||||
}
|
||||
|
||||
last_rows = Some(rows);
|
||||
return Some(rows);
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Expands line-wise with a bias toward one direction.
|
||||
/// Returns (start_row, end_row, remaining_tokens).
|
||||
fn expand_linewise_biased(
|
||||
snapshot: &BufferSnapshot,
|
||||
mut start_row: u32,
|
||||
mut end_row: u32,
|
||||
mut remaining_tokens: usize,
|
||||
prefer_up: bool,
|
||||
) -> (u32, u32, usize) {
|
||||
loop {
|
||||
let can_expand_up = start_row > 0;
|
||||
let can_expand_down = end_row < snapshot.max_point().row;
|
||||
|
||||
if remaining_tokens == 0 || (!can_expand_up && !can_expand_down) {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut expanded = false;
|
||||
|
||||
// Try preferred direction first.
|
||||
if prefer_up {
|
||||
if can_expand_up {
|
||||
let next_row = start_row - 1;
|
||||
let line_tokens = line_token_count(snapshot, next_row);
|
||||
if line_tokens <= remaining_tokens {
|
||||
start_row = next_row;
|
||||
remaining_tokens = remaining_tokens.saturating_sub(line_tokens);
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
if can_expand_down && remaining_tokens > 0 {
|
||||
let next_row = end_row + 1;
|
||||
let line_tokens = line_token_count(snapshot, next_row);
|
||||
if line_tokens <= remaining_tokens {
|
||||
end_row = next_row;
|
||||
remaining_tokens = remaining_tokens.saturating_sub(line_tokens);
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if can_expand_down {
|
||||
let next_row = end_row + 1;
|
||||
let line_tokens = line_token_count(snapshot, next_row);
|
||||
if line_tokens <= remaining_tokens {
|
||||
end_row = next_row;
|
||||
remaining_tokens = remaining_tokens.saturating_sub(line_tokens);
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
if can_expand_up && remaining_tokens > 0 {
|
||||
let next_row = start_row - 1;
|
||||
let line_tokens = line_token_count(snapshot, next_row);
|
||||
if line_tokens <= remaining_tokens {
|
||||
start_row = next_row;
|
||||
remaining_tokens = remaining_tokens.saturating_sub(line_tokens);
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !expanded {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(start_row, end_row, remaining_tokens)
|
||||
}
|
||||
|
||||
/// Estimates token count for rows in range [start_row, end_row).
|
||||
fn estimate_tokens_for_rows(snapshot: &BufferSnapshot, start_row: u32, end_row: u32) -> usize {
|
||||
let mut tokens = 0;
|
||||
for row in start_row..end_row {
|
||||
tokens += line_token_count(snapshot, row);
|
||||
}
|
||||
tokens
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{App, AppContext as _};
|
||||
use indoc::indoc;
|
||||
use language::{Buffer, rust_lang};
|
||||
use util::test::{TextRangeMarker, marked_text_ranges_by};
|
||||
use zeta_prompt::compute_editable_and_context_ranges;
|
||||
|
||||
struct TestCase {
|
||||
name: &'static str,
|
||||
marked_text: &'static str,
|
||||
editable_token_limit: usize,
|
||||
context_token_limit: usize,
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_editable_and_context_ranges(cx: &mut App) {
|
||||
// Markers:
|
||||
// ˇ = cursor position
|
||||
// « » = expected editable range
|
||||
// [ ] = expected context range
|
||||
let test_cases = vec![
|
||||
TestCase {
|
||||
name: "small function fits entirely in editable and context",
|
||||
marked_text: indoc! {r#"
|
||||
[«fn foo() {
|
||||
let x = 1;ˇ
|
||||
let y = 2;
|
||||
}»]
|
||||
"#},
|
||||
editable_token_limit: 30,
|
||||
context_token_limit: 60,
|
||||
},
|
||||
TestCase {
|
||||
name: "cursor near end of function - editable expands to syntax boundaries",
|
||||
marked_text: indoc! {r#"
|
||||
[fn first() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
}
|
||||
|
||||
fn foo() {
|
||||
« let x = 1;
|
||||
let y = 2;
|
||||
println!("{}", x + y);ˇ
|
||||
}»]
|
||||
"#},
|
||||
editable_token_limit: 18,
|
||||
context_token_limit: 35,
|
||||
},
|
||||
TestCase {
|
||||
name: "cursor at function start - editable expands to syntax boundaries",
|
||||
marked_text: indoc! {r#"
|
||||
[fn before() {
|
||||
« let a = 1;
|
||||
}
|
||||
|
||||
fn foo() {ˇ
|
||||
let x = 1;
|
||||
let y = 2;
|
||||
let z = 3;
|
||||
}
|
||||
»
|
||||
fn after() {
|
||||
let b = 2;
|
||||
}]
|
||||
"#},
|
||||
editable_token_limit: 25,
|
||||
context_token_limit: 50,
|
||||
},
|
||||
TestCase {
|
||||
name: "tiny budget - just lines around cursor, no syntax expansion",
|
||||
marked_text: indoc! {r#"
|
||||
fn outer() {
|
||||
[ let line1 = 1;
|
||||
let line2 = 2;
|
||||
« let line3 = 3;
|
||||
let line4 = 4;ˇ»
|
||||
let line5 = 5;
|
||||
let line6 = 6;]
|
||||
let line7 = 7;
|
||||
}
|
||||
"#},
|
||||
editable_token_limit: 12,
|
||||
context_token_limit: 24,
|
||||
},
|
||||
TestCase {
|
||||
name: "context extends beyond editable",
|
||||
marked_text: indoc! {r#"
|
||||
[fn first() { let a = 1; }
|
||||
«fn second() { let b = 2; }
|
||||
fn third() { let c = 3; }ˇ
|
||||
fn fourth() { let d = 4; }»
|
||||
fn fifth() { let e = 5; }]
|
||||
"#},
|
||||
editable_token_limit: 25,
|
||||
context_token_limit: 45,
|
||||
},
|
||||
TestCase {
|
||||
name: "cursor in first if-block - editable expands to syntax boundaries",
|
||||
marked_text: indoc! {r#"
|
||||
[«fn before() { }
|
||||
|
||||
fn process() {
|
||||
if condition1 {
|
||||
let a = 1;ˇ
|
||||
let b = 2;
|
||||
}
|
||||
if condition2 {»
|
||||
let c = 3;
|
||||
let d = 4;
|
||||
}
|
||||
if condition3 {
|
||||
let e = 5;
|
||||
let f = 6;
|
||||
}
|
||||
}
|
||||
|
||||
fn after() { }]
|
||||
"#},
|
||||
editable_token_limit: 35,
|
||||
context_token_limit: 60,
|
||||
},
|
||||
TestCase {
|
||||
name: "cursor in middle if-block - editable spans surrounding blocks",
|
||||
marked_text: indoc! {r#"
|
||||
[fn before() { }
|
||||
|
||||
fn process() {
|
||||
if condition1 {
|
||||
let a = 1;
|
||||
« let b = 2;
|
||||
}
|
||||
if condition2 {
|
||||
let c = 3;ˇ
|
||||
let d = 4;
|
||||
}
|
||||
if condition3 {
|
||||
let e = 5;»
|
||||
let f = 6;
|
||||
}
|
||||
}
|
||||
|
||||
fn after() { }]
|
||||
"#},
|
||||
editable_token_limit: 40,
|
||||
context_token_limit: 60,
|
||||
},
|
||||
TestCase {
|
||||
name: "cursor near bottom of long function - context reaches function boundary",
|
||||
marked_text: indoc! {r#"
|
||||
[fn other() { }
|
||||
|
||||
fn long_function() {
|
||||
let line1 = 1;
|
||||
let line2 = 2;
|
||||
let line3 = 3;
|
||||
let line4 = 4;
|
||||
let line5 = 5;
|
||||
let line6 = 6;
|
||||
« let line7 = 7;
|
||||
let line8 = 8;
|
||||
let line9 = 9;
|
||||
let line10 = 10;ˇ
|
||||
let line11 = 11;
|
||||
}
|
||||
|
||||
fn another() { }»]
|
||||
"#},
|
||||
editable_token_limit: 40,
|
||||
context_token_limit: 55,
|
||||
},
|
||||
TestCase {
|
||||
name: "zero context budget - context equals editable",
|
||||
marked_text: indoc! {r#"
|
||||
fn before() {
|
||||
let p = 1;
|
||||
let q = 2;
|
||||
[«}
|
||||
|
||||
fn foo() {
|
||||
let x = 1;ˇ
|
||||
let y = 2;
|
||||
}
|
||||
»]
|
||||
fn after() {
|
||||
let r = 3;
|
||||
let s = 4;
|
||||
}
|
||||
"#},
|
||||
editable_token_limit: 15,
|
||||
context_token_limit: 0,
|
||||
},
|
||||
];
|
||||
|
||||
for test_case in test_cases {
|
||||
let cursor_marker: TextRangeMarker = 'ˇ'.into();
|
||||
let editable_marker: TextRangeMarker = ('«', '»').into();
|
||||
let context_marker: TextRangeMarker = ('[', ']').into();
|
||||
|
||||
let (text, mut ranges) = marked_text_ranges_by(
|
||||
test_case.marked_text,
|
||||
vec![
|
||||
cursor_marker.clone(),
|
||||
editable_marker.clone(),
|
||||
context_marker.clone(),
|
||||
],
|
||||
);
|
||||
|
||||
let cursor_ranges = ranges.remove(&cursor_marker).unwrap_or_default();
|
||||
let expected_editable = ranges.remove(&editable_marker).unwrap_or_default();
|
||||
let expected_context = ranges.remove(&context_marker).unwrap_or_default();
|
||||
assert_eq!(expected_editable.len(), 1, "{}", test_case.name);
|
||||
assert_eq!(expected_context.len(), 1, "{}", test_case.name);
|
||||
|
||||
cx.new(|cx: &mut gpui::Context<Buffer>| {
|
||||
let text = text.trim_end_matches('\n');
|
||||
let buffer = Buffer::local(text, cx).with_language(rust_lang(), cx);
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
let cursor_offset = cursor_ranges[0].start;
|
||||
|
||||
let (_, excerpt_offset_range, cursor_offset_in_excerpt) =
|
||||
compute_cursor_excerpt(&snapshot, cursor_offset);
|
||||
let excerpt_text: String = snapshot
|
||||
.text_for_range(excerpt_offset_range.clone())
|
||||
.collect();
|
||||
let syntax_ranges =
|
||||
compute_syntax_ranges(&snapshot, cursor_offset, &excerpt_offset_range);
|
||||
|
||||
let (actual_editable, actual_context) = compute_editable_and_context_ranges(
|
||||
&excerpt_text,
|
||||
cursor_offset_in_excerpt,
|
||||
&syntax_ranges,
|
||||
test_case.editable_token_limit,
|
||||
test_case.context_token_limit,
|
||||
);
|
||||
|
||||
let to_buffer_range = |range: Range<usize>| -> Range<usize> {
|
||||
(excerpt_offset_range.start + range.start)
|
||||
..(excerpt_offset_range.start + range.end)
|
||||
};
|
||||
|
||||
let actual_editable = to_buffer_range(actual_editable);
|
||||
let actual_context = to_buffer_range(actual_context);
|
||||
|
||||
let expected_editable_range = expected_editable[0].clone();
|
||||
let expected_context_range = expected_context[0].clone();
|
||||
|
||||
let editable_match = actual_editable == expected_editable_range;
|
||||
let context_match = actual_context == expected_context_range;
|
||||
|
||||
if !editable_match || !context_match {
|
||||
let range_text = |range: &Range<usize>| {
|
||||
snapshot.text_for_range(range.clone()).collect::<String>()
|
||||
};
|
||||
|
||||
println!("\n=== FAILED: {} ===", test_case.name);
|
||||
if !editable_match {
|
||||
println!("\nExpected editable ({:?}):", expected_editable_range);
|
||||
println!("---\n{}---", range_text(&expected_editable_range));
|
||||
println!("\nActual editable ({:?}):", actual_editable);
|
||||
println!("---\n{}---", range_text(&actual_editable));
|
||||
}
|
||||
if !context_match {
|
||||
println!("\nExpected context ({:?}):", expected_context_range);
|
||||
println!("---\n{}---", range_text(&expected_context_range));
|
||||
println!("\nActual context ({:?}):", actual_context);
|
||||
println!("---\n{}---", range_text(&actual_context));
|
||||
}
|
||||
panic!("Test '{}' failed - see output above", test_case.name);
|
||||
}
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
3061
crates/edit_prediction/src/edit_prediction.rs
Normal file
3061
crates/edit_prediction/src/edit_prediction.rs
Normal file
File diff suppressed because it is too large
Load Diff
3804
crates/edit_prediction/src/edit_prediction_tests.rs
Normal file
3804
crates/edit_prediction/src/edit_prediction_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
855
crates/edit_prediction/src/example_spec.rs
Normal file
855
crates/edit_prediction/src/example_spec.rs
Normal file
@@ -0,0 +1,855 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{borrow::Cow, fmt::Write as _, mem, path::Path, sync::Arc};
|
||||
use telemetry_events::EditPredictionRating;
|
||||
|
||||
pub use zeta_prompt::udiff::{
|
||||
CURSOR_POSITION_MARKER, encode_cursor_in_patch, extract_cursor_from_patch,
|
||||
};
|
||||
pub const INLINE_CURSOR_MARKER: &str = "<|user_cursor|>";
|
||||
|
||||
/// Maximum cursor file size to capture (64KB).
|
||||
/// Files larger than this will not have their content captured,
|
||||
/// falling back to git-based loading.
|
||||
pub const MAX_CURSOR_FILE_SIZE: usize = 64 * 1024;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct ExampleSpec {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
pub repository_url: String,
|
||||
pub revision: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning: Option<String>,
|
||||
#[serde(default)]
|
||||
pub uncommitted_diff: String,
|
||||
pub cursor_path: Arc<Path>,
|
||||
pub cursor_position: String,
|
||||
pub edit_history: String,
|
||||
pub expected_patches: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub rejected_patch: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub telemetry: Option<TelemetrySource>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub human_feedback: Vec<HumanFeedback>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub rating: Option<EditPredictionRating>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct HumanFeedback {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Metadata for examples sourced from production telemetry (rejected predictions).
|
||||
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct TelemetrySource {
|
||||
pub request_id: String,
|
||||
pub device_id: String,
|
||||
pub time: String,
|
||||
pub rejection_reason: String,
|
||||
pub was_shown: bool,
|
||||
}
|
||||
|
||||
const REASONING_HEADING: &str = "Reasoning";
|
||||
const UNCOMMITTED_DIFF_HEADING: &str = "Uncommitted Diff";
|
||||
const EDIT_HISTORY_HEADING: &str = "Edit History";
|
||||
const CURSOR_POSITION_HEADING: &str = "Cursor Position";
|
||||
const EXPECTED_PATCH_HEADING: &str = "Expected Patch";
|
||||
const REJECTED_PATCH_HEADING: &str = "Rejected Patch";
|
||||
const ACCEPTED_PREDICTION_MARKER: &str = "// User accepted prediction:";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct FrontMatter<'a> {
|
||||
repository_url: Cow<'a, str>,
|
||||
revision: Cow<'a, str>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
impl ExampleSpec {
|
||||
/// Generate a sanitized filename for this example.
|
||||
pub fn filename(&self) -> String {
|
||||
self.name
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
' ' | ':' | '~' | '^' | '?' | '*' | '[' | '\\' | '@' | '{' | '/' | '<' | '>'
|
||||
| '|' | '"' => '-',
|
||||
c => c,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Format this example spec as markdown.
|
||||
pub fn to_markdown(&self) -> String {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
let front_matter = FrontMatter {
|
||||
repository_url: Cow::Borrowed(&self.repository_url),
|
||||
revision: Cow::Borrowed(&self.revision),
|
||||
tags: self.tags.clone(),
|
||||
};
|
||||
let front_matter_toml =
|
||||
toml::to_string_pretty(&front_matter).unwrap_or_else(|_| String::new());
|
||||
|
||||
let mut markdown = String::new();
|
||||
|
||||
_ = writeln!(markdown, "+++");
|
||||
markdown.push_str(&front_matter_toml);
|
||||
if !markdown.ends_with('\n') {
|
||||
markdown.push('\n');
|
||||
}
|
||||
_ = writeln!(markdown, "+++");
|
||||
markdown.push('\n');
|
||||
|
||||
_ = writeln!(markdown, "# {}", self.name);
|
||||
markdown.push('\n');
|
||||
|
||||
if let Some(reasoning) = &self.reasoning {
|
||||
_ = writeln!(markdown, "## {}", REASONING_HEADING);
|
||||
markdown.push('\n');
|
||||
markdown.push_str(reasoning);
|
||||
if !markdown.ends_with('\n') {
|
||||
markdown.push('\n');
|
||||
}
|
||||
markdown.push('\n');
|
||||
}
|
||||
|
||||
if !self.uncommitted_diff.is_empty() {
|
||||
_ = writeln!(markdown, "## {}", UNCOMMITTED_DIFF_HEADING);
|
||||
_ = writeln!(markdown);
|
||||
_ = writeln!(markdown, "```diff");
|
||||
markdown.push_str(&self.uncommitted_diff);
|
||||
if !markdown.ends_with('\n') {
|
||||
markdown.push('\n');
|
||||
}
|
||||
_ = writeln!(markdown, "```");
|
||||
markdown.push('\n');
|
||||
}
|
||||
|
||||
_ = writeln!(markdown, "## {}", EDIT_HISTORY_HEADING);
|
||||
_ = writeln!(markdown);
|
||||
|
||||
if self.edit_history.is_empty() {
|
||||
_ = writeln!(markdown, "(No edit history)");
|
||||
_ = writeln!(markdown);
|
||||
} else {
|
||||
_ = writeln!(markdown, "```diff");
|
||||
markdown.push_str(&self.edit_history);
|
||||
if !markdown.ends_with('\n') {
|
||||
markdown.push('\n');
|
||||
}
|
||||
_ = writeln!(markdown, "```");
|
||||
markdown.push('\n');
|
||||
}
|
||||
|
||||
_ = writeln!(markdown, "## {}", CURSOR_POSITION_HEADING);
|
||||
_ = writeln!(markdown);
|
||||
_ = writeln!(markdown, "```{}", self.cursor_path.to_string_lossy());
|
||||
markdown.push_str(&self.cursor_position);
|
||||
if !markdown.ends_with('\n') {
|
||||
markdown.push('\n');
|
||||
}
|
||||
_ = writeln!(markdown, "```");
|
||||
markdown.push('\n');
|
||||
|
||||
_ = writeln!(markdown, "## {}", EXPECTED_PATCH_HEADING);
|
||||
markdown.push('\n');
|
||||
for patch in &self.expected_patches {
|
||||
_ = writeln!(markdown, "```diff");
|
||||
markdown.push_str(patch);
|
||||
if !markdown.ends_with('\n') {
|
||||
markdown.push('\n');
|
||||
}
|
||||
_ = writeln!(markdown, "```");
|
||||
markdown.push('\n');
|
||||
}
|
||||
|
||||
if let Some(rejected_patch) = &self.rejected_patch {
|
||||
_ = writeln!(markdown, "## {}", REJECTED_PATCH_HEADING);
|
||||
markdown.push('\n');
|
||||
_ = writeln!(markdown, "```diff");
|
||||
markdown.push_str(rejected_patch);
|
||||
if !markdown.ends_with('\n') {
|
||||
markdown.push('\n');
|
||||
}
|
||||
_ = writeln!(markdown, "```");
|
||||
markdown.push('\n');
|
||||
}
|
||||
|
||||
markdown
|
||||
}
|
||||
|
||||
/// Parse an example spec from markdown.
|
||||
pub fn from_markdown(mut input: &str) -> anyhow::Result<Self> {
|
||||
use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Parser, Tag, TagEnd};
|
||||
|
||||
let mut spec = ExampleSpec {
|
||||
name: String::new(),
|
||||
repository_url: String::new(),
|
||||
revision: String::new(),
|
||||
tags: Vec::new(),
|
||||
reasoning: None,
|
||||
uncommitted_diff: String::new(),
|
||||
cursor_path: Path::new("").into(),
|
||||
cursor_position: String::new(),
|
||||
edit_history: String::new(),
|
||||
expected_patches: Vec::new(),
|
||||
rejected_patch: None,
|
||||
telemetry: None,
|
||||
human_feedback: Vec::new(),
|
||||
rating: None,
|
||||
};
|
||||
|
||||
if let Some(rest) = input.strip_prefix("+++\n")
|
||||
&& let Some((front_matter, rest)) = rest.split_once("+++\n")
|
||||
{
|
||||
if let Ok(data) = toml::from_str::<FrontMatter<'_>>(front_matter) {
|
||||
spec.repository_url = data.repository_url.into_owned();
|
||||
spec.revision = data.revision.into_owned();
|
||||
spec.tags = data.tags;
|
||||
}
|
||||
input = rest.trim_start();
|
||||
}
|
||||
|
||||
let parser = Parser::new(input);
|
||||
let mut text = String::new();
|
||||
let mut block_info: CowStr = "".into();
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum Section {
|
||||
Start,
|
||||
UncommittedDiff,
|
||||
EditHistory,
|
||||
CursorPosition,
|
||||
ExpectedPatch,
|
||||
RejectedPatch,
|
||||
Other,
|
||||
}
|
||||
|
||||
let mut current_section = Section::Start;
|
||||
let mut next_edit_predicted = false;
|
||||
|
||||
for event in parser {
|
||||
match event {
|
||||
Event::Text(line) => {
|
||||
text.push_str(&line);
|
||||
}
|
||||
Event::End(TagEnd::Heading(HeadingLevel::H1)) => {
|
||||
spec.name = mem::take(&mut text);
|
||||
}
|
||||
Event::End(TagEnd::Heading(HeadingLevel::H2)) => {
|
||||
let title = mem::take(&mut text);
|
||||
current_section = if title.eq_ignore_ascii_case(UNCOMMITTED_DIFF_HEADING) {
|
||||
Section::UncommittedDiff
|
||||
} else if title.eq_ignore_ascii_case(EDIT_HISTORY_HEADING) {
|
||||
Section::EditHistory
|
||||
} else if title.eq_ignore_ascii_case(CURSOR_POSITION_HEADING) {
|
||||
Section::CursorPosition
|
||||
} else if title.eq_ignore_ascii_case(EXPECTED_PATCH_HEADING) {
|
||||
Section::ExpectedPatch
|
||||
} else if title.eq_ignore_ascii_case(REJECTED_PATCH_HEADING) {
|
||||
Section::RejectedPatch
|
||||
} else {
|
||||
Section::Other
|
||||
};
|
||||
}
|
||||
Event::End(TagEnd::Heading(HeadingLevel::H3)) => {
|
||||
mem::take(&mut text);
|
||||
}
|
||||
Event::End(TagEnd::Heading(HeadingLevel::H4)) => {
|
||||
mem::take(&mut text);
|
||||
}
|
||||
Event::End(TagEnd::Heading(level)) => {
|
||||
anyhow::bail!("Unexpected heading level: {level}");
|
||||
}
|
||||
Event::Start(Tag::CodeBlock(kind)) => {
|
||||
if current_section == Section::EditHistory
|
||||
&& text.trim() == ACCEPTED_PREDICTION_MARKER
|
||||
{
|
||||
next_edit_predicted = true;
|
||||
}
|
||||
text.clear();
|
||||
match kind {
|
||||
CodeBlockKind::Fenced(info) => {
|
||||
block_info = info;
|
||||
}
|
||||
CodeBlockKind::Indented => {
|
||||
anyhow::bail!("Unexpected indented codeblock");
|
||||
}
|
||||
};
|
||||
}
|
||||
Event::Start(_) => {
|
||||
text.clear();
|
||||
block_info = "".into();
|
||||
}
|
||||
Event::End(TagEnd::CodeBlock) => {
|
||||
let block_info = block_info.trim();
|
||||
match current_section {
|
||||
Section::UncommittedDiff => {
|
||||
spec.uncommitted_diff = mem::take(&mut text);
|
||||
}
|
||||
Section::EditHistory => {
|
||||
if next_edit_predicted {
|
||||
spec.edit_history
|
||||
.push_str(&format!("{}\n", ACCEPTED_PREDICTION_MARKER));
|
||||
next_edit_predicted = false;
|
||||
}
|
||||
spec.edit_history.push_str(&mem::take(&mut text));
|
||||
}
|
||||
Section::CursorPosition => {
|
||||
spec.cursor_path = Path::new(block_info).into();
|
||||
spec.cursor_position = mem::take(&mut text);
|
||||
}
|
||||
Section::ExpectedPatch => {
|
||||
spec.expected_patches.push(mem::take(&mut text));
|
||||
}
|
||||
Section::RejectedPatch => {
|
||||
spec.rejected_patch = Some(mem::take(&mut text));
|
||||
}
|
||||
Section::Start | Section::Other => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if spec.cursor_path.as_ref() == Path::new("") || spec.cursor_position.is_empty() {
|
||||
anyhow::bail!("Missing cursor position codeblock");
|
||||
}
|
||||
|
||||
Ok(spec)
|
||||
}
|
||||
|
||||
/// Returns the excerpt of text around the cursor, and the offset of the cursor within that
|
||||
/// excerpt.
|
||||
///
|
||||
/// The cursor's position is marked with a special comment that appears
|
||||
/// below the cursor line, which contains the string `[CURSOR_POSITION]`,
|
||||
/// preceded by an arrow marking the cursor's column. The arrow can be
|
||||
/// either:
|
||||
/// - `^` - The cursor column is at the position of the `^` character (pointing up to the cursor)
|
||||
/// - `<` - The cursor column is at the first non-whitespace character on that line.
|
||||
pub fn cursor_excerpt(&self) -> Result<(String, usize)> {
|
||||
let input = &self.cursor_position;
|
||||
|
||||
// Check for inline cursor marker first
|
||||
if let Some(inline_offset) = input.find(INLINE_CURSOR_MARKER) {
|
||||
let excerpt = input[..inline_offset].to_string()
|
||||
+ &input[inline_offset + INLINE_CURSOR_MARKER.len()..];
|
||||
return Ok((excerpt, inline_offset));
|
||||
}
|
||||
|
||||
let marker_offset = input
|
||||
.find(CURSOR_POSITION_MARKER)
|
||||
.context("missing [CURSOR_POSITION] marker")?;
|
||||
let marker_line_start = input[..marker_offset]
|
||||
.rfind('\n')
|
||||
.map(|pos| pos + 1)
|
||||
.unwrap_or(0);
|
||||
let marker_line_end = input[marker_line_start..]
|
||||
.find('\n')
|
||||
.map(|pos| marker_line_start + pos + 1)
|
||||
.unwrap_or(input.len());
|
||||
let marker_line = &input[marker_line_start..marker_line_end].trim_end_matches('\n');
|
||||
|
||||
let cursor_column = if let Some(cursor_offset) = marker_line.find('^') {
|
||||
cursor_offset
|
||||
} else if let Some(less_than_pos) = marker_line.find('<') {
|
||||
marker_line
|
||||
.find(|c: char| !c.is_whitespace())
|
||||
.unwrap_or(less_than_pos)
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"cursor position marker line must contain '^' or '<' before [CURSOR_POSITION]"
|
||||
);
|
||||
};
|
||||
|
||||
let mut excerpt = input[..marker_line_start].to_string() + &input[marker_line_end..];
|
||||
excerpt.truncate(excerpt.trim_end_matches('\n').len());
|
||||
|
||||
// The cursor is on the line above the marker line.
|
||||
let cursor_line_end = marker_line_start.saturating_sub(1);
|
||||
let cursor_line_start = excerpt[..cursor_line_end]
|
||||
.rfind('\n')
|
||||
.map(|pos| pos + 1)
|
||||
.unwrap_or(0);
|
||||
let cursor_offset = cursor_line_start + cursor_column;
|
||||
|
||||
Ok((excerpt, cursor_offset))
|
||||
}
|
||||
|
||||
/// Sets the cursor position excerpt from a plain excerpt and cursor byte offset.
|
||||
///
|
||||
/// The `line_comment_prefix` is used to format the marker line as a comment.
|
||||
/// If the cursor column is less than the comment prefix length, the `<` format is used.
|
||||
/// Otherwise, the `^` format is used.
|
||||
pub fn set_cursor_excerpt(
|
||||
&mut self,
|
||||
excerpt: &str,
|
||||
cursor_offset: usize,
|
||||
line_comment_prefix: &str,
|
||||
) {
|
||||
// Find which line the cursor is on and its column
|
||||
let cursor_line_start = excerpt[..cursor_offset]
|
||||
.rfind('\n')
|
||||
.map(|pos| pos + 1)
|
||||
.unwrap_or(0);
|
||||
let cursor_line_end = excerpt[cursor_line_start..]
|
||||
.find('\n')
|
||||
.map(|pos| cursor_line_start + pos + 1)
|
||||
.unwrap_or(excerpt.len());
|
||||
let cursor_line = &excerpt[cursor_line_start..cursor_line_end];
|
||||
let cursor_line_indent = &cursor_line[..cursor_line.len() - cursor_line.trim_start().len()];
|
||||
let cursor_column = cursor_offset - cursor_line_start;
|
||||
|
||||
// Build the marker line
|
||||
let mut marker_line = String::new();
|
||||
if cursor_column < line_comment_prefix.len() {
|
||||
for _ in 0..cursor_column {
|
||||
marker_line.push(' ');
|
||||
}
|
||||
marker_line.push_str(line_comment_prefix);
|
||||
write!(marker_line, " <{}", CURSOR_POSITION_MARKER).unwrap();
|
||||
} else {
|
||||
if cursor_column >= cursor_line_indent.len() + line_comment_prefix.len() {
|
||||
marker_line.push_str(cursor_line_indent);
|
||||
}
|
||||
marker_line.push_str(line_comment_prefix);
|
||||
while marker_line.len() < cursor_column {
|
||||
marker_line.push(' ');
|
||||
}
|
||||
write!(marker_line, "^{}", CURSOR_POSITION_MARKER).unwrap();
|
||||
}
|
||||
|
||||
// Build the final cursor_position string
|
||||
let mut result = String::with_capacity(excerpt.len() + marker_line.len() + 2);
|
||||
result.push_str(&excerpt[..cursor_line_end]);
|
||||
if !result.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
result.push_str(&marker_line);
|
||||
if cursor_line_end < excerpt.len() {
|
||||
result.push('\n');
|
||||
result.push_str(&excerpt[cursor_line_end..]);
|
||||
}
|
||||
|
||||
self.cursor_position = result;
|
||||
}
|
||||
|
||||
/// Returns all of the possible expected patches for this example, each with an optional
|
||||
/// cursor offset.
|
||||
///
|
||||
/// The cursor offset is an offset within the new text (after applying the patch), relative
|
||||
/// to the start of the hunk.
|
||||
///
|
||||
/// In the serialized representation of this example, the cursor position is represented
|
||||
/// using a comment line in the diff, beginning with `#`, and containing a `[CURSOR_POSITION]`
|
||||
/// marker with the same format as the [`Self::cursor_excerpt`].
|
||||
pub fn expected_patches_with_cursor_positions(&self) -> Vec<(String, Option<usize>)> {
|
||||
self.expected_patches
|
||||
.iter()
|
||||
.map(|patch| extract_cursor_from_patch(patch))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn set_expected_patches_with_cursor_positions(
|
||||
&mut self,
|
||||
patches: Vec<(String, Option<usize>)>,
|
||||
) {
|
||||
self.expected_patches = patches
|
||||
.into_iter()
|
||||
.map(|(patch, cursor_offset)| encode_cursor_in_patch(&patch, cursor_offset))
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use indoc::indoc;
|
||||
|
||||
#[test]
|
||||
fn test_cursor_excerpt_with_caret() {
|
||||
let mut spec = ExampleSpec {
|
||||
name: String::new(),
|
||||
repository_url: String::new(),
|
||||
revision: String::new(),
|
||||
tags: Vec::new(),
|
||||
reasoning: None,
|
||||
uncommitted_diff: String::new(),
|
||||
cursor_path: Path::new("test.rs").into(),
|
||||
cursor_position: String::new(),
|
||||
edit_history: String::new(),
|
||||
expected_patches: Vec::new(),
|
||||
rejected_patch: None,
|
||||
telemetry: None,
|
||||
human_feedback: Vec::new(),
|
||||
rating: None,
|
||||
};
|
||||
|
||||
// Cursor before `42`
|
||||
let excerpt = indoc! {"
|
||||
fn main() {
|
||||
let x = 42;
|
||||
println!(\"{}\", x);
|
||||
}"
|
||||
};
|
||||
let offset = excerpt.find("42").unwrap();
|
||||
let position_string = indoc! {"
|
||||
fn main() {
|
||||
let x = 42;
|
||||
// ^[CURSOR_POSITION]
|
||||
println!(\"{}\", x);
|
||||
}"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
spec.set_cursor_excerpt(excerpt, offset, "//");
|
||||
assert_eq!(spec.cursor_position, position_string);
|
||||
assert_eq!(
|
||||
spec.cursor_excerpt().unwrap(),
|
||||
(excerpt.to_string(), offset)
|
||||
);
|
||||
|
||||
// Cursor after `l` in `let`
|
||||
let offset = excerpt.find("et x").unwrap();
|
||||
let position_string = indoc! {"
|
||||
fn main() {
|
||||
let x = 42;
|
||||
// ^[CURSOR_POSITION]
|
||||
println!(\"{}\", x);
|
||||
}"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
spec.set_cursor_excerpt(excerpt, offset, "//");
|
||||
assert_eq!(spec.cursor_position, position_string);
|
||||
assert_eq!(
|
||||
spec.cursor_excerpt().unwrap(),
|
||||
(excerpt.to_string(), offset)
|
||||
);
|
||||
|
||||
// Cursor before `let`
|
||||
let offset = excerpt.find("let").unwrap();
|
||||
let position_string = indoc! {"
|
||||
fn main() {
|
||||
let x = 42;
|
||||
// ^[CURSOR_POSITION]
|
||||
println!(\"{}\", x);
|
||||
}"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
spec.set_cursor_excerpt(excerpt, offset, "//");
|
||||
assert_eq!(spec.cursor_position, position_string);
|
||||
assert_eq!(
|
||||
spec.cursor_excerpt().unwrap(),
|
||||
(excerpt.to_string(), offset)
|
||||
);
|
||||
|
||||
// Cursor at beginning of the line with `let`
|
||||
let offset = excerpt.find(" let").unwrap();
|
||||
let position_string = indoc! {"
|
||||
fn main() {
|
||||
let x = 42;
|
||||
// <[CURSOR_POSITION]
|
||||
println!(\"{}\", x);
|
||||
}"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
spec.set_cursor_excerpt(excerpt, offset, "//");
|
||||
assert_eq!(spec.cursor_position, position_string);
|
||||
assert_eq!(
|
||||
spec.cursor_excerpt().unwrap(),
|
||||
(excerpt.to_string(), offset)
|
||||
);
|
||||
|
||||
// Cursor at end of line, after the semicolon
|
||||
let offset = excerpt.find(';').unwrap() + 1;
|
||||
let position_string = indoc! {"
|
||||
fn main() {
|
||||
let x = 42;
|
||||
// ^[CURSOR_POSITION]
|
||||
println!(\"{}\", x);
|
||||
}"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
spec.set_cursor_excerpt(excerpt, offset, "//");
|
||||
assert_eq!(spec.cursor_position, position_string);
|
||||
assert_eq!(
|
||||
spec.cursor_excerpt().unwrap(),
|
||||
(excerpt.to_string(), offset)
|
||||
);
|
||||
|
||||
// Caret at end of file (no trailing newline)
|
||||
let excerpt = indoc! {"
|
||||
fn main() {
|
||||
let x = 42;"
|
||||
};
|
||||
let offset = excerpt.find(';').unwrap() + 1;
|
||||
let position_string = indoc! {"
|
||||
fn main() {
|
||||
let x = 42;
|
||||
// ^[CURSOR_POSITION]"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
spec.set_cursor_excerpt(excerpt, offset, "//");
|
||||
assert_eq!(spec.cursor_position, position_string);
|
||||
assert_eq!(
|
||||
spec.cursor_excerpt().unwrap(),
|
||||
(excerpt.to_string(), offset)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cursor_excerpt_with_inline_marker() {
|
||||
let mut spec = ExampleSpec {
|
||||
name: String::new(),
|
||||
repository_url: String::new(),
|
||||
revision: String::new(),
|
||||
tags: Vec::new(),
|
||||
reasoning: None,
|
||||
uncommitted_diff: String::new(),
|
||||
cursor_path: Path::new("test.rs").into(),
|
||||
cursor_position: String::new(),
|
||||
edit_history: String::new(),
|
||||
expected_patches: Vec::new(),
|
||||
rejected_patch: None,
|
||||
telemetry: None,
|
||||
human_feedback: Vec::new(),
|
||||
rating: None,
|
||||
};
|
||||
|
||||
// Cursor before `42` using inline marker
|
||||
spec.cursor_position = indoc! {"
|
||||
fn main() {
|
||||
let x = <|user_cursor|>42;
|
||||
println!(\"{}\", x);
|
||||
}"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let expected_excerpt = indoc! {"
|
||||
fn main() {
|
||||
let x = 42;
|
||||
println!(\"{}\", x);
|
||||
}"
|
||||
};
|
||||
let expected_offset = expected_excerpt.find("42").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
spec.cursor_excerpt().unwrap(),
|
||||
(expected_excerpt.to_string(), expected_offset)
|
||||
);
|
||||
|
||||
// Cursor at beginning of line
|
||||
spec.cursor_position = indoc! {"
|
||||
fn main() {
|
||||
<|user_cursor|> let x = 42;
|
||||
}"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let expected_excerpt = indoc! {"
|
||||
fn main() {
|
||||
let x = 42;
|
||||
}"
|
||||
};
|
||||
let expected_offset = expected_excerpt.find(" let").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
spec.cursor_excerpt().unwrap(),
|
||||
(expected_excerpt.to_string(), expected_offset)
|
||||
);
|
||||
|
||||
// Cursor at end of file
|
||||
spec.cursor_position = "fn main() {}<|user_cursor|>".to_string();
|
||||
let expected_excerpt = "fn main() {}";
|
||||
let expected_offset = expected_excerpt.len();
|
||||
|
||||
assert_eq!(
|
||||
spec.cursor_excerpt().unwrap(),
|
||||
(expected_excerpt.to_string(), expected_offset)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expected_patches_with_cursor_positions() {
|
||||
let mut spec = ExampleSpec {
|
||||
name: String::new(),
|
||||
repository_url: String::new(),
|
||||
revision: String::new(),
|
||||
tags: Vec::new(),
|
||||
reasoning: None,
|
||||
uncommitted_diff: String::new(),
|
||||
cursor_path: Path::new("test.rs").into(),
|
||||
cursor_position: String::new(),
|
||||
edit_history: String::new(),
|
||||
expected_patches: Vec::new(),
|
||||
rejected_patch: None,
|
||||
telemetry: None,
|
||||
human_feedback: Vec::new(),
|
||||
rating: None,
|
||||
};
|
||||
|
||||
let new_content = indoc! {r#"
|
||||
// prints a greeting
|
||||
fn main() {
|
||||
println!("hello, {}", );
|
||||
let x = 42;
|
||||
}
|
||||
"#};
|
||||
let cursor_offset = new_content.find(");").unwrap();
|
||||
|
||||
let clean_patch = indoc! {r#"
|
||||
--- a/test.rs
|
||||
+++ b/test.rs
|
||||
@@ -1,3 +1,4 @@
|
||||
+// prints a greeting
|
||||
fn main() {
|
||||
- println!("hi");
|
||||
+ println!("hello, {}", );
|
||||
let x = 42;
|
||||
}
|
||||
"#}
|
||||
.to_string();
|
||||
|
||||
let encoded_patch = indoc! {r#"
|
||||
--- a/test.rs
|
||||
+++ b/test.rs
|
||||
@@ -1,3 +1,4 @@
|
||||
+// prints a greeting
|
||||
fn main() {
|
||||
- println!("hi");
|
||||
+ println!("hello, {}", );
|
||||
# ^[CURSOR_POSITION]
|
||||
let x = 42;
|
||||
}
|
||||
"#}
|
||||
.to_string();
|
||||
|
||||
spec.set_expected_patches_with_cursor_positions(vec![(
|
||||
clean_patch.clone(),
|
||||
Some(cursor_offset),
|
||||
)]);
|
||||
assert_eq!(spec.expected_patches, vec![encoded_patch]);
|
||||
|
||||
let results = spec.expected_patches_with_cursor_positions();
|
||||
assert_eq!(results, vec![(clean_patch.clone(), Some(cursor_offset))]);
|
||||
|
||||
spec.set_expected_patches_with_cursor_positions(vec![(clean_patch.clone(), None)]);
|
||||
assert_eq!(spec.expected_patches, vec![clean_patch.clone()]);
|
||||
|
||||
let results = spec.expected_patches_with_cursor_positions();
|
||||
assert_eq!(results, vec![(clean_patch, None)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_cursor_in_patch_is_idempotent() {
|
||||
let patch = indoc! {r#"
|
||||
--- a/test.rs
|
||||
+++ b/test.rs
|
||||
@@ -1,2 +1,2 @@
|
||||
-fn old() {}
|
||||
+fn new_name() {}
|
||||
# ^[CURSOR_POSITION]
|
||||
"#};
|
||||
|
||||
let cursor_offset = "fn new_name() {}".find("name").unwrap();
|
||||
let encoded_once = encode_cursor_in_patch(patch, Some(cursor_offset));
|
||||
let encoded_twice = encode_cursor_in_patch(&encoded_once, Some(cursor_offset));
|
||||
|
||||
assert_eq!(encoded_once, encoded_twice);
|
||||
assert_eq!(
|
||||
encoded_once
|
||||
.lines()
|
||||
.filter(|line| line.contains(CURSOR_POSITION_MARKER))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_markdown_accepted_prediction_marker() {
|
||||
let markdown = indoc! {r#"
|
||||
+++
|
||||
repository_url = "https://github.com/example/repo"
|
||||
revision = "abc123"
|
||||
+++
|
||||
|
||||
## Edit History
|
||||
|
||||
```diff
|
||||
--- a/src/main.rs
|
||||
+++ b/src/main.rs
|
||||
@@ -1,3 +1,3 @@
|
||||
-fn hello() {}
|
||||
+fn hello_world() {}
|
||||
```
|
||||
|
||||
// User accepted prediction:
|
||||
```diff
|
||||
--- a/src/main.rs
|
||||
+++ b/src/main.rs
|
||||
@@ -1,3 +1,3 @@
|
||||
-fn hello_world() {}
|
||||
+fn hello_world() { println!("hi"); }
|
||||
```
|
||||
|
||||
```diff
|
||||
--- a/src/main.rs
|
||||
+++ b/src/main.rs
|
||||
@@ -1,3 +1,3 @@
|
||||
-fn hello_world() { println!("hi"); }
|
||||
+fn hello_world() { println!("hello"); }
|
||||
```
|
||||
|
||||
## Cursor Position
|
||||
|
||||
```src/main.rs
|
||||
fn hello_world() { println!("hello"); }
|
||||
# ^[CURSOR_POSITION]
|
||||
```
|
||||
|
||||
## Expected Patch
|
||||
|
||||
```diff
|
||||
--- a/src/main.rs
|
||||
+++ b/src/main.rs
|
||||
@@ -1,3 +1,3 @@
|
||||
-fn hello_world() { println!("hello"); }
|
||||
+fn hello_world() { println!("hello, world!"); }
|
||||
```
|
||||
"#};
|
||||
|
||||
let spec = ExampleSpec::from_markdown(markdown).unwrap();
|
||||
|
||||
// The first diff should NOT have the marker
|
||||
assert!(spec.edit_history.starts_with("--- a/src/main.rs"));
|
||||
|
||||
// The second diff should be preceded by the accepted prediction marker
|
||||
assert!(
|
||||
spec.edit_history
|
||||
.contains("// User accepted prediction:\n--- a/src/main.rs")
|
||||
);
|
||||
|
||||
// Count occurrences of the marker - should be exactly one
|
||||
let marker_count = spec
|
||||
.edit_history
|
||||
.matches("// User accepted prediction:")
|
||||
.count();
|
||||
assert_eq!(marker_count, 1);
|
||||
|
||||
// The third diff should NOT have the marker
|
||||
// Verify all three diffs are present
|
||||
let diff_count = spec.edit_history.matches("--- a/src/main.rs").count();
|
||||
assert_eq!(diff_count, 3);
|
||||
}
|
||||
}
|
||||
234
crates/edit_prediction/src/fim.rs
Normal file
234
crates/edit_prediction/src/fim.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use crate::{
|
||||
EditPredictionId, EditPredictionModelInput, cursor_excerpt,
|
||||
open_ai_compatible::{self, load_open_ai_compatible_api_key_if_needed},
|
||||
prediction::EditPredictionResult,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use gpui::{App, AppContext as _, Entity, Task};
|
||||
use language::{
|
||||
Anchor, Buffer, BufferSnapshot, EditPredictionPromptFormat, ToOffset, ToPoint as _,
|
||||
language_settings::all_language_settings,
|
||||
};
|
||||
use std::{path::Path, sync::Arc, time::Instant};
|
||||
use zeta_prompt::{ZetaPromptInput, compute_editable_and_context_ranges};
|
||||
|
||||
const FIM_CONTEXT_TOKENS: usize = 512;
|
||||
|
||||
struct FimRequestOutput {
|
||||
request_id: String,
|
||||
edits: Vec<(std::ops::Range<Anchor>, Arc<str>)>,
|
||||
snapshot: BufferSnapshot,
|
||||
inputs: ZetaPromptInput,
|
||||
buffer: Entity<Buffer>,
|
||||
}
|
||||
|
||||
pub fn request_prediction(
|
||||
EditPredictionModelInput {
|
||||
buffer,
|
||||
snapshot,
|
||||
position,
|
||||
events,
|
||||
..
|
||||
}: EditPredictionModelInput,
|
||||
prompt_format: EditPredictionPromptFormat,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<EditPredictionResult>>> {
|
||||
let settings = &all_language_settings(None, cx).edit_predictions;
|
||||
let provider = settings.provider;
|
||||
|
||||
let full_path: Arc<Path> = snapshot
|
||||
.file()
|
||||
.map(|file| file.full_path(cx))
|
||||
.unwrap_or_else(|| "untitled".into())
|
||||
.into();
|
||||
|
||||
let http_client = cx.http_client();
|
||||
let cursor_point = position.to_point(&snapshot);
|
||||
let request_start = cx.background_executor().now();
|
||||
|
||||
let Some(settings) = (match provider {
|
||||
settings::EditPredictionProvider::Ollama => settings.ollama.clone(),
|
||||
settings::EditPredictionProvider::OpenAiCompatibleApi => {
|
||||
settings.open_ai_compatible_api.clone()
|
||||
}
|
||||
_ => None,
|
||||
}) else {
|
||||
return Task::ready(Err(anyhow!("Unsupported edit prediction provider for FIM")));
|
||||
};
|
||||
|
||||
let api_key = load_open_ai_compatible_api_key_if_needed(provider, cx);
|
||||
|
||||
let result = cx.background_spawn(async move {
|
||||
let cursor_offset = cursor_point.to_offset(&snapshot);
|
||||
let (excerpt_point_range, excerpt_offset_range, cursor_offset_in_excerpt) =
|
||||
cursor_excerpt::compute_cursor_excerpt(&snapshot, cursor_offset);
|
||||
let cursor_excerpt: Arc<str> = snapshot
|
||||
.text_for_range(excerpt_point_range.clone())
|
||||
.collect::<String>()
|
||||
.into();
|
||||
let syntax_ranges =
|
||||
cursor_excerpt::compute_syntax_ranges(&snapshot, cursor_offset, &excerpt_offset_range);
|
||||
let (editable_range, _) = compute_editable_and_context_ranges(
|
||||
&cursor_excerpt,
|
||||
cursor_offset_in_excerpt,
|
||||
&syntax_ranges,
|
||||
FIM_CONTEXT_TOKENS,
|
||||
0,
|
||||
);
|
||||
|
||||
let inputs = ZetaPromptInput {
|
||||
events,
|
||||
related_files: Some(Vec::new()),
|
||||
active_buffer_diagnostics: Vec::new(),
|
||||
cursor_offset_in_excerpt: cursor_offset - excerpt_offset_range.start,
|
||||
cursor_path: full_path.clone(),
|
||||
excerpt_start_row: Some(excerpt_point_range.start.row),
|
||||
cursor_excerpt,
|
||||
excerpt_ranges: Default::default(),
|
||||
syntax_ranges: None,
|
||||
in_open_source_repo: false,
|
||||
can_collect_data: false,
|
||||
repo_url: None,
|
||||
};
|
||||
|
||||
let editable_text = &inputs.cursor_excerpt[editable_range.clone()];
|
||||
let cursor_in_editable = cursor_offset_in_excerpt.saturating_sub(editable_range.start);
|
||||
let prefix = editable_text[..cursor_in_editable].to_string();
|
||||
let suffix = editable_text[cursor_in_editable..].to_string();
|
||||
let prompt = format_fim_prompt(prompt_format, &prefix, &suffix);
|
||||
let stop_tokens = get_fim_stop_tokens();
|
||||
|
||||
let max_tokens = settings.max_output_tokens;
|
||||
|
||||
let (response_text, request_id) = open_ai_compatible::send_custom_server_request(
|
||||
provider,
|
||||
&settings,
|
||||
prompt,
|
||||
max_tokens,
|
||||
stop_tokens,
|
||||
api_key,
|
||||
&http_client,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response_received_at = Instant::now();
|
||||
|
||||
log::debug!(
|
||||
"fim: completion received ({:.2}s)",
|
||||
(response_received_at - request_start).as_secs_f64()
|
||||
);
|
||||
|
||||
let completion: Arc<str> = clean_fim_completion(&response_text).into();
|
||||
let edits = if completion.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
let cursor_offset = cursor_point.to_offset(&snapshot);
|
||||
let anchor = snapshot.anchor_after(cursor_offset);
|
||||
vec![(anchor..anchor, completion)]
|
||||
};
|
||||
|
||||
anyhow::Ok(FimRequestOutput {
|
||||
request_id,
|
||||
edits,
|
||||
snapshot,
|
||||
inputs,
|
||||
buffer,
|
||||
})
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx: &mut gpui::AsyncApp| {
|
||||
let output = result.await.context("fim edit prediction failed")?;
|
||||
anyhow::Ok(Some(
|
||||
EditPredictionResult::new(
|
||||
EditPredictionId(output.request_id.into()),
|
||||
&output.buffer,
|
||||
&output.snapshot,
|
||||
output.edits.into(),
|
||||
None,
|
||||
output.inputs,
|
||||
None,
|
||||
cx.background_executor().now() - request_start,
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn format_fim_prompt(
|
||||
prompt_format: EditPredictionPromptFormat,
|
||||
prefix: &str,
|
||||
suffix: &str,
|
||||
) -> String {
|
||||
match prompt_format {
|
||||
EditPredictionPromptFormat::CodeLlama => {
|
||||
format!("<PRE> {prefix} <SUF>{suffix} <MID>")
|
||||
}
|
||||
EditPredictionPromptFormat::StarCoder => {
|
||||
format!("<fim_prefix>{prefix}<fim_suffix>{suffix}<fim_middle>")
|
||||
}
|
||||
EditPredictionPromptFormat::DeepseekCoder => {
|
||||
format!("<|fim▁begin|>{prefix}<|fim▁hole|>{suffix}<|fim▁end|>")
|
||||
}
|
||||
EditPredictionPromptFormat::Qwen | EditPredictionPromptFormat::CodeGemma => {
|
||||
format!("<|fim_prefix|>{prefix}<|fim_suffix|>{suffix}<|fim_middle|>")
|
||||
}
|
||||
EditPredictionPromptFormat::Codestral => {
|
||||
format!("[SUFFIX]{suffix}[PREFIX]{prefix}")
|
||||
}
|
||||
EditPredictionPromptFormat::Glm => {
|
||||
format!("<|code_prefix|>{prefix}<|code_suffix|>{suffix}<|code_middle|>")
|
||||
}
|
||||
_ => {
|
||||
format!("<fim_prefix>{prefix}<fim_suffix>{suffix}<fim_middle>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_fim_stop_tokens() -> Vec<String> {
|
||||
vec![
|
||||
"<|endoftext|>".to_string(),
|
||||
"<|file_separator|>".to_string(),
|
||||
"<|fim_pad|>".to_string(),
|
||||
"<|fim_prefix|>".to_string(),
|
||||
"<|fim_middle|>".to_string(),
|
||||
"<|fim_suffix|>".to_string(),
|
||||
"<fim_prefix>".to_string(),
|
||||
"<fim_middle>".to_string(),
|
||||
"<fim_suffix>".to_string(),
|
||||
"<PRE>".to_string(),
|
||||
"<SUF>".to_string(),
|
||||
"<MID>".to_string(),
|
||||
"[PREFIX]".to_string(),
|
||||
"[SUFFIX]".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
fn clean_fim_completion(response: &str) -> String {
|
||||
let mut result = response.to_string();
|
||||
|
||||
let end_tokens = [
|
||||
"<|endoftext|>",
|
||||
"<|file_separator|>",
|
||||
"<|fim_pad|>",
|
||||
"<|fim_prefix|>",
|
||||
"<|fim_middle|>",
|
||||
"<|fim_suffix|>",
|
||||
"<fim_prefix>",
|
||||
"<fim_middle>",
|
||||
"<fim_suffix>",
|
||||
"<PRE>",
|
||||
"<SUF>",
|
||||
"<MID>",
|
||||
"[PREFIX]",
|
||||
"[SUFFIX]",
|
||||
];
|
||||
|
||||
for token in &end_tokens {
|
||||
if let Some(pos) = result.find(token) {
|
||||
result.truncate(pos);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
866
crates/edit_prediction/src/license_detection.rs
Normal file
866
crates/edit_prediction/src/license_detection.rs
Normal file
@@ -0,0 +1,866 @@
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
fmt::{Display, Formatter},
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{App, AppContext as _, Entity, Subscription, Task};
|
||||
use itertools::Itertools;
|
||||
use postage::watch;
|
||||
use project::Worktree;
|
||||
use strum::VariantArray;
|
||||
use util::{ResultExt as _, maybe, rel_path::RelPath};
|
||||
use worktree::ChildEntriesOptions;
|
||||
|
||||
/// Matches the most common license locations, with US and UK English spelling.
|
||||
static LICENSE_FILE_NAME_REGEX: LazyLock<regex::bytes::Regex> = LazyLock::new(|| {
|
||||
regex::bytes::RegexBuilder::new(
|
||||
"^ \
|
||||
(?: \
|
||||
(?: license | licence) \
|
||||
(?: [\\-._]? \
|
||||
(?: apache (?: [\\-._] (?: 2.0 | 2 ))? | \
|
||||
0? bsd (?: [\\-._] [0123])? (?: [\\-._] clause)? | \
|
||||
isc | \
|
||||
mit | \
|
||||
upl | \
|
||||
zlib))? \
|
||||
| \
|
||||
(?: apache (?: [\\-._] (?: 2.0 | 2 ))? | \
|
||||
0? bsd (?: [\\-._] [0123])? (?: [\\-._] clause)? | \
|
||||
isc | \
|
||||
mit | \
|
||||
upl | \
|
||||
zlib) \
|
||||
) \
|
||||
(?: [\\-._]? (?: license | licence))? \
|
||||
(?: \\.txt | \\.md)? \
|
||||
$",
|
||||
)
|
||||
.ignore_whitespace(true)
|
||||
.case_insensitive(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, VariantArray)]
|
||||
pub enum OpenSourceLicense {
|
||||
Apache2_0,
|
||||
BSDZero,
|
||||
BSD,
|
||||
ISC,
|
||||
MIT,
|
||||
UPL1_0,
|
||||
Zlib,
|
||||
}
|
||||
|
||||
impl Display for OpenSourceLicense {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.spdx_identifier())
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenSourceLicense {
|
||||
/// These are SPDX identifiers for the licenses, except for BSD, where the variants are not
|
||||
/// distinguished.
|
||||
pub fn spdx_identifier(&self) -> &'static str {
|
||||
match self {
|
||||
OpenSourceLicense::Apache2_0 => "apache-2.0",
|
||||
OpenSourceLicense::BSDZero => "0bsd",
|
||||
OpenSourceLicense::BSD => "bsd",
|
||||
OpenSourceLicense::ISC => "isc",
|
||||
OpenSourceLicense::MIT => "mit",
|
||||
OpenSourceLicense::UPL1_0 => "upl-1.0",
|
||||
OpenSourceLicense::Zlib => "zlib",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn patterns(&self) -> &'static [&'static str] {
|
||||
match self {
|
||||
OpenSourceLicense::Apache2_0 => &[
|
||||
include_str!("../license_patterns/apache-2.0-pattern"),
|
||||
include_str!("../license_patterns/apache-2.0-reference-pattern"),
|
||||
],
|
||||
OpenSourceLicense::BSDZero => &[include_str!("../license_patterns/0bsd-pattern")],
|
||||
OpenSourceLicense::BSD => &[include_str!("../license_patterns/bsd-pattern")],
|
||||
OpenSourceLicense::ISC => &[include_str!("../license_patterns/isc-pattern")],
|
||||
OpenSourceLicense::MIT => &[include_str!("../license_patterns/mit-pattern")],
|
||||
OpenSourceLicense::UPL1_0 => &[include_str!("../license_patterns/upl-1.0-pattern")],
|
||||
OpenSourceLicense::Zlib => &[include_str!("../license_patterns/zlib-pattern")],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider using databake or similar to not parse at runtime.
|
||||
static LICENSE_PATTERNS: LazyLock<LicensePatterns> = LazyLock::new(|| {
|
||||
let mut approximate_max_length = 0;
|
||||
let mut patterns = Vec::new();
|
||||
for license in OpenSourceLicense::VARIANTS {
|
||||
for pattern in license.patterns() {
|
||||
let (pattern, length) = parse_pattern(pattern).unwrap();
|
||||
patterns.push((*license, pattern));
|
||||
approximate_max_length = approximate_max_length.max(length);
|
||||
}
|
||||
}
|
||||
LicensePatterns {
|
||||
patterns,
|
||||
approximate_max_length,
|
||||
}
|
||||
});
|
||||
|
||||
fn detect_license(text: &str) -> Option<OpenSourceLicense> {
|
||||
let text = canonicalize_license_text(text);
|
||||
for (license, pattern) in LICENSE_PATTERNS.patterns.iter() {
|
||||
log::trace!("Checking if license is {}", license);
|
||||
if check_pattern(&pattern, &text) {
|
||||
return Some(*license);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
struct LicensePatterns {
|
||||
patterns: Vec<(OpenSourceLicense, Vec<PatternPart>)>,
|
||||
approximate_max_length: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
struct PatternPart {
|
||||
/// Indicates that matching `text` is optional. Skipping `match_any_chars` is conditional on
|
||||
/// matching `text`.
|
||||
optional: bool,
|
||||
/// Indicates the number of characters that can be skipped before matching `text`.
|
||||
match_any_chars: Range<usize>,
|
||||
/// The text to match, may be empty.
|
||||
text: String,
|
||||
}
|
||||
|
||||
/// Lines that start with "-- " begin a `PatternPart`. `-- 1..10` specifies `match_any_chars:
|
||||
/// 1..10`. `-- 1..10 optional:` additionally specifies `optional: true`. It's a parse error for a
|
||||
/// line to start with `--` without matching this format.
|
||||
///
|
||||
/// Text that does not have `--` prefixes participate in the `text` field and are canonicalized by
|
||||
/// lowercasing, replacing all runs of whitespace with a single space, and otherwise only keeping
|
||||
/// ascii alphanumeric characters.
|
||||
fn parse_pattern(pattern_source: &str) -> Result<(Vec<PatternPart>, usize)> {
|
||||
let mut pattern = Vec::new();
|
||||
let mut part = PatternPart::default();
|
||||
let mut approximate_max_length = 0;
|
||||
for line in pattern_source.lines() {
|
||||
if let Some(directive) = line.trim().strip_prefix("--") {
|
||||
if part != PatternPart::default() {
|
||||
pattern.push(part);
|
||||
part = PatternPart::default();
|
||||
}
|
||||
let valid = maybe!({
|
||||
let directive_chunks = directive.split_whitespace().collect::<Vec<_>>();
|
||||
if !(1..=2).contains(&directive_chunks.len()) {
|
||||
return None;
|
||||
}
|
||||
if directive_chunks.len() == 2 {
|
||||
part.optional = true;
|
||||
}
|
||||
let range_chunks = directive_chunks[0].split("..").collect::<Vec<_>>();
|
||||
if range_chunks.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
part.match_any_chars.start = range_chunks[0].parse::<usize>().ok()?;
|
||||
part.match_any_chars.end = range_chunks[1].parse::<usize>().ok()?;
|
||||
if part.match_any_chars.start > part.match_any_chars.end {
|
||||
return None;
|
||||
}
|
||||
approximate_max_length += part.match_any_chars.end;
|
||||
Some(())
|
||||
});
|
||||
if valid.is_none() {
|
||||
return Err(anyhow!("Invalid pattern directive: {}", line));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
approximate_max_length += line.len() + 1;
|
||||
let line = canonicalize_license_text(line);
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !part.text.is_empty() {
|
||||
part.text.push(' ');
|
||||
}
|
||||
part.text.push_str(&line);
|
||||
}
|
||||
if part != PatternPart::default() {
|
||||
pattern.push(part);
|
||||
}
|
||||
Ok((pattern, approximate_max_length))
|
||||
}
|
||||
|
||||
/// Checks a pattern against text by iterating over the pattern parts in reverse order, and checking
|
||||
/// matches with the end of a prefix of the input. Assumes that `canonicalize_license_text` has
|
||||
/// already been applied to the input.
|
||||
fn check_pattern(pattern: &[PatternPart], input: &str) -> bool {
|
||||
let mut input_ix = input.len();
|
||||
let mut match_any_chars = 0..0;
|
||||
for part in pattern.iter().rev() {
|
||||
if part.text.is_empty() {
|
||||
match_any_chars.start += part.match_any_chars.start;
|
||||
match_any_chars.end += part.match_any_chars.end;
|
||||
continue;
|
||||
}
|
||||
|
||||
let search_range_end = n_chars_before_offset(match_any_chars.start, input_ix, input);
|
||||
let search_range_start = n_chars_before_offset(
|
||||
match_any_chars.len() + part.text.len(),
|
||||
search_range_end,
|
||||
input,
|
||||
);
|
||||
let found_ix = input[search_range_start..search_range_end].rfind(&part.text);
|
||||
|
||||
if let Some(found_ix) = found_ix {
|
||||
input_ix = search_range_start + found_ix;
|
||||
match_any_chars = part.match_any_chars.clone();
|
||||
} else if !part.optional {
|
||||
log::trace!(
|
||||
"Failed to match pattern\n`...{}`\nagainst input\n`...{}`",
|
||||
&part.text[n_chars_before_offset(128, part.text.len(), &part.text)..],
|
||||
&input[n_chars_before_offset(128, search_range_end, input)..search_range_end],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
is_char_count_within_range(&input[..input_ix], match_any_chars)
|
||||
}
|
||||
|
||||
fn n_chars_before_offset(char_count: usize, offset: usize, string: &str) -> usize {
|
||||
if char_count == 0 {
|
||||
return offset;
|
||||
}
|
||||
string[..offset]
|
||||
.char_indices()
|
||||
.nth_back(char_count.saturating_sub(1))
|
||||
.map_or(0, |(byte_ix, _)| byte_ix)
|
||||
}
|
||||
|
||||
fn is_char_count_within_range(string: &str, char_count_range: Range<usize>) -> bool {
|
||||
if string.len() >= char_count_range.start * 4 && string.len() < char_count_range.end {
|
||||
return true;
|
||||
}
|
||||
if string.len() < char_count_range.start || string.len() >= char_count_range.end * 4 {
|
||||
return false;
|
||||
}
|
||||
char_count_range.contains(&string.chars().count())
|
||||
}
|
||||
|
||||
/// Canonicalizes license text by removing all non-alphanumeric characters, lowercasing, and turning
|
||||
/// runs of whitespace into a single space. Unicode alphanumeric characters are intentionally
|
||||
/// preserved since these should cause license mismatch when not within a portion of the license
|
||||
/// where arbitrary text is allowed.
|
||||
fn canonicalize_license_text(license: &str) -> String {
|
||||
license
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_whitespace() || c.is_alphanumeric())
|
||||
.map(|c| c.to_ascii_lowercase())
|
||||
.collect::<String>()
|
||||
.split_ascii_whitespace()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
pub enum LicenseDetectionWatcher {
|
||||
Local {
|
||||
is_open_source_rx: watch::Receiver<bool>,
|
||||
_is_open_source_task: Task<()>,
|
||||
_worktree_subscription: Subscription,
|
||||
},
|
||||
SingleFile,
|
||||
Remote,
|
||||
}
|
||||
|
||||
impl LicenseDetectionWatcher {
|
||||
pub fn new(worktree: &Entity<Worktree>, cx: &mut App) -> Self {
|
||||
let worktree_ref = worktree.read(cx);
|
||||
if worktree_ref.is_single_file() {
|
||||
return Self::SingleFile;
|
||||
}
|
||||
|
||||
let (files_to_check_tx, mut files_to_check_rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
let Worktree::Local(local_worktree) = worktree_ref else {
|
||||
return Self::Remote;
|
||||
};
|
||||
let fs = local_worktree.fs().clone();
|
||||
|
||||
let options = ChildEntriesOptions {
|
||||
include_files: true,
|
||||
include_dirs: false,
|
||||
include_ignored: true,
|
||||
};
|
||||
for top_file in local_worktree.child_entries_with_options(RelPath::empty(), options) {
|
||||
let path_bytes = top_file.path.as_unix_str().as_bytes();
|
||||
if top_file.is_created() && LICENSE_FILE_NAME_REGEX.is_match(path_bytes) {
|
||||
let rel_path = top_file.path.clone();
|
||||
files_to_check_tx.unbounded_send(rel_path).ok();
|
||||
}
|
||||
}
|
||||
|
||||
let _worktree_subscription =
|
||||
cx.subscribe(worktree, move |_worktree, event, _cx| match event {
|
||||
worktree::Event::UpdatedEntries(updated_entries) => {
|
||||
for updated_entry in updated_entries.iter() {
|
||||
let rel_path = &updated_entry.0;
|
||||
let path_bytes = rel_path.as_unix_str().as_bytes();
|
||||
if LICENSE_FILE_NAME_REGEX.is_match(path_bytes) {
|
||||
files_to_check_tx.unbounded_send(rel_path.clone()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
worktree::Event::DeletedEntry(_)
|
||||
| worktree::Event::UpdatedGitRepositories(_)
|
||||
| worktree::Event::UpdatedRootRepoCommonDir { .. }
|
||||
| worktree::Event::Deleted => {}
|
||||
});
|
||||
|
||||
let worktree_snapshot = worktree.read(cx).snapshot();
|
||||
let (mut is_open_source_tx, is_open_source_rx) = watch::channel_with::<bool>(false);
|
||||
|
||||
let _is_open_source_task = cx.background_spawn(async move {
|
||||
let mut eligible_licenses = BTreeSet::new();
|
||||
while let Some(rel_path) = files_to_check_rx.next().await {
|
||||
let abs_path = worktree_snapshot.absolutize(&rel_path);
|
||||
let was_open_source = !eligible_licenses.is_empty();
|
||||
if Self::is_path_eligible(&fs, abs_path).await.unwrap_or(false) {
|
||||
eligible_licenses.insert(rel_path);
|
||||
} else {
|
||||
eligible_licenses.remove(&rel_path);
|
||||
}
|
||||
let is_open_source = !eligible_licenses.is_empty();
|
||||
if is_open_source != was_open_source {
|
||||
*is_open_source_tx.borrow_mut() = is_open_source;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self::Local {
|
||||
is_open_source_rx,
|
||||
_is_open_source_task,
|
||||
_worktree_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_path_eligible(fs: &Arc<dyn Fs>, abs_path: PathBuf) -> Option<bool> {
|
||||
log::debug!("checking if `{abs_path:?}` is an open source license");
|
||||
// resolve symlinks so that the file size from metadata is correct
|
||||
let Some(abs_path) = fs.canonicalize(&abs_path).await.ok() else {
|
||||
log::debug!(
|
||||
"`{abs_path:?}` license file probably deleted (error canonicalizing the path)"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
let metadata = fs.metadata(&abs_path).await.log_err()??;
|
||||
if metadata.is_dir {
|
||||
return None;
|
||||
}
|
||||
if metadata.len > LICENSE_PATTERNS.approximate_max_length as u64 {
|
||||
log::debug!(
|
||||
"`{abs_path:?}` license file was skipped \
|
||||
because its size of {} bytes was larger than the max size of {} bytes",
|
||||
metadata.len,
|
||||
LICENSE_PATTERNS.approximate_max_length
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let text = fs.load(&abs_path).await.log_err()?;
|
||||
let is_eligible = detect_license(&text).is_some();
|
||||
if is_eligible {
|
||||
log::debug!(
|
||||
"`{abs_path:?}` matches a license that is eligible for data collection (if enabled)"
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
"`{abs_path:?}` does not match a license that is eligible for data collection"
|
||||
);
|
||||
}
|
||||
Some(is_eligible)
|
||||
}
|
||||
|
||||
/// Answers false until we find out it's open source
|
||||
pub fn is_project_open_source(&self) -> bool {
|
||||
match self {
|
||||
Self::Local {
|
||||
is_open_source_rx, ..
|
||||
} => *is_open_source_rx.borrow(),
|
||||
Self::SingleFile | Self::Remote => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use project::WorktreeId;
|
||||
use rand::Rng as _;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
|
||||
use super::*;
|
||||
|
||||
const APACHE_2_0_TXT: &str = include_str!("../license_examples/apache-2.0-ex0.txt");
|
||||
const ISC_TXT: &str = include_str!("../license_examples/isc.txt");
|
||||
const MIT_TXT: &str = include_str!("../license_examples/mit-ex0.txt");
|
||||
const UPL_1_0_TXT: &str = include_str!("../license_examples/upl-1.0.txt");
|
||||
const BSD_0_TXT: &str = include_str!("../license_examples/0bsd.txt");
|
||||
|
||||
#[track_caller]
|
||||
fn assert_matches_license(text: &str, license: OpenSourceLicense) {
|
||||
assert_eq!(detect_license(text), Some(license));
|
||||
assert!(text.len() < LICENSE_PATTERNS.approximate_max_length);
|
||||
}
|
||||
|
||||
/*
|
||||
// Uncomment this and run with `cargo test -p zeta -- --no-capture &> licenses-output` to
|
||||
// traverse your entire home directory and run license detection on every file that has a
|
||||
// license-like name.
|
||||
#[test]
|
||||
fn test_check_all_licenses_in_home_dir() {
|
||||
let mut detected = Vec::new();
|
||||
let mut unrecognized = Vec::new();
|
||||
let mut walked_entries = 0;
|
||||
let homedir = std::env::home_dir().unwrap();
|
||||
for entry in walkdir::WalkDir::new(&homedir) {
|
||||
walked_entries += 1;
|
||||
if walked_entries % 10000 == 0 {
|
||||
println!(
|
||||
"So far visited {} files in {}",
|
||||
walked_entries,
|
||||
homedir.display()
|
||||
);
|
||||
}
|
||||
let Ok(entry) = entry else {
|
||||
continue;
|
||||
};
|
||||
if !LICENSE_FILE_NAME_REGEX.is_match(entry.file_name().as_encoded_bytes()) {
|
||||
continue;
|
||||
}
|
||||
let Ok(contents) = std::fs::read_to_string(entry.path()) else {
|
||||
continue;
|
||||
};
|
||||
let path_string = entry.path().to_string_lossy().into_owned();
|
||||
let license = detect_license(&contents);
|
||||
match license {
|
||||
Some(license) => detected.push((license, path_string)),
|
||||
None => unrecognized.push(path_string),
|
||||
}
|
||||
}
|
||||
println!("\nDetected licenses:\n");
|
||||
detected.sort();
|
||||
for (license, path) in &detected {
|
||||
println!("{}: {}", license.spdx_identifier(), path);
|
||||
}
|
||||
println!("\nUnrecognized licenses:\n");
|
||||
for path in &unrecognized {
|
||||
println!("{}", path);
|
||||
}
|
||||
panic!(
|
||||
"{} licenses detected, {} unrecognized",
|
||||
detected.len(),
|
||||
unrecognized.len()
|
||||
);
|
||||
println!("This line has a warning to make sure this test is always commented out");
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn test_apache_positive_detection() {
|
||||
assert_matches_license(APACHE_2_0_TXT, OpenSourceLicense::Apache2_0);
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/apache-2.0-ex1.txt"),
|
||||
OpenSourceLicense::Apache2_0,
|
||||
);
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/apache-2.0-ex2.txt"),
|
||||
OpenSourceLicense::Apache2_0,
|
||||
);
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/apache-2.0-ex3.txt"),
|
||||
OpenSourceLicense::Apache2_0,
|
||||
);
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/apache-2.0-ex4.txt"),
|
||||
OpenSourceLicense::Apache2_0,
|
||||
);
|
||||
assert_matches_license(
|
||||
include_str!("../../../LICENSE-APACHE"),
|
||||
OpenSourceLicense::Apache2_0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apache_negative_detection() {
|
||||
assert_eq!(
|
||||
detect_license(&format!(
|
||||
"{APACHE_2_0_TXT}\n\nThe terms in this license are void if P=NP."
|
||||
)),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bsd_1_clause_positive_detection() {
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/bsd-1-clause.txt"),
|
||||
OpenSourceLicense::BSD,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bsd_2_clause_positive_detection() {
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/bsd-2-clause-ex0.txt"),
|
||||
OpenSourceLicense::BSD,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bsd_3_clause_positive_detection() {
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/bsd-3-clause-ex0.txt"),
|
||||
OpenSourceLicense::BSD,
|
||||
);
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/bsd-3-clause-ex1.txt"),
|
||||
OpenSourceLicense::BSD,
|
||||
);
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/bsd-3-clause-ex2.txt"),
|
||||
OpenSourceLicense::BSD,
|
||||
);
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/bsd-3-clause-ex3.txt"),
|
||||
OpenSourceLicense::BSD,
|
||||
);
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/bsd-3-clause-ex4.txt"),
|
||||
OpenSourceLicense::BSD,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bsd_0_positive_detection() {
|
||||
assert_matches_license(BSD_0_TXT, OpenSourceLicense::BSDZero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isc_positive_detection() {
|
||||
assert_matches_license(ISC_TXT, OpenSourceLicense::ISC);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isc_negative_detection() {
|
||||
let license_text = format!(
|
||||
r#"{ISC_TXT}
|
||||
|
||||
This project is dual licensed under the ISC License and the MIT License."#
|
||||
);
|
||||
|
||||
assert_eq!(detect_license(&license_text), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mit_positive_detection() {
|
||||
assert_matches_license(MIT_TXT, OpenSourceLicense::MIT);
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/mit-ex1.txt"),
|
||||
OpenSourceLicense::MIT,
|
||||
);
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/mit-ex2.txt"),
|
||||
OpenSourceLicense::MIT,
|
||||
);
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/mit-ex3.txt"),
|
||||
OpenSourceLicense::MIT,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mit_negative_detection() {
|
||||
let license_text = format!(
|
||||
r#"{MIT_TXT}
|
||||
|
||||
This project is dual licensed under the MIT License and the Apache License, Version 2.0."#
|
||||
);
|
||||
assert_eq!(detect_license(&license_text), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upl_positive_detection() {
|
||||
assert_matches_license(UPL_1_0_TXT, OpenSourceLicense::UPL1_0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upl_negative_detection() {
|
||||
let license_text = format!(
|
||||
r#"{UPL_1_0_TXT}
|
||||
|
||||
This project is dual licensed under the UPL License and the MIT License."#
|
||||
);
|
||||
|
||||
assert_eq!(detect_license(&license_text), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zlib_positive_detection() {
|
||||
assert_matches_license(
|
||||
include_str!("../license_examples/zlib-ex0.txt"),
|
||||
OpenSourceLicense::Zlib,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_strings_negative_detection() {
|
||||
for _i in 0..20 {
|
||||
let random_string = rand::rng()
|
||||
.sample_iter::<char, _>(rand::distr::StandardUniform)
|
||||
.take(512)
|
||||
.collect::<String>();
|
||||
assert_eq!(detect_license(&random_string), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_n_chars_before_offset() {
|
||||
assert_eq!(n_chars_before_offset(2, 4, "hello"), 2);
|
||||
|
||||
let input = "ㄒ乇丂ㄒ";
|
||||
assert_eq!(n_chars_before_offset(2, input.len(), input), "ㄒ乇".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_char_count_within_range() {
|
||||
// TODO: make this into a proper property test.
|
||||
for _i in 0..20 {
|
||||
let mut rng = rand::rng();
|
||||
let random_char_count = rng.random_range(0..64);
|
||||
let random_string = rand::rng()
|
||||
.sample_iter::<char, _>(rand::distr::StandardUniform)
|
||||
.take(random_char_count)
|
||||
.collect::<String>();
|
||||
let min_chars = rng.random_range(0..10);
|
||||
let max_chars = rng.random_range(min_chars..32);
|
||||
let char_count_range = min_chars..max_chars;
|
||||
assert_eq!(
|
||||
is_char_count_within_range(&random_string, char_count_range.clone()),
|
||||
char_count_range.contains(&random_char_count),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_license_file_name_regex() {
|
||||
// Test basic license file names
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"licence"));
|
||||
|
||||
// Test with extensions
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.txt"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.md"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.txt"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.md"));
|
||||
|
||||
// Test with specific license types
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-APACHE"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-MIT"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.MIT"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE_MIT"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-ISC"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-UPL"));
|
||||
|
||||
// Test with "license" coming after
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"APACHE-LICENSE"));
|
||||
|
||||
// Test version numbers
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"APACHE-2"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"APACHE-2.0"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-1"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-2"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-3"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-3-CLAUSE"));
|
||||
|
||||
// Test combinations
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-MIT.txt"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.ISC.md"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license_upl"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.APACHE.2.0"));
|
||||
|
||||
// Test case insensitive
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"License"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license-mit.TXT"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE_isc.MD"));
|
||||
|
||||
// Test edge cases that should match
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license.mit"));
|
||||
assert!(LICENSE_FILE_NAME_REGEX.is_match(b"licence-upl.txt"));
|
||||
|
||||
// Test non-matching patterns
|
||||
assert!(!LICENSE_FILE_NAME_REGEX.is_match(b""));
|
||||
assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"COPYING"));
|
||||
assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.html"));
|
||||
assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"MYLICENSE"));
|
||||
assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"src/LICENSE"));
|
||||
assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.old"));
|
||||
assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-GPL"));
|
||||
assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSEABC"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_canonicalize_license_text() {
|
||||
let input = " Paragraph 1\nwith multiple lines\n\n\n\nParagraph 2\nwith more lines\n ";
|
||||
let expected = "paragraph 1 with multiple lines paragraph 2 with more lines";
|
||||
assert_eq!(canonicalize_license_text(input), expected);
|
||||
|
||||
// Test tabs and mixed whitespace
|
||||
let input = "Word1\t\tWord2\n\n Word3\r\n\r\n\r\nWord4 ";
|
||||
let expected = "word1 word2 word3 word4";
|
||||
assert_eq!(canonicalize_license_text(input), expected);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_watcher_single_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree("/root", json!({ "main.rs": "fn main() {}" }))
|
||||
.await;
|
||||
|
||||
let worktree = Worktree::local(
|
||||
Path::new("/root/main.rs"),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
true,
|
||||
WorktreeId::from_proto(0),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx));
|
||||
assert!(matches!(watcher, LicenseDetectionWatcher::SingleFile));
|
||||
assert!(!watcher.is_project_open_source());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_watcher_updates_on_changes(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree("/root", json!({ "main.rs": "fn main() {}" }))
|
||||
.await;
|
||||
|
||||
let worktree = Worktree::local(
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
true,
|
||||
WorktreeId::from_proto(0),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx));
|
||||
assert!(matches!(watcher, LicenseDetectionWatcher::Local { .. }));
|
||||
assert!(!watcher.is_project_open_source());
|
||||
|
||||
fs.write(Path::new("/root/LICENSE-MIT"), MIT_TXT.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
assert!(watcher.is_project_open_source());
|
||||
|
||||
fs.write(Path::new("/root/LICENSE-APACHE"), APACHE_2_0_TXT.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
assert!(watcher.is_project_open_source());
|
||||
|
||||
fs.write(Path::new("/root/LICENSE-MIT"), "Nevermind".as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Still considered open source as LICENSE-APACHE is present
|
||||
cx.background_executor.run_until_parked();
|
||||
assert!(watcher.is_project_open_source());
|
||||
|
||||
fs.write(
|
||||
Path::new("/root/LICENSE-APACHE"),
|
||||
"Also nevermind".as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
assert!(!watcher.is_project_open_source());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_watcher_initially_opensource_and_then_deleted(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({ "main.rs": "fn main() {}", "LICENSE-MIT": MIT_TXT }),
|
||||
)
|
||||
.await;
|
||||
|
||||
let worktree = Worktree::local(
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
true,
|
||||
WorktreeId::from_proto(0),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx));
|
||||
assert!(matches!(watcher, LicenseDetectionWatcher::Local { .. }));
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
assert!(watcher.is_project_open_source());
|
||||
|
||||
fs.remove_file(
|
||||
Path::new("/root/LICENSE-MIT"),
|
||||
fs::RemoveOptions {
|
||||
recursive: false,
|
||||
ignore_if_not_exists: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
assert!(!watcher.is_project_open_source());
|
||||
}
|
||||
}
|
||||
477
crates/edit_prediction/src/mercury.rs
Normal file
477
crates/edit_prediction/src/mercury.rs
Normal file
@@ -0,0 +1,477 @@
|
||||
use crate::{
|
||||
DebugEvent, EditPredictionFinishedDebugEvent, EditPredictionId, EditPredictionModelInput,
|
||||
EditPredictionStartedDebugEvent, EditPredictionStore, open_ai_response::text_from_response,
|
||||
prediction::EditPredictionResult, zeta::compute_edits,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use cloud_llm_client::EditPredictionRejectReason;
|
||||
use credentials_provider::CredentialsProvider;
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Context, Entity, Global, SharedString, Task, TaskExt,
|
||||
http_client::{self, AsyncBody, HttpClient, Method, StatusCode},
|
||||
};
|
||||
use language::{ToOffset, ToPoint as _};
|
||||
use language_model::{ApiKeyState, EnvVar, env_var};
|
||||
use release_channel::AppVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{mem, ops::Range, path::Path, sync::Arc};
|
||||
use zeta_prompt::ZetaPromptInput;
|
||||
|
||||
const MERCURY_API_URL: &str = "https://api.inceptionlabs.ai/v1/edit/completions";
|
||||
|
||||
pub struct Mercury {
|
||||
pub api_token: Entity<ApiKeyState>,
|
||||
payment_required_error: bool,
|
||||
}
|
||||
|
||||
impl Mercury {
|
||||
pub fn new(cx: &mut App) -> Self {
|
||||
Mercury {
|
||||
api_token: mercury_api_token(cx),
|
||||
payment_required_error: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_payment_required_error(&self) -> bool {
|
||||
self.payment_required_error
|
||||
}
|
||||
|
||||
pub fn set_payment_required_error(&mut self, payment_required_error: bool) {
|
||||
self.payment_required_error = payment_required_error;
|
||||
}
|
||||
|
||||
pub(crate) fn request_prediction(
|
||||
&mut self,
|
||||
EditPredictionModelInput {
|
||||
buffer,
|
||||
snapshot,
|
||||
position,
|
||||
events,
|
||||
related_files,
|
||||
debug_tx,
|
||||
..
|
||||
}: EditPredictionModelInput,
|
||||
credentials_provider: Arc<dyn CredentialsProvider>,
|
||||
cx: &mut Context<EditPredictionStore>,
|
||||
) -> Task<Result<Option<EditPredictionResult>>> {
|
||||
self.api_token.update(cx, |key_state, cx| {
|
||||
_ = key_state.load_if_needed(MERCURY_CREDENTIALS_URL, |s| s, credentials_provider, cx);
|
||||
});
|
||||
let Some(api_token) = self.api_token.read(cx).key(&MERCURY_CREDENTIALS_URL) else {
|
||||
return Task::ready(Ok(None));
|
||||
};
|
||||
let full_path: Arc<Path> = snapshot
|
||||
.file()
|
||||
.map(|file| file.full_path(cx))
|
||||
.unwrap_or_else(|| "untitled".into())
|
||||
.into();
|
||||
|
||||
let http_client = cx.http_client();
|
||||
let cursor_point = position.to_point(&snapshot);
|
||||
let request_start = cx.background_executor().now();
|
||||
let active_buffer = buffer.clone();
|
||||
|
||||
let result = cx.background_spawn(async move {
|
||||
let cursor_offset = cursor_point.to_offset(&snapshot);
|
||||
let (excerpt_point_range, excerpt_offset_range, cursor_offset_in_excerpt) =
|
||||
crate::cursor_excerpt::compute_cursor_excerpt(&snapshot, cursor_offset);
|
||||
|
||||
let related_files = zeta_prompt::filter_redundant_excerpts(
|
||||
related_files,
|
||||
full_path.as_ref(),
|
||||
excerpt_point_range.start.row..excerpt_point_range.end.row,
|
||||
);
|
||||
|
||||
let cursor_excerpt: Arc<str> = snapshot
|
||||
.text_for_range(excerpt_point_range.clone())
|
||||
.collect::<String>()
|
||||
.into();
|
||||
let syntax_ranges = crate::cursor_excerpt::compute_syntax_ranges(
|
||||
&snapshot,
|
||||
cursor_offset,
|
||||
&excerpt_offset_range,
|
||||
);
|
||||
let excerpt_ranges = zeta_prompt::compute_legacy_excerpt_ranges(
|
||||
&cursor_excerpt,
|
||||
cursor_offset_in_excerpt,
|
||||
&syntax_ranges,
|
||||
);
|
||||
|
||||
let editable_offset_range = (excerpt_offset_range.start
|
||||
+ excerpt_ranges.editable_350.start)
|
||||
..(excerpt_offset_range.start + excerpt_ranges.editable_350.end);
|
||||
|
||||
let inputs = zeta_prompt::ZetaPromptInput {
|
||||
events,
|
||||
related_files: Some(related_files),
|
||||
cursor_offset_in_excerpt: cursor_point.to_offset(&snapshot)
|
||||
- excerpt_offset_range.start,
|
||||
cursor_path: full_path.clone(),
|
||||
cursor_excerpt,
|
||||
excerpt_start_row: Some(excerpt_point_range.start.row),
|
||||
excerpt_ranges,
|
||||
syntax_ranges: Some(syntax_ranges),
|
||||
active_buffer_diagnostics: vec![],
|
||||
in_open_source_repo: false,
|
||||
can_collect_data: false,
|
||||
repo_url: None,
|
||||
};
|
||||
|
||||
let prompt = build_prompt(&inputs);
|
||||
|
||||
if let Some(debug_tx) = &debug_tx {
|
||||
debug_tx
|
||||
.unbounded_send(DebugEvent::EditPredictionStarted(
|
||||
EditPredictionStartedDebugEvent {
|
||||
buffer: active_buffer.downgrade(),
|
||||
prompt: Some(prompt.clone()),
|
||||
position,
|
||||
},
|
||||
))
|
||||
.ok();
|
||||
}
|
||||
|
||||
let request_body = open_ai::Request {
|
||||
model: "mercury-coder".into(),
|
||||
messages: vec![open_ai::RequestMessage::User {
|
||||
content: open_ai::MessageContent::Plain(prompt),
|
||||
}],
|
||||
stream: false,
|
||||
stream_options: None,
|
||||
max_completion_tokens: None,
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
tool_choice: None,
|
||||
parallel_tool_calls: None,
|
||||
tools: vec![],
|
||||
prompt_cache_key: None,
|
||||
reasoning_effort: None,
|
||||
};
|
||||
|
||||
let buf = serde_json::to_vec(&request_body)?;
|
||||
let body: AsyncBody = buf.into();
|
||||
|
||||
let request = http_client::Request::builder()
|
||||
.uri(MERCURY_API_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_token))
|
||||
.header("Connection", "keep-alive")
|
||||
.method(Method::POST)
|
||||
.body(body)
|
||||
.context("Failed to create request")?;
|
||||
|
||||
let mut response = http_client
|
||||
.send(request)
|
||||
.await
|
||||
.context("Failed to send request")?;
|
||||
|
||||
let mut body: Vec<u8> = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("Failed to read response body")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
if response.status() == StatusCode::PAYMENT_REQUIRED {
|
||||
anyhow::bail!(MercuryPaymentRequiredError(
|
||||
mercury_payment_required_message(&body),
|
||||
));
|
||||
}
|
||||
|
||||
anyhow::bail!(
|
||||
"Request failed with status: {:?}\nBody: {}",
|
||||
response.status(),
|
||||
String::from_utf8_lossy(&body),
|
||||
);
|
||||
};
|
||||
|
||||
let mut response: open_ai::Response =
|
||||
serde_json::from_slice(&body).context("Failed to parse response")?;
|
||||
|
||||
let id = mem::take(&mut response.id);
|
||||
let response_str = text_from_response(response).unwrap_or_default();
|
||||
|
||||
if let Some(debug_tx) = &debug_tx {
|
||||
debug_tx
|
||||
.unbounded_send(DebugEvent::EditPredictionFinished(
|
||||
EditPredictionFinishedDebugEvent {
|
||||
buffer: active_buffer.downgrade(),
|
||||
model_output: Some(response_str.clone()),
|
||||
position,
|
||||
},
|
||||
))
|
||||
.ok();
|
||||
}
|
||||
|
||||
let response_str = response_str.strip_prefix("```\n").unwrap_or(&response_str);
|
||||
let response_str = response_str.strip_suffix("\n```").unwrap_or(&response_str);
|
||||
|
||||
let mut edits = Vec::new();
|
||||
const NO_PREDICTION_OUTPUT: &str = "None";
|
||||
|
||||
if response_str != NO_PREDICTION_OUTPUT {
|
||||
let old_text = snapshot
|
||||
.text_for_range(editable_offset_range.clone())
|
||||
.collect::<String>();
|
||||
edits = compute_edits(
|
||||
old_text,
|
||||
&response_str,
|
||||
editable_offset_range.start,
|
||||
&snapshot,
|
||||
);
|
||||
}
|
||||
|
||||
anyhow::Ok((id, edits, snapshot, inputs))
|
||||
});
|
||||
|
||||
cx.spawn(async move |ep_store, cx| {
|
||||
let result = result.await.context("Mercury edit prediction failed");
|
||||
|
||||
let has_payment_required_error = result
|
||||
.as_ref()
|
||||
.err()
|
||||
.is_some_and(is_mercury_payment_required_error);
|
||||
|
||||
ep_store.update(cx, |store, cx| {
|
||||
store
|
||||
.mercury
|
||||
.set_payment_required_error(has_payment_required_error);
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
let (id, edits, old_snapshot, inputs) = result?;
|
||||
anyhow::Ok(Some(
|
||||
EditPredictionResult::new(
|
||||
EditPredictionId(id.into()),
|
||||
&buffer,
|
||||
&old_snapshot,
|
||||
edits.into(),
|
||||
None,
|
||||
inputs,
|
||||
None,
|
||||
cx.background_executor().now() - request_start,
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn build_prompt(inputs: &ZetaPromptInput) -> String {
|
||||
const RECENTLY_VIEWED_SNIPPETS_START: &str = "<|recently_viewed_code_snippets|>\n";
|
||||
const RECENTLY_VIEWED_SNIPPETS_END: &str = "<|/recently_viewed_code_snippets|>\n";
|
||||
const RECENTLY_VIEWED_SNIPPET_START: &str = "<|recently_viewed_code_snippet|>\n";
|
||||
const RECENTLY_VIEWED_SNIPPET_END: &str = "<|/recently_viewed_code_snippet|>\n";
|
||||
const CURRENT_FILE_CONTENT_START: &str = "<|current_file_content|>\n";
|
||||
const CURRENT_FILE_CONTENT_END: &str = "<|/current_file_content|>\n";
|
||||
const CODE_TO_EDIT_START: &str = "<|code_to_edit|>\n";
|
||||
const CODE_TO_EDIT_END: &str = "<|/code_to_edit|>\n";
|
||||
const EDIT_DIFF_HISTORY_START: &str = "<|edit_diff_history|>\n";
|
||||
const EDIT_DIFF_HISTORY_END: &str = "<|/edit_diff_history|>\n";
|
||||
const CURSOR_TAG: &str = "<|cursor|>";
|
||||
const CODE_SNIPPET_FILE_PATH_PREFIX: &str = "code_snippet_file_path: ";
|
||||
const CURRENT_FILE_PATH_PREFIX: &str = "current_file_path: ";
|
||||
|
||||
let mut prompt = String::new();
|
||||
|
||||
push_delimited(
|
||||
&mut prompt,
|
||||
RECENTLY_VIEWED_SNIPPETS_START..RECENTLY_VIEWED_SNIPPETS_END,
|
||||
|prompt| {
|
||||
for related_file in inputs.related_files.as_deref().unwrap_or_default().iter() {
|
||||
for related_excerpt in &related_file.excerpts {
|
||||
push_delimited(
|
||||
prompt,
|
||||
RECENTLY_VIEWED_SNIPPET_START..RECENTLY_VIEWED_SNIPPET_END,
|
||||
|prompt| {
|
||||
prompt.push_str(CODE_SNIPPET_FILE_PATH_PREFIX);
|
||||
prompt.push_str(related_file.path.to_string_lossy().as_ref());
|
||||
prompt.push('\n');
|
||||
prompt.push_str(related_excerpt.text.as_ref());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
push_delimited(
|
||||
&mut prompt,
|
||||
CURRENT_FILE_CONTENT_START..CURRENT_FILE_CONTENT_END,
|
||||
|prompt| {
|
||||
prompt.push_str(CURRENT_FILE_PATH_PREFIX);
|
||||
prompt.push_str(inputs.cursor_path.as_os_str().to_string_lossy().as_ref());
|
||||
prompt.push('\n');
|
||||
|
||||
let editable_range = &inputs.excerpt_ranges.editable_350;
|
||||
prompt.push_str(&inputs.cursor_excerpt[0..editable_range.start]);
|
||||
push_delimited(prompt, CODE_TO_EDIT_START..CODE_TO_EDIT_END, |prompt| {
|
||||
prompt.push_str(
|
||||
&inputs.cursor_excerpt[editable_range.start..inputs.cursor_offset_in_excerpt],
|
||||
);
|
||||
prompt.push_str(CURSOR_TAG);
|
||||
prompt.push_str(
|
||||
&inputs.cursor_excerpt[inputs.cursor_offset_in_excerpt..editable_range.end],
|
||||
);
|
||||
});
|
||||
prompt.push_str(&inputs.cursor_excerpt[editable_range.end..]);
|
||||
},
|
||||
);
|
||||
|
||||
push_delimited(
|
||||
&mut prompt,
|
||||
EDIT_DIFF_HISTORY_START..EDIT_DIFF_HISTORY_END,
|
||||
|prompt| {
|
||||
for event in inputs.events.iter() {
|
||||
zeta_prompt::write_event(prompt, &event);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
prompt
|
||||
}
|
||||
|
||||
fn push_delimited(prompt: &mut String, delimiters: Range<&str>, cb: impl FnOnce(&mut String)) {
|
||||
prompt.push_str(delimiters.start);
|
||||
cb(prompt);
|
||||
prompt.push('\n');
|
||||
prompt.push_str(delimiters.end);
|
||||
}
|
||||
|
||||
pub const MERCURY_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
|
||||
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("{0}")]
|
||||
struct MercuryPaymentRequiredError(SharedString);
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MercuryErrorResponse {
|
||||
error: MercuryErrorMessage,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MercuryErrorMessage {
|
||||
message: String,
|
||||
}
|
||||
|
||||
fn is_mercury_payment_required_error(error: &anyhow::Error) -> bool {
|
||||
error
|
||||
.downcast_ref::<MercuryPaymentRequiredError>()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn mercury_payment_required_message(body: &[u8]) -> SharedString {
|
||||
serde_json::from_slice::<MercuryErrorResponse>(body)
|
||||
.map(|response| response.error.message.into())
|
||||
.unwrap_or_else(|_| String::from_utf8_lossy(body).trim().to_string().into())
|
||||
}
|
||||
|
||||
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
|
||||
|
||||
struct GlobalMercuryApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalMercuryApiKey {}
|
||||
|
||||
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
let entity =
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
|
||||
cx.set_global(GlobalMercuryApiKey(entity.clone()));
|
||||
entity
|
||||
}
|
||||
|
||||
pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
let credentials_provider = zed_credentials_provider::global(cx);
|
||||
mercury_api_token(cx).update(cx, |key_state, cx| {
|
||||
key_state.load_if_needed(MERCURY_CREDENTIALS_URL, |s| s, credentials_provider, cx)
|
||||
})
|
||||
}
|
||||
|
||||
const FEEDBACK_API_URL: &str = "https://api-feedback.inceptionlabs.ai/feedback";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum MercuryUserAction {
|
||||
Accept,
|
||||
Reject,
|
||||
Ignore,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FeedbackRequest {
|
||||
request_id: SharedString,
|
||||
provider_name: &'static str,
|
||||
user_action: MercuryUserAction,
|
||||
provider_version: String,
|
||||
}
|
||||
|
||||
pub(crate) fn edit_prediction_accepted(
|
||||
prediction_id: EditPredictionId,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
cx: &App,
|
||||
) {
|
||||
send_feedback(prediction_id, MercuryUserAction::Accept, http_client, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn edit_prediction_rejected(
|
||||
prediction_id: EditPredictionId,
|
||||
was_shown: bool,
|
||||
reason: EditPredictionRejectReason,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
cx: &App,
|
||||
) {
|
||||
if !was_shown {
|
||||
return;
|
||||
}
|
||||
let action = match reason {
|
||||
EditPredictionRejectReason::Rejected => MercuryUserAction::Reject,
|
||||
EditPredictionRejectReason::Discarded => MercuryUserAction::Ignore,
|
||||
_ => return,
|
||||
};
|
||||
send_feedback(prediction_id, action, http_client, cx);
|
||||
}
|
||||
|
||||
fn send_feedback(
|
||||
prediction_id: EditPredictionId,
|
||||
action: MercuryUserAction,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
cx: &App,
|
||||
) {
|
||||
let request_id = prediction_id.0;
|
||||
let app_version = AppVersion::global(cx);
|
||||
cx.background_spawn(async move {
|
||||
let body = FeedbackRequest {
|
||||
request_id,
|
||||
provider_name: "zed",
|
||||
user_action: action,
|
||||
provider_version: app_version.to_string(),
|
||||
};
|
||||
|
||||
let request = http_client::Request::builder()
|
||||
.uri(FEEDBACK_API_URL)
|
||||
.method(Method::POST)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(AsyncBody::from(serde_json::to_vec(&body)?))?;
|
||||
|
||||
let response = http_client.send(request).await?;
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("Feedback API returned status: {}", response.status());
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Mercury feedback sent: request_id={}, action={:?}",
|
||||
body.request_id,
|
||||
body.user_action
|
||||
);
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
8
crates/edit_prediction/src/metrics.rs
Normal file
8
crates/edit_prediction/src/metrics.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub use edit_prediction_metrics::KeptRateResult;
|
||||
|
||||
pub use edit_prediction_metrics::compute_kept_rate;
|
||||
use language::SyntaxLayer;
|
||||
|
||||
pub fn count_tree_sitter_errors<'a>(layers: impl Iterator<Item = SyntaxLayer<'a>>) -> usize {
|
||||
edit_prediction_metrics::count_tree_sitter_errors(layers.map(|layer| layer.node()))
|
||||
}
|
||||
108
crates/edit_prediction/src/ollama.rs
Normal file
108
crates/edit_prediction/src/ollama.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, SharedString, TaskExt,
|
||||
http_client::{self, HttpClient},
|
||||
};
|
||||
use language::language_settings::OpenAiCompatibleEditPredictionSettings;
|
||||
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct OllamaGenerateRequest {
|
||||
model: String,
|
||||
prompt: String,
|
||||
raw: bool,
|
||||
stream: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
options: Option<OllamaGenerateOptions>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct OllamaGenerateOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
num_predict: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
temperature: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stop: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct OllamaGenerateResponse {
|
||||
pub created_at: String,
|
||||
pub response: String,
|
||||
}
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("ollama");
|
||||
|
||||
pub fn is_available(cx: &App) -> bool {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.provider(&PROVIDER_ID)
|
||||
.is_some_and(|provider| provider.is_authenticated(cx))
|
||||
}
|
||||
|
||||
pub fn ensure_authenticated(cx: &mut App) {
|
||||
if let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&PROVIDER_ID) {
|
||||
provider.authenticate(cx).detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_models(cx: &mut App) -> Vec<SharedString> {
|
||||
let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&PROVIDER_ID) else {
|
||||
return Vec::new();
|
||||
};
|
||||
provider.authenticate(cx).detach_and_log_err(cx);
|
||||
let mut models: Vec<SharedString> = provider
|
||||
.provided_models(cx)
|
||||
.into_iter()
|
||||
.map(|model| model.id().0)
|
||||
.collect();
|
||||
models.sort();
|
||||
models
|
||||
}
|
||||
|
||||
pub(crate) async fn make_request(
|
||||
settings: OpenAiCompatibleEditPredictionSettings,
|
||||
prompt: String,
|
||||
stop_tokens: Vec<String>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
) -> Result<OllamaGenerateResponse> {
|
||||
let request = OllamaGenerateRequest {
|
||||
model: settings.model.clone(),
|
||||
prompt,
|
||||
raw: true,
|
||||
stream: false,
|
||||
options: Some(OllamaGenerateOptions {
|
||||
num_predict: Some(settings.max_output_tokens),
|
||||
temperature: Some(0.2),
|
||||
stop: Some(stop_tokens),
|
||||
}),
|
||||
};
|
||||
|
||||
let request_body = serde_json::to_string(&request)?;
|
||||
let http_request = http_client::Request::builder()
|
||||
.method(http_client::Method::POST)
|
||||
.uri(format!("{}/api/generate", settings.api_url))
|
||||
.header("Content-Type", "application/json")
|
||||
.body(http_client::AsyncBody::from(request_body))?;
|
||||
|
||||
let mut response = http_client.send(http_request).await?;
|
||||
let status = response.status();
|
||||
|
||||
log::debug!("Ollama: Response status: {}", status);
|
||||
|
||||
if !status.is_success() {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
return Err(anyhow::anyhow!("Ollama API error: {} - {}", status, body));
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
let ollama_response: OllamaGenerateResponse =
|
||||
serde_json::from_str(&body).context("Failed to parse Ollama response")?;
|
||||
Ok(ollama_response)
|
||||
}
|
||||
167
crates/edit_prediction/src/onboarding_modal.rs
Normal file
167
crates/edit_prediction/src/onboarding_modal.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{EditPredictionStore, ZedPredictUpsell};
|
||||
use ai_onboarding::EditPredictionOnboarding;
|
||||
use client::{Client, UserStore};
|
||||
use db::kvp::Dismissable;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
|
||||
linear_color_stop, linear_gradient,
|
||||
};
|
||||
use language::language_settings::EditPredictionProvider;
|
||||
use settings::update_settings_file;
|
||||
use ui::prelude::*;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! onboarding_event {
|
||||
($name:expr) => {
|
||||
telemetry::event!($name, source = "Edit Prediction Onboarding");
|
||||
};
|
||||
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
|
||||
telemetry::event!($name, source = "Edit Prediction Onboarding", $($key $(= $value)?),+);
|
||||
};
|
||||
}
|
||||
|
||||
/// Introduces user to Zed's Edit Prediction feature
|
||||
pub struct ZedPredictModal {
|
||||
onboarding: Entity<EditPredictionOnboarding>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
pub(crate) fn set_edit_prediction_provider(provider: EditPredictionProvider, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.get_or_insert(Default::default())
|
||||
.provider = Some(provider);
|
||||
});
|
||||
}
|
||||
|
||||
impl ZedPredictModal {
|
||||
pub fn toggle(
|
||||
workspace: &mut Workspace,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let project = workspace.project().clone();
|
||||
workspace.toggle_modal(window, cx, |_window, cx| {
|
||||
let weak_entity = cx.weak_entity();
|
||||
let copilot = EditPredictionStore::try_global(cx)
|
||||
.and_then(|store| store.read(cx).copilot_for_project(&project));
|
||||
Self {
|
||||
onboarding: cx.new(|cx| {
|
||||
EditPredictionOnboarding::new(
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
copilot
|
||||
.as_ref()
|
||||
.is_some_and(|copilot| copilot.read(cx).status().is_configured()),
|
||||
Arc::new({
|
||||
let this = weak_entity.clone();
|
||||
move |_window, cx| {
|
||||
ZedPredictUpsell::set_dismissed(true, cx);
|
||||
set_edit_prediction_provider(EditPredictionProvider::Zed, cx);
|
||||
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
|
||||
}
|
||||
}),
|
||||
Arc::new({
|
||||
let this = weak_entity.clone();
|
||||
move |window, cx| {
|
||||
ZedPredictUpsell::set_dismissed(true, cx);
|
||||
set_edit_prediction_provider(EditPredictionProvider::Copilot, cx);
|
||||
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
|
||||
if let Some(copilot) = copilot.clone() {
|
||||
copilot_ui::initiate_sign_in(copilot, window, cx);
|
||||
}
|
||||
}
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
ZedPredictUpsell::set_dismissed(true, cx);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ZedPredictModal {}
|
||||
|
||||
impl Focusable for ZedPredictModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for ZedPredictModal {
|
||||
fn on_before_dismiss(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> workspace::DismissDecision {
|
||||
ZedPredictUpsell::set_dismissed(true, cx);
|
||||
workspace::DismissDecision::Dismiss(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ZedPredictModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_height = window.viewport_size().height;
|
||||
let max_height = window_height - px(200.);
|
||||
let color = cx.theme().colors();
|
||||
|
||||
v_flex()
|
||||
.id("edit-prediction-onboarding")
|
||||
.key_context("ZedPredictModal")
|
||||
.relative()
|
||||
.w(px(550.))
|
||||
.h_full()
|
||||
.max_h(max_height)
|
||||
.p_1()
|
||||
.gap_2()
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.overflow_hidden()
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
onboarding_event!("Cancelled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.p_3()
|
||||
.size_full()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded(px(5.))
|
||||
.bg(linear_gradient(
|
||||
360.,
|
||||
linear_color_stop(color.panel_background, 1.0),
|
||||
linear_color_stop(color.editor_background, 0.45),
|
||||
))
|
||||
.child(self.onboarding.clone()),
|
||||
)
|
||||
.child(h_flex().absolute().top_3().right_3().child(
|
||||
IconButton::new("cancel", IconName::Close).on_click(cx.listener(
|
||||
|_, _: &ClickEvent, _window, cx| {
|
||||
onboarding_event!("Cancelled", trigger = "X click");
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)),
|
||||
))
|
||||
}
|
||||
}
|
||||
134
crates/edit_prediction/src/open_ai_compatible.rs
Normal file
134
crates/edit_prediction/src/open_ai_compatible.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use cloud_llm_client::predict_edits_v3::{RawCompletionRequest, RawCompletionResponse};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{App, AppContext as _, Entity, Global, SharedString, Task, http_client};
|
||||
use language::language_settings::{OpenAiCompatibleEditPredictionSettings, all_language_settings};
|
||||
use language_model::{ApiKeyState, EnvVar, env_var};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn open_ai_compatible_api_url(cx: &App) -> SharedString {
|
||||
all_language_settings(None, cx)
|
||||
.edit_predictions
|
||||
.open_ai_compatible_api
|
||||
.as_ref()
|
||||
.map(|settings| settings.api_url.clone())
|
||||
.unwrap_or_default()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub const OPEN_AI_COMPATIBLE_CREDENTIALS_USERNAME: &str = "openai-compatible-api-token";
|
||||
pub static OPEN_AI_COMPATIBLE_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> =
|
||||
env_var!("ZED_OPEN_AI_COMPATIBLE_EDIT_PREDICTION_API_KEY");
|
||||
|
||||
struct GlobalOpenAiCompatibleApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalOpenAiCompatibleApiKey {}
|
||||
|
||||
pub fn open_ai_compatible_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
if let Some(global) = cx.try_global::<GlobalOpenAiCompatibleApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
|
||||
let entity = cx.new(|cx| {
|
||||
ApiKeyState::new(
|
||||
open_ai_compatible_api_url(cx),
|
||||
OPEN_AI_COMPATIBLE_TOKEN_ENV_VAR.clone(),
|
||||
)
|
||||
});
|
||||
cx.set_global(GlobalOpenAiCompatibleApiKey(entity.clone()));
|
||||
entity
|
||||
}
|
||||
|
||||
pub fn load_open_ai_compatible_api_token(
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
let credentials_provider = zed_credentials_provider::global(cx);
|
||||
let api_url = open_ai_compatible_api_url(cx);
|
||||
open_ai_compatible_api_token(cx).update(cx, |key_state, cx| {
|
||||
key_state.load_if_needed(api_url, |s| s, credentials_provider, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_open_ai_compatible_api_key_if_needed(
|
||||
provider: settings::EditPredictionProvider,
|
||||
cx: &mut App,
|
||||
) -> Option<Arc<str>> {
|
||||
if provider != settings::EditPredictionProvider::OpenAiCompatibleApi {
|
||||
return None;
|
||||
}
|
||||
_ = load_open_ai_compatible_api_token(cx);
|
||||
let url = open_ai_compatible_api_url(cx);
|
||||
return open_ai_compatible_api_token(cx).read(cx).key(&url);
|
||||
}
|
||||
|
||||
pub(crate) async fn send_custom_server_request(
|
||||
provider: settings::EditPredictionProvider,
|
||||
settings: &OpenAiCompatibleEditPredictionSettings,
|
||||
prompt: String,
|
||||
max_tokens: u32,
|
||||
stop_tokens: Vec<String>,
|
||||
api_key: Option<Arc<str>>,
|
||||
http_client: &Arc<dyn http_client::HttpClient>,
|
||||
) -> Result<(String, String)> {
|
||||
match provider {
|
||||
settings::EditPredictionProvider::Ollama => {
|
||||
let response = crate::ollama::make_request(
|
||||
settings.clone(),
|
||||
prompt,
|
||||
stop_tokens,
|
||||
http_client.clone(),
|
||||
)
|
||||
.await?;
|
||||
Ok((response.response, response.created_at))
|
||||
}
|
||||
_ => {
|
||||
let request = RawCompletionRequest {
|
||||
model: settings.model.clone(),
|
||||
prompt,
|
||||
max_tokens: Some(max_tokens),
|
||||
temperature: None,
|
||||
stop: stop_tokens
|
||||
.into_iter()
|
||||
.map(std::borrow::Cow::Owned)
|
||||
.collect(),
|
||||
environment: None,
|
||||
};
|
||||
|
||||
let request_body = serde_json::to_string(&request)?;
|
||||
let mut http_request_builder = http_client::Request::builder()
|
||||
.method(http_client::Method::POST)
|
||||
.uri(settings.api_url.as_ref())
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
if let Some(api_key) = api_key {
|
||||
http_request_builder =
|
||||
http_request_builder.header("Authorization", format!("Bearer {}", api_key));
|
||||
}
|
||||
|
||||
let http_request =
|
||||
http_request_builder.body(http_client::AsyncBody::from(request_body))?;
|
||||
|
||||
let mut response = http_client.send(http_request).await?;
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
anyhow::bail!("custom server error: {} - {}", status, body);
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
let parsed: RawCompletionResponse =
|
||||
serde_json::from_str(&body).context("Failed to parse completion response")?;
|
||||
let text = parsed
|
||||
.choices
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|choice| choice.text)
|
||||
.unwrap_or_default();
|
||||
Ok((text, parsed.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
31
crates/edit_prediction/src/open_ai_response.rs
Normal file
31
crates/edit_prediction/src/open_ai_response.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
pub fn text_from_response(mut res: open_ai::Response) -> Option<String> {
|
||||
let choice = res.choices.pop()?;
|
||||
let output_text = match choice.message {
|
||||
open_ai::RequestMessage::Assistant {
|
||||
content: Some(open_ai::MessageContent::Plain(content)),
|
||||
..
|
||||
} => content,
|
||||
open_ai::RequestMessage::Assistant {
|
||||
content: Some(open_ai::MessageContent::Multipart(mut content)),
|
||||
..
|
||||
} => {
|
||||
if content.is_empty() {
|
||||
log::error!("No output from Baseten completion response");
|
||||
return None;
|
||||
}
|
||||
|
||||
match content.remove(0) {
|
||||
open_ai::MessagePart::Text { text } => text,
|
||||
open_ai::MessagePart::Image { .. } => {
|
||||
log::error!("Expected text, got an image");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::error!("Invalid response message: {:?}", choice.message);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(output_text)
|
||||
}
|
||||
286
crates/edit_prediction/src/prediction.rs
Normal file
286
crates/edit_prediction/src/prediction.rs
Normal file
@@ -0,0 +1,286 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use cloud_llm_client::EditPredictionRejectReason;
|
||||
use edit_prediction_types::{PredictedCursorPosition, interpolate_edits};
|
||||
use gpui::{AsyncApp, Entity, SharedString};
|
||||
use language::{Anchor, Buffer, BufferSnapshot, EditPreview, TextBufferSnapshot};
|
||||
use zeta_prompt::ZetaPromptInput;
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct EditPredictionId(pub SharedString);
|
||||
|
||||
impl From<EditPredictionId> for gpui::ElementId {
|
||||
fn from(value: EditPredictionId) -> Self {
|
||||
gpui::ElementId::Name(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EditPredictionId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A prediction response that was returned from the provider, whether it was ultimately valid or not.
|
||||
pub struct EditPredictionResult {
|
||||
pub id: EditPredictionId,
|
||||
pub prediction: Result<EditPrediction, EditPredictionRejectReason>,
|
||||
pub model_version: Option<String>,
|
||||
pub e2e_latency: std::time::Duration,
|
||||
}
|
||||
|
||||
impl EditPredictionResult {
|
||||
pub async fn new(
|
||||
id: EditPredictionId,
|
||||
edited_buffer: &Entity<Buffer>,
|
||||
edited_buffer_snapshot: &BufferSnapshot,
|
||||
edits: Arc<[(Range<Anchor>, Arc<str>)]>,
|
||||
cursor_position: Option<PredictedCursorPosition>,
|
||||
inputs: ZetaPromptInput,
|
||||
model_version: Option<String>,
|
||||
e2e_latency: std::time::Duration,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Self {
|
||||
if edits.is_empty() {
|
||||
return Self {
|
||||
id,
|
||||
prediction: Err(EditPredictionRejectReason::Empty),
|
||||
model_version,
|
||||
e2e_latency,
|
||||
};
|
||||
}
|
||||
|
||||
let Some((edits, snapshot, edit_preview_task)) =
|
||||
edited_buffer.read_with(cx, |buffer, cx| {
|
||||
let new_snapshot = buffer.snapshot();
|
||||
let edits: Arc<[_]> =
|
||||
interpolate_edits(&edited_buffer_snapshot, &new_snapshot, &edits)?.into();
|
||||
|
||||
Some((edits.clone(), new_snapshot, buffer.preview_edits(edits, cx)))
|
||||
})
|
||||
else {
|
||||
return Self {
|
||||
id,
|
||||
prediction: Err(EditPredictionRejectReason::InterpolatedEmpty),
|
||||
model_version,
|
||||
e2e_latency,
|
||||
};
|
||||
};
|
||||
|
||||
let edit_preview = edit_preview_task.await;
|
||||
|
||||
Self {
|
||||
id: id.clone(),
|
||||
prediction: Ok(EditPrediction {
|
||||
id,
|
||||
edits,
|
||||
cursor_position,
|
||||
snapshot,
|
||||
edit_preview,
|
||||
inputs,
|
||||
buffer: edited_buffer.clone(),
|
||||
model_version: model_version.clone(),
|
||||
}),
|
||||
model_version,
|
||||
e2e_latency,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EditPrediction {
|
||||
pub id: EditPredictionId,
|
||||
pub edits: Arc<[(Range<Anchor>, Arc<str>)]>,
|
||||
pub cursor_position: Option<PredictedCursorPosition>,
|
||||
pub snapshot: BufferSnapshot,
|
||||
pub edit_preview: EditPreview,
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub inputs: zeta_prompt::ZetaPromptInput,
|
||||
pub model_version: Option<String>,
|
||||
}
|
||||
|
||||
impl EditPrediction {
|
||||
pub fn interpolate(
|
||||
&self,
|
||||
new_snapshot: &TextBufferSnapshot,
|
||||
) -> Option<Vec<(Range<Anchor>, Arc<str>)>> {
|
||||
interpolate_edits(&self.snapshot, new_snapshot, &self.edits)
|
||||
}
|
||||
|
||||
pub fn targets_buffer(&self, buffer: &Buffer) -> bool {
|
||||
self.snapshot.remote_id() == buffer.remote_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EditPrediction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("EditPrediction")
|
||||
.field("id", &self.id)
|
||||
.field("edits", &self.edits)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::*;
|
||||
use gpui::{App, Entity, TestAppContext, prelude::*};
|
||||
use language::{Buffer, ToOffset as _};
|
||||
use zeta_prompt::ZetaPromptInput;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edit_prediction_basic_interpolation(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| Buffer::local("Lorem ipsum dolor", cx));
|
||||
let edits: Arc<[(Range<Anchor>, Arc<str>)]> = cx.update(|cx| {
|
||||
to_prediction_edits([(2..5, "REM".into()), (9..11, "".into())], &buffer, cx).into()
|
||||
});
|
||||
|
||||
let edit_preview = cx
|
||||
.read(|cx| buffer.read(cx).preview_edits(edits.clone(), cx))
|
||||
.await;
|
||||
|
||||
let prediction = EditPrediction {
|
||||
id: EditPredictionId("prediction-1".into()),
|
||||
edits,
|
||||
cursor_position: None,
|
||||
snapshot: cx.read(|cx| buffer.read(cx).snapshot()),
|
||||
buffer: buffer.clone(),
|
||||
edit_preview,
|
||||
model_version: None,
|
||||
inputs: ZetaPromptInput {
|
||||
events: vec![],
|
||||
related_files: Some(vec![]),
|
||||
active_buffer_diagnostics: vec![],
|
||||
cursor_path: Path::new("path.txt").into(),
|
||||
cursor_offset_in_excerpt: 0,
|
||||
cursor_excerpt: "".into(),
|
||||
excerpt_start_row: None,
|
||||
excerpt_ranges: Default::default(),
|
||||
syntax_ranges: None,
|
||||
in_open_source_repo: false,
|
||||
can_collect_data: false,
|
||||
repo_url: None,
|
||||
},
|
||||
};
|
||||
|
||||
cx.update(|cx| {
|
||||
assert_eq!(
|
||||
from_prediction_edits(
|
||||
&prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx
|
||||
),
|
||||
vec![(2..5, "REM".into()), (9..11, "".into())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx));
|
||||
assert_eq!(
|
||||
from_prediction_edits(
|
||||
&prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx
|
||||
),
|
||||
vec![(2..2, "REM".into()), (6..8, "".into())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
assert_eq!(
|
||||
from_prediction_edits(
|
||||
&prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx
|
||||
),
|
||||
vec![(2..5, "REM".into()), (9..11, "".into())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx));
|
||||
assert_eq!(
|
||||
from_prediction_edits(
|
||||
&prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx
|
||||
),
|
||||
vec![(3..3, "EM".into()), (7..9, "".into())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx));
|
||||
assert_eq!(
|
||||
from_prediction_edits(
|
||||
&prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx
|
||||
),
|
||||
vec![(4..4, "M".into()), (8..10, "".into())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx));
|
||||
assert_eq!(
|
||||
from_prediction_edits(
|
||||
&prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx
|
||||
),
|
||||
vec![(9..11, "".into())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx));
|
||||
assert_eq!(
|
||||
from_prediction_edits(
|
||||
&prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx
|
||||
),
|
||||
vec![(4..4, "M".into()), (8..10, "".into())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx));
|
||||
assert_eq!(
|
||||
from_prediction_edits(
|
||||
&prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx
|
||||
),
|
||||
vec![(4..4, "M".into())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx));
|
||||
assert_eq!(prediction.interpolate(&buffer.read(cx).snapshot()), None);
|
||||
})
|
||||
}
|
||||
|
||||
fn to_prediction_edits(
|
||||
iterator: impl IntoIterator<Item = (Range<usize>, Arc<str>)>,
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &App,
|
||||
) -> Vec<(Range<Anchor>, Arc<str>)> {
|
||||
let buffer = buffer.read(cx);
|
||||
iterator
|
||||
.into_iter()
|
||||
.map(|(range, text)| {
|
||||
(
|
||||
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||
text,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn from_prediction_edits(
|
||||
editor_edits: &[(Range<Anchor>, Arc<str>)],
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &App,
|
||||
) -> Vec<(Range<usize>, Arc<str>)> {
|
||||
let buffer = buffer.read(cx);
|
||||
editor_edits
|
||||
.iter()
|
||||
.map(|(range, text)| {
|
||||
(
|
||||
range.start.to_offset(buffer)..range.end.to_offset(buffer),
|
||||
text.clone(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
615
crates/edit_prediction/src/udiff.rs
Normal file
615
crates/edit_prediction/src/udiff.rs
Normal file
@@ -0,0 +1,615 @@
|
||||
use std::{mem, ops::Range, path::Path, path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::{HashMap, hash_map::Entry};
|
||||
use gpui::{AsyncApp, Entity};
|
||||
use language::{Anchor, Buffer, OffsetRangeExt as _, TextBufferSnapshot, text_diff};
|
||||
use postage::stream::Stream as _;
|
||||
use project::Project;
|
||||
use util::{paths::PathStyle, rel_path::RelPath};
|
||||
use worktree::Worktree;
|
||||
use zeta_prompt::udiff::{
|
||||
DiffEvent, DiffParser, FileStatus, Hunk, disambiguate_by_line_number, find_context_candidates,
|
||||
};
|
||||
|
||||
pub use zeta_prompt::udiff::{
|
||||
DiffLine, HunkLocation, apply_diff_to_string, apply_diff_to_string_with_hunk_offset,
|
||||
strip_diff_metadata, strip_diff_path_prefix,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OpenedBuffers(HashMap<String, Entity<Buffer>>);
|
||||
|
||||
impl OpenedBuffers {
|
||||
pub fn get(&self, path: &str) -> Option<&Entity<Buffer>> {
|
||||
self.0.get(path)
|
||||
}
|
||||
|
||||
pub fn buffers(&self) -> impl Iterator<Item = &Entity<Buffer>> {
|
||||
self.0.values()
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub async fn apply_diff(
|
||||
diff_str: &str,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<OpenedBuffers> {
|
||||
let worktree = project
|
||||
.read_with(cx, |project, cx| project.visible_worktrees(cx).next())
|
||||
.context("project has no worktree")?;
|
||||
|
||||
let paths: Vec<_> = diff_str
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
if let DiffLine::OldPath { path } = DiffLine::parse(line) {
|
||||
if path != "/dev/null" {
|
||||
return Some(PathBuf::from(path.as_ref()));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
refresh_worktree_entries(&worktree, paths.iter().map(|p| p.as_path()), cx).await?;
|
||||
|
||||
let mut included_files: HashMap<String, Entity<Buffer>> = HashMap::default();
|
||||
|
||||
let mut diff = DiffParser::new(diff_str);
|
||||
let mut current_file = None;
|
||||
let mut edits: Vec<(std::ops::Range<Anchor>, Arc<str>)> = vec![];
|
||||
|
||||
while let Some(event) = diff.next()? {
|
||||
match event {
|
||||
DiffEvent::Hunk { path, hunk, status } => {
|
||||
if status == FileStatus::Deleted {
|
||||
let delete_task = project.update(cx, |project, cx| {
|
||||
if let Some(path) = project.find_project_path(path.as_ref(), cx) {
|
||||
project.delete_file(path, false, cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(delete_task) = delete_task {
|
||||
delete_task.await?;
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let buffer = match current_file {
|
||||
None => {
|
||||
let buffer = match included_files.entry(path.to_string()) {
|
||||
Entry::Occupied(entry) => entry.get().clone(),
|
||||
Entry::Vacant(entry) => {
|
||||
let buffer: Entity<Buffer> = if status == FileStatus::Created {
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_buffer(None, true, cx)
|
||||
})
|
||||
.await?
|
||||
} else {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_project_path(path.as_ref(), cx)
|
||||
})
|
||||
.with_context(|| format!("no such path: {}", path))?;
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path, cx)
|
||||
})
|
||||
.await?
|
||||
};
|
||||
entry.insert(buffer.clone());
|
||||
buffer
|
||||
}
|
||||
};
|
||||
current_file = Some(buffer);
|
||||
current_file.as_ref().unwrap()
|
||||
}
|
||||
Some(ref current) => current,
|
||||
};
|
||||
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
edits.extend(resolve_hunk_edits_in_buffer(
|
||||
hunk,
|
||||
buffer,
|
||||
&[Anchor::min_max_range_for_buffer(buffer.remote_id())],
|
||||
status,
|
||||
)?);
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
}
|
||||
DiffEvent::FileEnd { renamed_to } => {
|
||||
let buffer = current_file
|
||||
.take()
|
||||
.context("Got a FileEnd event before an Hunk event")?;
|
||||
|
||||
if let Some(renamed_to) = renamed_to {
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
let new_project_path = project
|
||||
.find_project_path(Path::new(renamed_to.as_ref()), cx)
|
||||
.with_context(|| {
|
||||
format!("Failed to find worktree for new path: {}", renamed_to)
|
||||
})?;
|
||||
|
||||
let project_file = project::File::from_dyn(buffer.read(cx).file())
|
||||
.expect("Wrong file type");
|
||||
|
||||
anyhow::Ok(project.rename_entry(
|
||||
project_file.entry_id.unwrap(),
|
||||
new_project_path,
|
||||
cx,
|
||||
))
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
let edits = mem::take(&mut edits);
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, None, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OpenedBuffers(included_files))
|
||||
}
|
||||
|
||||
pub async fn refresh_worktree_entries(
|
||||
worktree: &Entity<Worktree>,
|
||||
paths: impl IntoIterator<Item = &Path>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let mut rel_paths = Vec::new();
|
||||
for path in paths {
|
||||
if let Ok(rel_path) = RelPath::new(path, PathStyle::Posix) {
|
||||
rel_paths.push(rel_path.into_arc());
|
||||
}
|
||||
|
||||
let path_without_root: PathBuf = path.components().skip(1).collect();
|
||||
if let Ok(rel_path) = RelPath::new(&path_without_root, PathStyle::Posix) {
|
||||
rel_paths.push(rel_path.into_arc());
|
||||
}
|
||||
}
|
||||
|
||||
if !rel_paths.is_empty() {
|
||||
worktree
|
||||
.update(cx, |worktree, _| {
|
||||
worktree
|
||||
.as_local()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(rel_paths)
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the individual edits that would be applied by a diff to the given content.
|
||||
/// Each edit is a tuple of (byte_range_in_content, replacement_text).
|
||||
/// Uses sub-line diffing to find the precise character positions of changes.
|
||||
/// Returns an empty vec if the hunk context is not found or is ambiguous.
|
||||
pub fn edits_for_diff(content: &str, diff_str: &str) -> Result<Vec<(Range<usize>, String)>> {
|
||||
let mut diff = DiffParser::new(diff_str);
|
||||
let mut result = Vec::new();
|
||||
|
||||
while let Some(event) = diff.next()? {
|
||||
match event {
|
||||
DiffEvent::Hunk {
|
||||
mut hunk,
|
||||
path: _,
|
||||
status: _,
|
||||
} => {
|
||||
if hunk.context.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let candidates = find_context_candidates(content, &mut hunk);
|
||||
|
||||
let Some(context_offset) =
|
||||
disambiguate_by_line_number(&candidates, hunk.start_line, &|offset| {
|
||||
content[..offset].matches('\n').count() as u32
|
||||
})
|
||||
else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
// Use sub-line diffing to find precise edit positions
|
||||
for edit in &hunk.edits {
|
||||
let old_text = &content
|
||||
[context_offset + edit.range.start..context_offset + edit.range.end];
|
||||
let edits_within_hunk = text_diff(old_text, &edit.text);
|
||||
for (inner_range, inner_text) in edits_within_hunk {
|
||||
let absolute_start = context_offset + edit.range.start + inner_range.start;
|
||||
let absolute_end = context_offset + edit.range.start + inner_range.end;
|
||||
result.push((absolute_start..absolute_end, inner_text.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
DiffEvent::FileEnd { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn resolve_hunk_edits_in_buffer(
|
||||
mut hunk: Hunk,
|
||||
buffer: &TextBufferSnapshot,
|
||||
ranges: &[Range<Anchor>],
|
||||
status: FileStatus,
|
||||
) -> Result<impl Iterator<Item = (Range<Anchor>, Arc<str>)>, anyhow::Error> {
|
||||
let context_offset = if status == FileStatus::Created || hunk.context.is_empty() {
|
||||
0
|
||||
} else {
|
||||
let mut candidates: Vec<usize> = Vec::new();
|
||||
for range in ranges {
|
||||
let range = range.to_offset(buffer);
|
||||
let text = buffer.text_for_range(range.clone()).collect::<String>();
|
||||
for ix in find_context_candidates(&text, &mut hunk) {
|
||||
candidates.push(range.start + ix);
|
||||
}
|
||||
}
|
||||
|
||||
disambiguate_by_line_number(&candidates, hunk.start_line, &|offset| {
|
||||
buffer.offset_to_point(offset).row
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
if candidates.is_empty() {
|
||||
anyhow!("Failed to match context:\n\n```\n{}```\n", hunk.context,)
|
||||
} else {
|
||||
anyhow!("Context is not unique enough:\n{}", hunk.context)
|
||||
}
|
||||
})?
|
||||
};
|
||||
|
||||
if let Some(edit) = hunk.edits.iter().find(|edit| edit.range.end > buffer.len()) {
|
||||
return Err(anyhow!("Edit range {:?} exceeds buffer length", edit.range));
|
||||
}
|
||||
|
||||
let iter = hunk.edits.into_iter().flat_map(move |edit| {
|
||||
let old_text = buffer
|
||||
.text_for_range(context_offset + edit.range.start..context_offset + edit.range.end)
|
||||
.collect::<String>();
|
||||
let edits_within_hunk = language::text_diff(&old_text, &edit.text);
|
||||
edits_within_hunk
|
||||
.into_iter()
|
||||
.map(move |(inner_range, inner_text)| {
|
||||
(
|
||||
buffer.anchor_after(context_offset + edit.range.start + inner_range.start)
|
||||
..buffer.anchor_before(context_offset + edit.range.start + inner_range.end),
|
||||
inner_text,
|
||||
)
|
||||
})
|
||||
});
|
||||
Ok(iter)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
#[test]
|
||||
fn test_line_number_disambiguation() {
|
||||
// Test that line numbers from hunk headers are used to disambiguate
|
||||
// when context before the operation appears multiple times
|
||||
let content = indoc! {"
|
||||
repeated line
|
||||
first unique
|
||||
repeated line
|
||||
second unique
|
||||
"};
|
||||
|
||||
// Context "repeated line" appears twice - line number selects first occurrence
|
||||
let diff = indoc! {"
|
||||
--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ -1,2 +1,2 @@
|
||||
repeated line
|
||||
-first unique
|
||||
+REPLACED
|
||||
"};
|
||||
|
||||
let result = edits_for_diff(content, diff).unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
|
||||
// The edit should replace "first unique" (after first "repeated line\n" at offset 14)
|
||||
let (range, text) = &result[0];
|
||||
assert_eq!(range.start, 14);
|
||||
assert_eq!(range.end, 26); // "first unique" is 12 bytes
|
||||
assert_eq!(text, "REPLACED");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_line_number_disambiguation_second_match() {
|
||||
// Test disambiguation when the edit should apply to a later occurrence
|
||||
let content = indoc! {"
|
||||
repeated line
|
||||
first unique
|
||||
repeated line
|
||||
second unique
|
||||
"};
|
||||
|
||||
// Context "repeated line" appears twice - line number selects second occurrence
|
||||
let diff = indoc! {"
|
||||
--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ -3,2 +3,2 @@
|
||||
repeated line
|
||||
-second unique
|
||||
+REPLACED
|
||||
"};
|
||||
|
||||
let result = edits_for_diff(content, diff).unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
|
||||
// The edit should replace "second unique" (after second "repeated line\n")
|
||||
// Offset: "repeated line\n" (14) + "first unique\n" (13) + "repeated line\n" (14) = 41
|
||||
let (range, text) = &result[0];
|
||||
assert_eq!(range.start, 41);
|
||||
assert_eq!(range.end, 54); // "second unique" is 13 bytes
|
||||
assert_eq!(text, "REPLACED");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_apply_diff_successful(cx: &mut TestAppContext) {
|
||||
let fs = init_test(cx);
|
||||
|
||||
let buffer_1_text = indoc! {r#"
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
"# };
|
||||
|
||||
let buffer_1_text_final = indoc! {r#"
|
||||
3
|
||||
4
|
||||
5
|
||||
"# };
|
||||
|
||||
let buffer_2_text = indoc! {r#"
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
"# };
|
||||
|
||||
let buffer_2_text_final = indoc! {r#"
|
||||
5
|
||||
six
|
||||
seven
|
||||
7.5
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
11
|
||||
"# };
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"file1": buffer_1_text,
|
||||
"file2": buffer_2_text,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let diff = indoc! {r#"
|
||||
--- a/file1
|
||||
+++ b/file1
|
||||
one
|
||||
two
|
||||
-three
|
||||
+3
|
||||
four
|
||||
five
|
||||
--- a/file1
|
||||
+++ b/file1
|
||||
3
|
||||
-four
|
||||
-five
|
||||
+4
|
||||
+5
|
||||
--- a/file1
|
||||
+++ b/file1
|
||||
-one
|
||||
-two
|
||||
3
|
||||
4
|
||||
--- a/file2
|
||||
+++ b/file2
|
||||
+5
|
||||
six
|
||||
--- a/file2
|
||||
+++ b/file2
|
||||
seven
|
||||
+7.5
|
||||
eight
|
||||
--- a/file2
|
||||
+++ b/file2
|
||||
ten
|
||||
+11
|
||||
"#};
|
||||
|
||||
let _buffers = apply_diff(diff, &project, &mut cx.to_async())
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer_1 = project
|
||||
.update(cx, |project, cx| {
|
||||
let project_path = project.find_project_path(path!("/root/file1"), cx).unwrap();
|
||||
project.open_buffer(project_path, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
buffer_1.read_with(cx, |buffer, _cx| {
|
||||
assert_eq!(buffer.text(), buffer_1_text_final);
|
||||
});
|
||||
let buffer_2 = project
|
||||
.update(cx, |project, cx| {
|
||||
let project_path = project.find_project_path(path!("/root/file2"), cx).unwrap();
|
||||
project.open_buffer(project_path, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
buffer_2.read_with(cx, |buffer, _cx| {
|
||||
assert_eq!(buffer.text(), buffer_2_text_final);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_apply_diff_unique_via_previous_context(cx: &mut TestAppContext) {
|
||||
let fs = init_test(cx);
|
||||
|
||||
let start = indoc! {r#"
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
|
||||
four
|
||||
five
|
||||
"# };
|
||||
|
||||
let end = indoc! {r#"
|
||||
one
|
||||
two
|
||||
3
|
||||
four
|
||||
5
|
||||
|
||||
four
|
||||
five
|
||||
"# };
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"file1": start,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let diff = indoc! {r#"
|
||||
--- a/file1
|
||||
+++ b/file1
|
||||
one
|
||||
two
|
||||
-three
|
||||
+3
|
||||
four
|
||||
-five
|
||||
+5
|
||||
"#};
|
||||
|
||||
let _buffers = apply_diff(diff, &project, &mut cx.to_async())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer_1 = project
|
||||
.update(cx, |project, cx| {
|
||||
let project_path = project.find_project_path(path!("/root/file1"), cx).unwrap();
|
||||
project.open_buffer(project_path, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
buffer_1.read_with(cx, |buffer, _cx| {
|
||||
assert_eq!(buffer.text(), end);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
|
||||
FakeFs::new(cx.background_executor.clone())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edits_for_diff() {
|
||||
let content = indoc! {"
|
||||
fn main() {
|
||||
let x = 1;
|
||||
let y = 2;
|
||||
println!(\"{} {}\", x, y);
|
||||
}
|
||||
"};
|
||||
|
||||
let diff = indoc! {"
|
||||
--- a/file.rs
|
||||
+++ b/file.rs
|
||||
@@ -1,5 +1,5 @@
|
||||
fn main() {
|
||||
- let x = 1;
|
||||
+ let x = 42;
|
||||
let y = 2;
|
||||
println!(\"{} {}\", x, y);
|
||||
}
|
||||
"};
|
||||
|
||||
let edits = edits_for_diff(content, diff).unwrap();
|
||||
assert_eq!(edits.len(), 1);
|
||||
|
||||
let (range, replacement) = &edits[0];
|
||||
// With sub-line diffing, the edit should start at "1" (the actual changed character)
|
||||
let expected_start = content.find("let x = 1;").unwrap() + "let x = ".len();
|
||||
assert_eq!(range.start, expected_start);
|
||||
// The deleted text is just "1"
|
||||
assert_eq!(range.end, expected_start + "1".len());
|
||||
// The replacement text
|
||||
assert_eq!(replacement, "42");
|
||||
|
||||
// Verify the cursor would be positioned at the column of "1"
|
||||
let line_start = content[..range.start]
|
||||
.rfind('\n')
|
||||
.map(|p| p + 1)
|
||||
.unwrap_or(0);
|
||||
let cursor_column = range.start - line_start;
|
||||
// " let x = " is 12 characters, so column 12
|
||||
assert_eq!(cursor_column, " let x = ".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edits_for_diff_no_trailing_newline() {
|
||||
let content = "foo\nbar\nbaz";
|
||||
let diff = indoc! {"
|
||||
--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ -1,3 +1,3 @@
|
||||
foo
|
||||
-bar
|
||||
+qux
|
||||
baz
|
||||
"};
|
||||
|
||||
let result = edits_for_diff(content, diff).unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
let (range, text) = &result[0];
|
||||
assert_eq!(&content[range.clone()], "bar");
|
||||
assert_eq!(text, "qux");
|
||||
}
|
||||
}
|
||||
264
crates/edit_prediction/src/zed_edit_prediction_delegate.rs
Normal file
264
crates/edit_prediction/src/zed_edit_prediction_delegate.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
use std::{cmp, sync::Arc};
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use cloud_llm_client::EditPredictionRejectReason;
|
||||
use edit_prediction_types::{
|
||||
DataCollectionState, EditPredictionDelegate, EditPredictionDiscardReason,
|
||||
EditPredictionIconSet, SuggestionDisplayType,
|
||||
};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::{App, Entity, prelude::*};
|
||||
use language::{Buffer, ToPoint as _};
|
||||
use project::Project;
|
||||
use settings::{EditPredictionDataCollectionChoice, update_settings_file};
|
||||
|
||||
use crate::{BufferEditPrediction, EditPredictionStore};
|
||||
|
||||
pub struct ZedEditPredictionDelegate {
|
||||
store: Entity<EditPredictionStore>,
|
||||
project: Entity<Project>,
|
||||
singleton_buffer: Option<Entity<Buffer>>,
|
||||
}
|
||||
|
||||
impl ZedEditPredictionDelegate {
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
singleton_buffer: Option<Entity<Buffer>>,
|
||||
client: &Arc<Client>,
|
||||
user_store: &Entity<UserStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let store = EditPredictionStore::global(client, user_store, cx);
|
||||
store.update(cx, |store, cx| {
|
||||
store.register_project(&project, cx);
|
||||
});
|
||||
|
||||
cx.observe(&store, |_this, _ep_store, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
project: project,
|
||||
store: store,
|
||||
singleton_buffer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EditPredictionDelegate for ZedEditPredictionDelegate {
|
||||
fn name() -> &'static str {
|
||||
"zed-predict"
|
||||
}
|
||||
|
||||
fn display_name() -> &'static str {
|
||||
"Zed's Edit Predictions"
|
||||
}
|
||||
|
||||
fn show_predictions_in_menu() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn show_tab_accept_marker() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn icons(&self, cx: &App) -> EditPredictionIconSet {
|
||||
self.store.read(cx).icons(cx)
|
||||
}
|
||||
|
||||
fn data_collection_state(&self, cx: &App) -> DataCollectionState {
|
||||
if let Some(buffer) = &self.singleton_buffer
|
||||
&& let Some(file) = buffer.read(cx).file()
|
||||
{
|
||||
let is_project_open_source =
|
||||
self.store
|
||||
.read(cx)
|
||||
.is_file_open_source(&self.project, file, cx);
|
||||
|
||||
if self.store.read(cx).is_data_collection_enabled(cx) {
|
||||
DataCollectionState::Enabled {
|
||||
is_project_open_source,
|
||||
}
|
||||
} else {
|
||||
DataCollectionState::Disabled {
|
||||
is_project_open_source,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DataCollectionState::Disabled {
|
||||
is_project_open_source: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn can_toggle_data_collection(&self, cx: &App) -> bool {
|
||||
if cx.is_staff() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.store
|
||||
.read(cx)
|
||||
.is_data_collection_allowed_by_organization(cx)
|
||||
}
|
||||
|
||||
fn toggle_data_collection(&mut self, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
let is_currently_enabled = self.store.read(cx).is_data_collection_enabled(cx);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let edit_predictions = settings
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.get_or_insert_default();
|
||||
|
||||
edit_predictions.allow_data_collection = Some(if is_currently_enabled {
|
||||
EditPredictionDataCollectionChoice::No
|
||||
} else {
|
||||
EditPredictionDataCollectionChoice::Yes
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn usage(&self, cx: &App) -> Option<client::EditPredictionUsage> {
|
||||
self.store.read(cx).usage(cx)
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
_buffer: &Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_cx: &App,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_refreshing(&self, cx: &App) -> bool {
|
||||
self.store.read(cx).is_refreshing(&self.project)
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&mut self,
|
||||
buffer: Entity<language::Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
_debounce: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let store = self.store.read(cx);
|
||||
|
||||
if store.user_store.read_with(cx, |user_store, _cx| {
|
||||
user_store.account_too_young() || user_store.has_overdue_invoices()
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.store.update(cx, |store, cx| {
|
||||
if let Some(current) =
|
||||
store.prediction_at(&buffer, Some(cursor_position), &self.project, cx)
|
||||
&& let BufferEditPrediction::Local { prediction } = current
|
||||
&& prediction.interpolate(buffer.read(cx)).is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
store.refresh_context(&self.project, &buffer, cursor_position, cx);
|
||||
store.refresh_prediction_from_buffer(self.project.clone(), buffer, cursor_position, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
self.store.update(cx, |store, cx| {
|
||||
store.accept_current_prediction(&self.project, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn discard(&mut self, reason: EditPredictionDiscardReason, cx: &mut Context<Self>) {
|
||||
let reject_reason = match reason {
|
||||
EditPredictionDiscardReason::Rejected => EditPredictionRejectReason::Rejected,
|
||||
EditPredictionDiscardReason::Ignored => EditPredictionRejectReason::Discarded,
|
||||
};
|
||||
self.store.update(cx, |store, cx| {
|
||||
store.reject_current_prediction(reject_reason, &self.project, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn did_show(&mut self, display_type: SuggestionDisplayType, cx: &mut Context<Self>) {
|
||||
self.store.update(cx, |store, cx| {
|
||||
store.did_show_current_prediction(&self.project, display_type, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn suggest(
|
||||
&mut self,
|
||||
buffer: &Entity<language::Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<edit_prediction_types::EditPrediction> {
|
||||
self.store.update(cx, |store, cx| {
|
||||
let prediction =
|
||||
store.prediction_at(buffer, Some(cursor_position), &self.project, cx)?;
|
||||
|
||||
let prediction = match prediction {
|
||||
BufferEditPrediction::Local { prediction } => prediction,
|
||||
BufferEditPrediction::Jump { prediction } => {
|
||||
return Some(edit_prediction_types::EditPrediction::Jump {
|
||||
id: Some(prediction.id.0.clone()),
|
||||
snapshot: prediction.snapshot.clone(),
|
||||
target: prediction.edits.first().unwrap().0.start,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
let Some(edits) = prediction.interpolate(&snapshot) else {
|
||||
store.reject_current_prediction(
|
||||
EditPredictionRejectReason::InterpolatedEmpty,
|
||||
&self.project,
|
||||
cx,
|
||||
);
|
||||
return None;
|
||||
};
|
||||
|
||||
let cursor_row = cursor_position.to_point(&snapshot).row;
|
||||
let (closest_edit_ix, (closest_edit_range, _)) =
|
||||
edits.iter().enumerate().min_by_key(|(_, (range, _))| {
|
||||
let distance_from_start =
|
||||
cursor_row.abs_diff(range.start.to_point(&snapshot).row);
|
||||
let distance_from_end = cursor_row.abs_diff(range.end.to_point(&snapshot).row);
|
||||
cmp::min(distance_from_start, distance_from_end)
|
||||
})?;
|
||||
|
||||
let mut edit_start_ix = closest_edit_ix;
|
||||
for (range, _) in edits[..edit_start_ix].iter().rev() {
|
||||
let distance_from_closest_edit = closest_edit_range.start.to_point(&snapshot).row
|
||||
- range.end.to_point(&snapshot).row;
|
||||
if distance_from_closest_edit <= 1 {
|
||||
edit_start_ix -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut edit_end_ix = closest_edit_ix + 1;
|
||||
for (range, _) in &edits[edit_end_ix..] {
|
||||
let distance_from_closest_edit = range.start.to_point(buffer).row
|
||||
- closest_edit_range.end.to_point(&snapshot).row;
|
||||
if distance_from_closest_edit <= 1 {
|
||||
edit_end_ix += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Some(edit_prediction_types::EditPrediction::Local {
|
||||
id: Some(prediction.id.0.clone()),
|
||||
edits: edits[edit_start_ix..edit_end_ix].to_vec(),
|
||||
cursor_position: prediction.cursor_position,
|
||||
edit_preview: Some(prediction.edit_preview.clone()),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
772
crates/edit_prediction/src/zeta.rs
Normal file
772
crates/edit_prediction/src/zeta.rs
Normal file
@@ -0,0 +1,772 @@
|
||||
use crate::{
|
||||
CurrentEditPrediction, DebugEvent, EditPredictionFinishedDebugEvent, EditPredictionId,
|
||||
EditPredictionModelInput, EditPredictionStartedDebugEvent, EditPredictionStore, StoredEvent,
|
||||
ZedUpdateRequiredError, buffer_path_with_id_fallback,
|
||||
cursor_excerpt::{self, compute_cursor_excerpt, compute_syntax_ranges},
|
||||
prediction::EditPredictionResult,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use cloud_llm_client::{
|
||||
AcceptEditPredictionBody, EditPredictionRejectReason, predict_edits_v3::RawCompletionRequest,
|
||||
};
|
||||
use edit_prediction_types::PredictedCursorPosition;
|
||||
use gpui::{App, AppContext as _, Entity, Task, TaskExt, WeakEntity, prelude::*};
|
||||
use language::{
|
||||
Buffer, BufferSnapshot, DiagnosticSeverity, EditPredictionPromptFormat, OffsetRangeExt as _,
|
||||
ToOffset as _, ZetaVersion, language_settings::all_language_settings, text_diff,
|
||||
};
|
||||
use release_channel::AppVersion;
|
||||
use text::{Anchor, Bias, Point};
|
||||
use ui::SharedString;
|
||||
use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification};
|
||||
use zeta_prompt::{ParsedOutput, ZetaPromptInput};
|
||||
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
use zeta_prompt::{
|
||||
ZetaFormat, format_zeta_prompt, get_prefill, parse_zeta2_model_output, stop_tokens_for_format,
|
||||
zeta1::{self, EDITABLE_REGION_END_MARKER},
|
||||
};
|
||||
|
||||
use crate::open_ai_compatible::{
|
||||
load_open_ai_compatible_api_key_if_needed, send_custom_server_request,
|
||||
};
|
||||
|
||||
pub fn request_prediction_with_zeta(
|
||||
store: &mut EditPredictionStore,
|
||||
EditPredictionModelInput {
|
||||
buffer,
|
||||
snapshot,
|
||||
position,
|
||||
related_files,
|
||||
events,
|
||||
debug_tx,
|
||||
mode,
|
||||
trigger,
|
||||
project,
|
||||
diagnostic_search_range,
|
||||
can_collect_data,
|
||||
is_open_source,
|
||||
..
|
||||
}: EditPredictionModelInput,
|
||||
capture_data: Option<Vec<StoredEvent>>,
|
||||
cx: &mut Context<EditPredictionStore>,
|
||||
) -> Task<Result<Option<EditPredictionResult>>> {
|
||||
let settings = &all_language_settings(None, cx).edit_predictions;
|
||||
let provider = settings.provider;
|
||||
let custom_server_settings = match provider {
|
||||
settings::EditPredictionProvider::Ollama => settings.ollama.clone(),
|
||||
settings::EditPredictionProvider::OpenAiCompatibleApi => {
|
||||
settings.open_ai_compatible_api.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let http_client = cx.http_client();
|
||||
let request_start = cx.background_executor().now();
|
||||
let raw_config = store.zeta2_raw_config().cloned();
|
||||
let preferred_experiment = store.preferred_experiment().map(|s| s.to_owned());
|
||||
let open_ai_compatible_api_key = load_open_ai_compatible_api_key_if_needed(provider, cx);
|
||||
|
||||
let excerpt_path = buffer_path_with_id_fallback(snapshot.file(), &snapshot.text, cx);
|
||||
|
||||
let repo_url = if can_collect_data {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
project
|
||||
.read(cx)
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer_id, cx)
|
||||
.and_then(|(repo, _)| repo.read(cx).default_remote_url())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let client = store.client.clone();
|
||||
let llm_token = store.llm_token.clone();
|
||||
let organization_id = store
|
||||
.user_store
|
||||
.read(cx)
|
||||
.current_organization()
|
||||
.map(|organization| organization.id.clone());
|
||||
let app_version = AppVersion::global(cx);
|
||||
|
||||
struct Prediction {
|
||||
prompt_input: ZetaPromptInput,
|
||||
buffer: Entity<Buffer>,
|
||||
snapshot: BufferSnapshot,
|
||||
edits: Vec<(Range<Anchor>, Arc<str>)>,
|
||||
cursor_position: Option<PredictedCursorPosition>,
|
||||
editable_range_in_buffer: Range<usize>,
|
||||
}
|
||||
|
||||
let request_task = cx.background_spawn({
|
||||
async move {
|
||||
let local_zeta_version = custom_server_settings
|
||||
.as_ref()
|
||||
.and_then(|settings| match settings.prompt_format {
|
||||
EditPredictionPromptFormat::Zeta(version) => Some(version),
|
||||
EditPredictionPromptFormat::Infer => {
|
||||
match settings.model.to_ascii_lowercase().as_str() {
|
||||
"zeta" | "zeta1" => Some(ZetaVersion::Zeta1),
|
||||
"zeta2" => Some(ZetaVersion::Zeta2),
|
||||
"zeta2.1" => Some(ZetaVersion::Zeta2_1),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let zeta_format = raw_config
|
||||
.as_ref()
|
||||
.map(|config| config.format)
|
||||
.or(match local_zeta_version {
|
||||
ZetaVersion::Zeta1 => None,
|
||||
ZetaVersion::Zeta2 => Some(ZetaFormat::V0211SeedCoder),
|
||||
ZetaVersion::Zeta2_1 => Some(ZetaFormat::V0318SeedMultiRegions),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let cursor_offset = position.to_offset(&snapshot);
|
||||
let (full_context_offset_range, prompt_input) = zeta2_prompt_input(
|
||||
&snapshot,
|
||||
related_files,
|
||||
events,
|
||||
diagnostic_search_range,
|
||||
excerpt_path,
|
||||
cursor_offset,
|
||||
is_open_source,
|
||||
can_collect_data,
|
||||
repo_url,
|
||||
);
|
||||
|
||||
let formatted_prompt = format_zeta_prompt(&prompt_input, zeta_format);
|
||||
|
||||
if let Some(debug_tx) = &debug_tx {
|
||||
debug_tx
|
||||
.unbounded_send(DebugEvent::EditPredictionStarted(
|
||||
EditPredictionStartedDebugEvent {
|
||||
buffer: buffer.downgrade(),
|
||||
prompt: formatted_prompt.clone(),
|
||||
position,
|
||||
},
|
||||
))
|
||||
.ok();
|
||||
}
|
||||
|
||||
log::trace!("Sending edit prediction request");
|
||||
|
||||
let Some((request_id, output, model_version, usage)) =
|
||||
(if let Some(custom_settings) = &custom_server_settings {
|
||||
let max_tokens = custom_settings.max_output_tokens * 4;
|
||||
|
||||
Some(match local_zeta_version {
|
||||
ZetaVersion::Zeta1 => {
|
||||
let ranges = &prompt_input.excerpt_ranges;
|
||||
let editable_range_in_excerpt = ranges.editable_350.clone();
|
||||
let prompt = zeta1::format_zeta1_from_input(
|
||||
&prompt_input,
|
||||
editable_range_in_excerpt.clone(),
|
||||
ranges.editable_350_context_150.clone(),
|
||||
);
|
||||
let stop_tokens = vec![
|
||||
EDITABLE_REGION_END_MARKER.to_string(),
|
||||
format!("{EDITABLE_REGION_END_MARKER}\n"),
|
||||
format!("{EDITABLE_REGION_END_MARKER}\n\n"),
|
||||
format!("{EDITABLE_REGION_END_MARKER}\n\n\n"),
|
||||
];
|
||||
|
||||
let (response_text, request_id) = send_custom_server_request(
|
||||
provider,
|
||||
custom_settings,
|
||||
prompt,
|
||||
max_tokens,
|
||||
stop_tokens,
|
||||
open_ai_compatible_api_key.clone(),
|
||||
&http_client,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let request_id = EditPredictionId(request_id.into());
|
||||
let output_text = zeta1::clean_zeta1_model_output(&response_text);
|
||||
let parsed_output = output_text.map(|text| ParsedOutput {
|
||||
new_editable_region: text,
|
||||
range_in_excerpt: editable_range_in_excerpt,
|
||||
cursor_offset_in_new_editable_region: None,
|
||||
});
|
||||
|
||||
(request_id, parsed_output, None, None)
|
||||
}
|
||||
ZetaVersion::Zeta2 | ZetaVersion::Zeta2_1 => {
|
||||
let Some(prompt) = formatted_prompt.clone() else {
|
||||
return Ok((None, None));
|
||||
};
|
||||
let prefill = get_prefill(&prompt_input, zeta_format);
|
||||
let prompt = format!("{prompt}{prefill}");
|
||||
|
||||
let (response_text, request_id) = send_custom_server_request(
|
||||
provider,
|
||||
custom_settings,
|
||||
prompt,
|
||||
max_tokens,
|
||||
stop_tokens_for_format(zeta_format)
|
||||
.iter()
|
||||
.map(|token| token.to_string())
|
||||
.collect(),
|
||||
open_ai_compatible_api_key.clone(),
|
||||
&http_client,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let request_id = EditPredictionId(request_id.into());
|
||||
let output_text = if response_text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let output = format!("{prefill}{response_text}");
|
||||
Some(parse_zeta2_model_output(
|
||||
&output,
|
||||
zeta_format,
|
||||
&prompt_input,
|
||||
)?)
|
||||
};
|
||||
|
||||
(request_id, output_text, None, None)
|
||||
}
|
||||
})
|
||||
} else if let Some(config) = &raw_config {
|
||||
let Some(prompt) = format_zeta_prompt(&prompt_input, config.format) else {
|
||||
return Ok((None, None));
|
||||
};
|
||||
let prefill = get_prefill(&prompt_input, config.format);
|
||||
let prompt = format!("{prompt}{prefill}");
|
||||
let environment = config
|
||||
.environment
|
||||
.clone()
|
||||
.or_else(|| Some(config.format.to_string().to_lowercase()));
|
||||
let request = RawCompletionRequest {
|
||||
model: config.model_id.clone().unwrap_or_default(),
|
||||
prompt,
|
||||
temperature: None,
|
||||
stop: stop_tokens_for_format(config.format)
|
||||
.iter()
|
||||
.map(|token| std::borrow::Cow::Borrowed(*token))
|
||||
.collect(),
|
||||
max_tokens: Some(2048),
|
||||
environment,
|
||||
};
|
||||
|
||||
let (mut response, usage) = EditPredictionStore::send_raw_llm_request(
|
||||
request,
|
||||
client,
|
||||
None,
|
||||
llm_token,
|
||||
organization_id,
|
||||
app_version,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let request_id = EditPredictionId(response.id.clone().into());
|
||||
let output = if let Some(choice) = response.choices.pop() {
|
||||
let response = &choice.text;
|
||||
let output = format!("{prefill}{response}");
|
||||
Some(parse_zeta2_model_output(
|
||||
&output,
|
||||
config.format,
|
||||
&prompt_input,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Some((request_id, output, None, usage))
|
||||
} else {
|
||||
// Use V3 endpoint - server handles model/version selection and suffix stripping
|
||||
let (response, usage) = EditPredictionStore::send_v3_request(
|
||||
prompt_input.clone(),
|
||||
preferred_experiment.clone(),
|
||||
client,
|
||||
llm_token,
|
||||
organization_id,
|
||||
app_version,
|
||||
trigger,
|
||||
mode,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let request_id = EditPredictionId(response.request_id.into());
|
||||
let model_version = response.model_version;
|
||||
let parsed_output = ParsedOutput {
|
||||
new_editable_region: response.output,
|
||||
range_in_excerpt: response.editable_range,
|
||||
cursor_offset_in_new_editable_region: response.cursor_offset,
|
||||
};
|
||||
|
||||
Some((request_id, Some(parsed_output), model_version, usage))
|
||||
})
|
||||
else {
|
||||
return Ok((None, None));
|
||||
};
|
||||
|
||||
log::trace!("Got edit prediction response");
|
||||
|
||||
let Some(ParsedOutput {
|
||||
new_editable_region: mut output_text,
|
||||
range_in_excerpt: editable_range_in_excerpt,
|
||||
cursor_offset_in_new_editable_region: cursor_offset_in_output,
|
||||
}) = output
|
||||
else {
|
||||
return Ok((Some((request_id, None, model_version)), None));
|
||||
};
|
||||
|
||||
let editable_range_in_buffer = editable_range_in_excerpt.start
|
||||
+ full_context_offset_range.start
|
||||
..editable_range_in_excerpt.end + full_context_offset_range.start;
|
||||
|
||||
let mut old_text = snapshot
|
||||
.text_for_range(editable_range_in_buffer.clone())
|
||||
.collect::<String>();
|
||||
|
||||
if let Some(debug_tx) = &debug_tx {
|
||||
debug_tx
|
||||
.unbounded_send(DebugEvent::EditPredictionFinished(
|
||||
EditPredictionFinishedDebugEvent {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
model_output: Some(output_text.clone()),
|
||||
},
|
||||
))
|
||||
.ok();
|
||||
}
|
||||
|
||||
if !output_text.is_empty() && !output_text.ends_with('\n') {
|
||||
output_text.push('\n');
|
||||
}
|
||||
if !old_text.is_empty() && !old_text.ends_with('\n') {
|
||||
old_text.push('\n');
|
||||
}
|
||||
|
||||
let (edits, cursor_position) = compute_edits_and_cursor_position(
|
||||
old_text,
|
||||
&output_text,
|
||||
editable_range_in_buffer.start,
|
||||
cursor_offset_in_output,
|
||||
&snapshot,
|
||||
);
|
||||
|
||||
let prediction = Some(Prediction {
|
||||
prompt_input,
|
||||
buffer,
|
||||
snapshot: snapshot.clone(),
|
||||
edits,
|
||||
cursor_position,
|
||||
editable_range_in_buffer,
|
||||
});
|
||||
|
||||
anyhow::Ok((Some((request_id, prediction, model_version)), usage))
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let Some((id, prediction, model_version)) =
|
||||
handle_api_response(&this, request_task.await, cx)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let request_duration = cx.background_executor().now() - request_start;
|
||||
|
||||
let Some(Prediction {
|
||||
prompt_input: inputs,
|
||||
buffer: edited_buffer,
|
||||
snapshot: edited_buffer_snapshot,
|
||||
edits,
|
||||
cursor_position,
|
||||
editable_range_in_buffer,
|
||||
..
|
||||
}) = prediction
|
||||
else {
|
||||
return Ok(Some(EditPredictionResult {
|
||||
id,
|
||||
prediction: Err(EditPredictionRejectReason::Empty),
|
||||
model_version,
|
||||
e2e_latency: request_duration,
|
||||
}));
|
||||
};
|
||||
|
||||
let result = EditPredictionResult::new(
|
||||
id,
|
||||
&edited_buffer,
|
||||
&edited_buffer_snapshot,
|
||||
edits.into(),
|
||||
cursor_position,
|
||||
inputs,
|
||||
model_version,
|
||||
request_duration,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
if can_collect_data && let Ok(prediction) = &result.prediction {
|
||||
let weak_this = this.clone();
|
||||
let request_id = prediction.id.clone();
|
||||
let edited_buffer = edited_buffer.clone();
|
||||
let edited_buffer_snapshot = edited_buffer_snapshot.clone();
|
||||
let editable_range_in_buffer = editable_range_in_buffer.clone();
|
||||
let edit_preview = prediction.edit_preview.clone();
|
||||
let example_task = capture_data.and_then(|stored_events| {
|
||||
cx.update(|cx| {
|
||||
crate::capture_example(
|
||||
project.clone(),
|
||||
edited_buffer.clone(),
|
||||
position,
|
||||
stored_events,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
cx.spawn(async move |cx| {
|
||||
let example_spec = if let Some(task) = example_task {
|
||||
task.await.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
weak_this
|
||||
.update(cx, |this, cx| {
|
||||
this.enqueue_settled_prediction(
|
||||
request_id.clone(),
|
||||
&project,
|
||||
&edited_buffer,
|
||||
&edited_buffer_snapshot,
|
||||
editable_range_in_buffer,
|
||||
&edit_preview,
|
||||
example_spec,
|
||||
request_duration,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
Ok(Some(result))
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_api_response<T>(
|
||||
this: &WeakEntity<EditPredictionStore>,
|
||||
response: Result<(T, Option<client::EditPredictionUsage>)>,
|
||||
cx: &mut gpui::AsyncApp,
|
||||
) -> Result<T> {
|
||||
match response {
|
||||
Ok((data, usage)) => {
|
||||
if let Some(usage) = usage {
|
||||
this.update(cx, |this, cx| {
|
||||
this.user_store.update(cx, |user_store, cx| {
|
||||
user_store.update_edit_prediction_usage(usage, cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
Err(err) => {
|
||||
if err.is::<ZedUpdateRequiredError>() {
|
||||
cx.update(|cx| {
|
||||
this.update(cx, |this, _cx| {
|
||||
this.update_required = true;
|
||||
})
|
||||
.ok();
|
||||
|
||||
let error_message: SharedString = err.to_string().into();
|
||||
show_app_notification(
|
||||
NotificationId::unique::<ZedUpdateRequiredError>(),
|
||||
cx,
|
||||
move |cx| {
|
||||
cx.new(|cx| {
|
||||
ErrorMessagePrompt::new(error_message.clone(), cx)
|
||||
.with_link_button("Update Zed", "https://zed.dev/releases")
|
||||
})
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ACTIVE_BUFFER_DIAGNOSTIC_ADDITIONAL_CONTEXT_TOKEN_COUNT: usize = 100;
|
||||
const MAX_ACTIVE_BUFFER_DIAGNOSTICS_TO_COLLECT: usize = 20;
|
||||
const MAX_ACTIVE_BUFFER_DIAGNOSTIC_SNIPPET_TOKENS_TO_COLLECT: usize = 512;
|
||||
|
||||
pub(crate) fn active_buffer_diagnostics(
|
||||
snapshot: &language::BufferSnapshot,
|
||||
diagnostic_search_range: Range<Point>,
|
||||
cursor_row: u32,
|
||||
additional_context_token_count: usize,
|
||||
) -> Vec<zeta_prompt::ActiveBufferDiagnostic> {
|
||||
let mut diagnostics = snapshot
|
||||
.diagnostics_in_range::<Point, Point>(diagnostic_search_range, false)
|
||||
.collect::<Vec<_>>();
|
||||
diagnostics.sort_by_key(|entry| {
|
||||
cursor_row.abs_diff(entry.range.start.row) + cursor_row.abs_diff(entry.range.end.row)
|
||||
});
|
||||
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
let diagnostic_point_range = entry.range.clone();
|
||||
let snippet_point_range = cursor_excerpt::expand_context_syntactically_then_linewise(
|
||||
snapshot,
|
||||
diagnostic_point_range.clone(),
|
||||
additional_context_token_count,
|
||||
);
|
||||
|
||||
let severity = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => Some(1),
|
||||
DiagnosticSeverity::WARNING => Some(2),
|
||||
DiagnosticSeverity::INFORMATION => Some(3),
|
||||
DiagnosticSeverity::HINT => Some(4),
|
||||
_ => None,
|
||||
};
|
||||
(
|
||||
severity,
|
||||
entry.diagnostic.message.clone(),
|
||||
diagnostic_point_range,
|
||||
snippet_point_range,
|
||||
)
|
||||
})
|
||||
.take(MAX_ACTIVE_BUFFER_DIAGNOSTICS_TO_COLLECT)
|
||||
.map(
|
||||
|(severity, message, diagnostic_point_range, snippet_point_range)| {
|
||||
let (snippet, diagnostic_range_in_snippet) = if snippet_point_range.start
|
||||
== Point::new(0, 0)
|
||||
&& snippet_point_range.end == snapshot.max_point()
|
||||
{
|
||||
(String::new(), 0..0)
|
||||
} else {
|
||||
let snippet = snapshot
|
||||
.text_for_range(snippet_point_range.clone())
|
||||
.collect::<String>();
|
||||
let snippet = zeta_prompt::clamp_text_to_token_count(
|
||||
&snippet,
|
||||
MAX_ACTIVE_BUFFER_DIAGNOSTIC_SNIPPET_TOKENS_TO_COLLECT,
|
||||
)
|
||||
.to_string();
|
||||
let snippet_start_offset = snippet_point_range.start.to_offset(snapshot);
|
||||
let diagnostic_offset_range = diagnostic_point_range.to_offset(snapshot);
|
||||
let diagnostic_range_start = diagnostic_offset_range
|
||||
.start
|
||||
.saturating_sub(snippet_start_offset)
|
||||
.min(snippet.len());
|
||||
let diagnostic_range_end = diagnostic_offset_range
|
||||
.end
|
||||
.saturating_sub(snippet_start_offset)
|
||||
.min(snippet.len());
|
||||
(snippet, diagnostic_range_start..diagnostic_range_end)
|
||||
};
|
||||
zeta_prompt::ActiveBufferDiagnostic {
|
||||
severity,
|
||||
message,
|
||||
snippet,
|
||||
snippet_buffer_row_range: diagnostic_point_range.start.row
|
||||
..diagnostic_point_range.end.row,
|
||||
diagnostic_range_in_snippet,
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn zeta2_prompt_input(
|
||||
snapshot: &language::BufferSnapshot,
|
||||
related_files: Vec<zeta_prompt::RelatedFile>,
|
||||
events: Vec<Arc<zeta_prompt::Event>>,
|
||||
diagnostic_search_range: Range<Point>,
|
||||
excerpt_path: Arc<Path>,
|
||||
cursor_offset: usize,
|
||||
is_open_source: bool,
|
||||
can_collect_data: bool,
|
||||
repo_url: Option<String>,
|
||||
) -> (Range<usize>, zeta_prompt::ZetaPromptInput) {
|
||||
let (excerpt_point_range, excerpt_offset_range, cursor_offset_in_excerpt) =
|
||||
compute_cursor_excerpt(snapshot, cursor_offset);
|
||||
|
||||
let cursor_excerpt: Arc<str> = snapshot
|
||||
.text_for_range(excerpt_point_range.clone())
|
||||
.collect::<String>()
|
||||
.into();
|
||||
let syntax_ranges = compute_syntax_ranges(snapshot, cursor_offset, &excerpt_offset_range);
|
||||
let excerpt_ranges = zeta_prompt::compute_legacy_excerpt_ranges(
|
||||
&cursor_excerpt,
|
||||
cursor_offset_in_excerpt,
|
||||
&syntax_ranges,
|
||||
);
|
||||
|
||||
let active_buffer_diagnostics = active_buffer_diagnostics(
|
||||
snapshot,
|
||||
diagnostic_search_range,
|
||||
snapshot.offset_to_point(cursor_offset).row,
|
||||
ACTIVE_BUFFER_DIAGNOSTIC_ADDITIONAL_CONTEXT_TOKEN_COUNT,
|
||||
);
|
||||
|
||||
let prompt_input = zeta_prompt::ZetaPromptInput {
|
||||
cursor_path: excerpt_path,
|
||||
cursor_excerpt,
|
||||
cursor_offset_in_excerpt,
|
||||
excerpt_start_row: Some(excerpt_point_range.start.row),
|
||||
events,
|
||||
related_files: Some(related_files),
|
||||
active_buffer_diagnostics,
|
||||
excerpt_ranges,
|
||||
syntax_ranges: Some(syntax_ranges),
|
||||
in_open_source_repo: is_open_source,
|
||||
can_collect_data,
|
||||
repo_url,
|
||||
};
|
||||
(excerpt_offset_range, prompt_input)
|
||||
}
|
||||
|
||||
pub(crate) fn edit_prediction_accepted(
|
||||
store: &EditPredictionStore,
|
||||
current_prediction: CurrentEditPrediction,
|
||||
cx: &App,
|
||||
) {
|
||||
if store.zeta2_raw_config().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let request_id = current_prediction.prediction.id.to_string();
|
||||
let model_version = current_prediction.prediction.model_version;
|
||||
let e2e_latency = current_prediction.e2e_latency;
|
||||
let client = store.client.clone();
|
||||
let llm_token = store.llm_token.clone();
|
||||
let organization_id = store
|
||||
.user_store
|
||||
.read(cx)
|
||||
.current_organization()
|
||||
.map(|organization| organization.id.clone());
|
||||
let app_version = AppVersion::global(cx);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let body = serde_json::to_string(&AcceptEditPredictionBody {
|
||||
request_id,
|
||||
model_version,
|
||||
e2e_latency_ms: Some(e2e_latency.as_millis()),
|
||||
})?;
|
||||
|
||||
let url = client
|
||||
.http_client()
|
||||
.build_zed_llm_url("/predict_edits/accept", &[])?;
|
||||
EditPredictionStore::send_api_request::<()>(
|
||||
move |builder| Ok(builder.uri(url.as_ref()).body(body.clone().into())?),
|
||||
client,
|
||||
llm_token,
|
||||
organization_id,
|
||||
app_version,
|
||||
)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn compute_edits(
|
||||
old_text: String,
|
||||
new_text: &str,
|
||||
offset: usize,
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> Vec<(Range<Anchor>, Arc<str>)> {
|
||||
compute_edits_and_cursor_position(old_text, new_text, offset, None, snapshot).0
|
||||
}
|
||||
|
||||
pub fn compute_edits_and_cursor_position(
|
||||
old_text: String,
|
||||
new_text: &str,
|
||||
offset: usize,
|
||||
cursor_offset_in_new_text: Option<usize>,
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> (
|
||||
Vec<(Range<Anchor>, Arc<str>)>,
|
||||
Option<PredictedCursorPosition>,
|
||||
) {
|
||||
let diffs = text_diff(&old_text, new_text);
|
||||
|
||||
// Delta represents the cumulative change in byte count from all preceding edits.
|
||||
// new_offset = old_offset + delta, so old_offset = new_offset - delta
|
||||
let mut delta: isize = 0;
|
||||
let mut cursor_position: Option<PredictedCursorPosition> = None;
|
||||
let buffer_len = snapshot.len();
|
||||
|
||||
let edits = diffs
|
||||
.iter()
|
||||
.map(|(raw_old_range, new_text)| {
|
||||
// Compute cursor position if it falls within or before this edit.
|
||||
if let (Some(cursor_offset), None) = (cursor_offset_in_new_text, cursor_position) {
|
||||
let edit_start_in_new = (raw_old_range.start as isize + delta) as usize;
|
||||
let edit_end_in_new = edit_start_in_new + new_text.len();
|
||||
|
||||
if cursor_offset < edit_start_in_new {
|
||||
let cursor_in_old = (cursor_offset as isize - delta) as usize;
|
||||
let buffer_offset = (offset + cursor_in_old).min(buffer_len);
|
||||
cursor_position = Some(PredictedCursorPosition::at_anchor(
|
||||
snapshot.anchor_after(buffer_offset),
|
||||
));
|
||||
} else if cursor_offset < edit_end_in_new {
|
||||
let buffer_offset = (offset + raw_old_range.start).min(buffer_len);
|
||||
let offset_within_insertion = cursor_offset - edit_start_in_new;
|
||||
cursor_position = Some(PredictedCursorPosition::new(
|
||||
snapshot.anchor_before(buffer_offset),
|
||||
offset_within_insertion,
|
||||
));
|
||||
}
|
||||
|
||||
delta += new_text.len() as isize - raw_old_range.len() as isize;
|
||||
}
|
||||
|
||||
// Compute the edit with prefix/suffix trimming.
|
||||
let mut old_range = raw_old_range.clone();
|
||||
let old_slice = &old_text[old_range.clone()];
|
||||
|
||||
let prefix_len = common_prefix(old_slice.chars(), new_text.chars());
|
||||
let suffix_len = common_prefix(
|
||||
old_slice[prefix_len..].chars().rev(),
|
||||
new_text[prefix_len..].chars().rev(),
|
||||
);
|
||||
|
||||
old_range.start += offset;
|
||||
old_range.end += offset;
|
||||
old_range.start += prefix_len;
|
||||
old_range.end -= suffix_len;
|
||||
|
||||
old_range.start = old_range.start.min(buffer_len);
|
||||
old_range.end = old_range.end.min(buffer_len);
|
||||
|
||||
let new_text = new_text[prefix_len..new_text.len() - suffix_len].into();
|
||||
let range = if old_range.is_empty() {
|
||||
let anchor = snapshot.anchor_after(old_range.start);
|
||||
anchor..anchor
|
||||
} else {
|
||||
snapshot.anchor_after(old_range.start)..snapshot.anchor_before(old_range.end)
|
||||
};
|
||||
(range, new_text)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let (Some(cursor_offset), None) = (cursor_offset_in_new_text, cursor_position) {
|
||||
let cursor_in_old = (cursor_offset as isize - delta) as usize;
|
||||
let buffer_offset = snapshot.clip_offset(offset + cursor_in_old, Bias::Right);
|
||||
cursor_position = Some(PredictedCursorPosition::at_anchor(
|
||||
snapshot.anchor_after(buffer_offset),
|
||||
));
|
||||
}
|
||||
|
||||
(edits, cursor_position)
|
||||
}
|
||||
|
||||
fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
|
||||
a.zip(b)
|
||||
.take_while(|(a, b)| a == b)
|
||||
.map(|(a, _)| a.len_utf8())
|
||||
.sum()
|
||||
}
|
||||
Reference in New Issue
Block a user