Send telemetry (#1702)

* restart on auth

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix deps

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* hash the iuser

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add comment;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* zip up the contents

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix logic

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more code coverage

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* u[dates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* u[dates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* up[dates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more coverage

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more coverage

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup dead code

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* start of accept / reject

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* accept/reject

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2024-03-12 23:57:43 -07:00
committed by GitHub
parent dfc51e6c30
commit 2d979b56f5
17 changed files with 1726 additions and 155 deletions

View File

@ -62,8 +62,16 @@ jobs:
shell: bash
run: |-
cd "${{ matrix.dir }}"
cargo nextest run --workspace --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
cargo llvm-cov nextest --all --lcov --output-path lcov.info --test-threads=1 --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
with:
token: ${{secrets.CODECOV_TOKEN}}
fail_ci_if_error: true
flags: wasm-lib
verbose: true
files: lcov.info

View File

@ -40,9 +40,11 @@ export function App() {
const filePath = useAbsoluteFilePath()
const { onProjectOpen } = useLspContext()
const projectName = project?.name || null
const projectPath = project?.path || null
useEffect(() => {
onProjectOpen(project || null, file || null)
}, [])
onProjectOpen({ name: projectName, path: projectPath }, file || null)
}, [projectName, projectPath])
useHotKeyListener()
const {

View File

@ -14,7 +14,6 @@ import { LanguageSupport } from '@codemirror/language'
import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths'
import { FileEntry } from '@tauri-apps/api/fs'
import { ProjectWithEntryPointMetadata } from 'lib/types'
const DEFAULT_FILE_NAME: string = 'main.kcl'
@ -40,7 +39,7 @@ type LspContext = {
redirect: boolean
) => void
onProjectOpen: (
project: ProjectWithEntryPointMetadata | null,
project: { name: string | null; path: string | null } | null,
file: FileEntry | null
) => void
onFileOpen: (filePath: string | null, projectPath: string | null) => void
@ -69,6 +68,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
}))
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const navigate = useNavigate()
// So this is a bit weird, we need to initialize the lsp server and client.
@ -80,7 +80,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const client = new Client(fromServer, intoServer)
if (!TEST) {
Server.initialize(intoServer, fromServer).then((lspServer) => {
const token = auth?.context?.token
lspServer.start('kcl', token)
setIsKclLspServerReady(true)
})
@ -88,7 +87,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const lspClient = new LanguageServerClient({ client, name: 'kcl' })
return { lspClient }
}, [setIsKclLspServerReady])
}, [setIsKclLspServerReady, token])
// Here we initialize the plugin which will start the client.
// Now that we have multi-file support the name of the file is a dep of
@ -116,7 +115,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const client = new Client(fromServer, intoServer)
if (!TEST) {
Server.initialize(intoServer, fromServer).then((lspServer) => {
const token = auth?.context?.token
lspServer.start('copilot', token)
setIsCopilotLspServerReady(true)
})
@ -124,7 +122,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const lspClient = new LanguageServerClient({ client, name: 'copilot' })
return { lspClient }
}, [setIsCopilotLspServerReady])
}, [setIsCopilotLspServerReady, token])
// Here we initialize the plugin which will start the client.
// When we have multi-file support the name of the file will be a dep of
@ -172,7 +170,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
}
const onProjectOpen = (
project: ProjectWithEntryPointMetadata | null,
project: { name: string | null; path: string | null } | null,
file: FileEntry | null
) => {
const projectName = project?.name || 'ProjectRoot'

View File

@ -2,67 +2,10 @@ import type * as LSP from 'vscode-languageserver-protocol'
import Client from './client'
import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens'
import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin'
export interface CopilotGetCompletionsParams {
doc: {
source: string
tabSize: number
indentSize: number
insertSpaces: boolean
path: string
uri: string
relativePath: string
languageId: string
position: {
line: number
character: number
}
}
}
interface CopilotGetCompletionsResult {
completions: {
text: string
position: {
line: number
character: number
}
uuid: string
range: {
start: {
line: number
character: number
}
end: {
line: number
character: number
}
}
displayText: string
point: {
line: number
character: number
}
region: {
start: {
line: number
character: number
}
end: {
line: number
character: number
}
}
}[]
}
interface CopilotAcceptCompletionParams {
uuid: string
}
interface CopilotRejectCompletionParams {
uuids: string[]
}
import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams'
import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse'
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
@ -78,7 +21,7 @@ interface LSPRequestMap {
LSP.SemanticTokensParams,
LSP.SemanticTokens
]
getCompletions: [CopilotGetCompletionsParams, CopilotGetCompletionsResult]
getCompletions: [CopilotLspCompletionParams, CopilotCompletionResponse]
notifyAccepted: [CopilotAcceptCompletionParams, any]
notifyRejected: [CopilotRejectCompletionParams, any]
}
@ -271,7 +214,7 @@ export class LanguageServerClient {
return this.client.notify(method, params)
}
async getCompletion(params: CopilotGetCompletionsParams) {
async getCompletion(params: CopilotLspCompletionParams) {
const response = await this.request('getCompletions', params)
//
this.queuedUids = [...response.completions.map((c) => c.uuid)]

View File

@ -37,6 +37,7 @@ import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
import { useHotkeys } from 'react-hotkeys-hook'
import { isTauri } from 'lib/isTauri'
import { kclManager } from 'lang/KclSingleton'
import { useLspContext } from 'components/LspProvider'
// This route only opens in the Tauri desktop context for now,
// as defined in Router.tsx, so we can use the Tauri APIs and types.
@ -51,6 +52,7 @@ const Home = () => {
send: sendToSettings,
},
} = useSettingsAuthContext()
const { onProjectOpen } = useLspContext()
// Set the default directory if it's been updated
// during the loading of the home page. This is wrapped
@ -84,12 +86,16 @@ const Home = () => {
event: EventFrom<typeof homeMachine>
) => {
if (event.data && 'name' in event.data) {
commandBarSend({ type: 'Close' })
navigate(
`${paths.FILE}/${encodeURIComponent(
context.defaultDirectory + sep + event.data.name
)}`
let projectPath = context.defaultDirectory + sep + event.data.name
onProjectOpen(
{
name: event.data.name,
path: projectPath,
},
null
)
commandBarSend({ type: 'Close' })
navigate(`${paths.FILE}/${encodeURIComponent(projectPath)}`)
}
},
toastSuccess: (_, event) => toast.success((event.data || '') + ''),

View File

@ -522,9 +522,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.34"
version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
dependencies = [
"android-tzdata",
"iana-time-zone",
@ -1914,6 +1914,7 @@ dependencies = [
"async-recursion",
"async-trait",
"bson",
"chrono",
"clap",
"criterion",
"dashmap",
@ -1929,6 +1930,7 @@ dependencies = [
"kittycad-execution-plan-macros",
"kittycad-execution-plan-traits",
"lazy_static",
"mime_guess",
"parse-display 0.9.0",
"pretty_assertions",
"reqwest",
@ -1936,6 +1938,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"sha2",
"thiserror",
"tokio",
"tokio-tungstenite",
@ -1946,6 +1949,7 @@ dependencies = [
"wasm-bindgen-futures",
"web-sys",
"winnow",
"zip",
]
[[package]]
@ -1962,9 +1966,9 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.2.59"
version = "0.2.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4080db4364c103601db486e4a8aa889ea56c011991e4c454373d8050a165d3da"
checksum = "f8aa5906d0730bd90f6b3331fe57c04951d00743169a29ee96408767b4060605"
dependencies = [
"anyhow",
"async-trait",
@ -1978,6 +1982,7 @@ dependencies = [
"http 0.2.9",
"itertools 0.10.5",
"log",
"mime_guess",
"parse-display 0.8.2",
"phonenumber",
"rand 0.8.5",
@ -2000,7 +2005,7 @@ dependencies = [
[[package]]
name = "kittycad-execution-plan"
version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [
"bytes",
"insta",
@ -2020,7 +2025,7 @@ dependencies = [
[[package]]
name = "kittycad-execution-plan-macros"
version = "0.1.8"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [
"proc-macro2",
"quote",
@ -2030,7 +2035,7 @@ dependencies = [
[[package]]
name = "kittycad-execution-plan-traits"
version = "0.1.12"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [
"serde",
"thiserror",
@ -2040,7 +2045,7 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.1.28"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [
"anyhow",
"chrono",
@ -2068,7 +2073,7 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds-macros"
version = "0.1.2"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [
"proc-macro2",
"quote",
@ -2077,8 +2082,8 @@ dependencies = [
[[package]]
name = "kittycad-modeling-session"
version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c554cbeda3f217c1baab8a33ffad50e2ecdc8ab9"
version = "0.1.1"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#c7722adf9744b9e4eead0a7a88662ad5e7e3adbf"
dependencies = [
"futures",
"kittycad",
@ -5505,6 +5510,17 @@ dependencies = [
"syn 2.0.52",
]
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"byteorder",
"crc32fast",
"crossbeam-utils",
]
[[package]]
name = "zune-inflate"
version = "0.2.54"

View File

@ -59,7 +59,7 @@ members = [
]
[workspace.dependencies]
kittycad = { version = "0.2.59", default-features = false, features = ["js", "requests"] }
kittycad = { version = "0.2.60", default-features = false, features = ["js", "requests"] }
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-traits = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }

View File

@ -14,6 +14,7 @@ keywords = ["kcl", "KittyCAD", "CAD"]
anyhow = { version = "1.0.80", features = ["backtrace"] }
async-recursion = "1.0.5"
async-trait = "0.1.77"
chrono = "0.4.35"
clap = { version = "4.5.2", features = ["cargo", "derive", "env", "unicode"], optional = true }
dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] }
@ -25,16 +26,19 @@ kittycad = { workspace = true }
kittycad-execution-plan-macros = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }
lazy_static = "1.4.0"
mime_guess = "2.0.4"
parse-display = "0.9.0"
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1"
schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
sha2 = "0.10.8"
thiserror = "1.0.57"
ts-rs = { version = "7.1.1", features = ["uuid-impl"] }
uuid = { version = "1.7.0", features = ["v4", "js", "serde"] }
winnow = "0.5.40"
zip = { version = "0.6.6", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" }

View File

@ -75,11 +75,33 @@ pub trait Backend {
}
async fn do_did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
// If we are adding a folder that we were previously on, we should not clear the
// state.
let should_clear = if !params.event.added.is_empty() {
let mut should_clear = false;
for folder in params.event.added.iter() {
if !self
.workspace_folders()
.iter()
.any(|f| f.uri == folder.uri && f.name == folder.name)
{
should_clear = true;
break;
}
}
should_clear
} else {
!(params.event.removed.is_empty() && params.event.added.is_empty())
};
self.add_workspace_folders(params.event.added.clone());
self.remove_workspace_folders(params.event.removed);
// Remove the code from the current code map.
// We do this since it means the user is changing projects so let's refresh the state.
if !self.current_code_map().is_empty() && should_clear {
self.clear_code_state();
}
for added in params.event.added {
// Try to read all the files in the project.
let project_dir = added.uri.to_string().replace("file://", "");
@ -181,14 +203,5 @@ pub trait Backend {
self.client()
.log_message(MessageType::INFO, format!("document closed: {:?}", params))
.await;
self.client()
.log_message(MessageType::INFO, format!("uri: {:?}", params.text_document.uri))
.await;
// Get the workspace folders.
// The key of the workspace folder is the project name.
let workspace_folders = self.workspace_folders();
self.client()
.log_message(MessageType::INFO, format!("workspace: {:?}", workspace_folders))
.await;
}
}

View File

@ -29,6 +29,8 @@ use crate::lsp::{
copilot::types::{CopilotCompletionResponse, CopilotEditorInfo, CopilotLspCompletionParams, DocParams},
};
use self::types::{CopilotAcceptCompletionParams, CopilotCompletionTelemetry, CopilotRejectCompletionParams};
#[derive(Deserialize, Serialize, Debug)]
pub struct Success {
success: bool,
@ -39,7 +41,7 @@ impl Success {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Backend {
/// The client is used to send notifications and requests to the client.
pub client: tower_lsp::Client,
@ -54,7 +56,9 @@ pub struct Backend {
/// The editor info is used to store information about the editor.
pub editor_info: Arc<RwLock<CopilotEditorInfo>>,
/// The cache is used to store the results of previous requests.
pub cache: cache::CopilotCache,
pub cache: Arc<cache::CopilotCache>,
/// Storage so we can send telemetry data back out.
pub telemetry: DashMap<uuid::Uuid, CopilotCompletionTelemetry>,
}
// Implement the shared backend trait for the language server.
@ -158,7 +162,7 @@ impl Backend {
let pos = params.doc.position;
let uri = params.doc.uri.to_string();
let rope = ropey::Rope::from_str(&params.doc.source);
let offset = crate::lsp::util::position_to_offset(pos, &rope).unwrap_or_default();
let offset = crate::lsp::util::position_to_offset(pos.into(), &rope).unwrap_or_default();
Ok(DocParams {
uri: uri.to_string(),
@ -166,7 +170,7 @@ impl Backend {
language: params.doc.language_id.to_string(),
prefix: crate::lsp::util::get_text_before(offset, &rope).unwrap_or_default(),
suffix: crate::lsp::util::get_text_after(offset, &rope).unwrap_or_default(),
line_before: crate::lsp::util::get_line_before(pos, &rope).unwrap_or_default(),
line_before: crate::lsp::util::get_line_before(pos.into(), &rope).unwrap_or_default(),
rope,
})
}
@ -185,37 +189,69 @@ impl Backend {
let line_before = doc_params.line_before.to_string();
// Let's not call it yet since it's not our model.
/*let completion_list = self
// We will need to wrap in spawn_local like we do in kcl/mod.rs for wasm only.
#[cfg(test)]
let completion_list = self
.get_completions(doc_params.language, doc_params.prefix, doc_params.suffix)
.await
.map_err(|err| Error {
code: tower_lsp::jsonrpc::ErrorCode::from(69),
data: None,
message: Cow::from(format!("Failed to get completions: {}", err)),
})?;*/
})?;
#[cfg(not(test))]
let completion_list = vec![];
let response = CopilotCompletionResponse::from_str_vec(completion_list, line_before, doc_params.pos);
// Set the telemetry data for each completion.
for completion in response.completions.iter() {
let telemetry = CopilotCompletionTelemetry {
completion: completion.clone(),
params: params.clone(),
};
self.telemetry.insert(completion.uuid, telemetry);
}
self.cache
.set_cached_result(&doc_params.uri, &doc_params.pos.line, &response);
Ok(response)
}
pub async fn accept_completions(&self, params: Vec<String>) {
pub async fn accept_completion(&self, params: CopilotAcceptCompletionParams) {
self.client
.log_message(MessageType::INFO, format!("Accepted completions: {:?}", params))
.await;
// TODO: send telemetry data back out that we accepted the completions
// Get the original telemetry data.
let Some((_, original)) = self.telemetry.remove(&params.uuid) else {
return;
};
self.client
.log_message(MessageType::INFO, format!("Original telemetry: {:?}", original))
.await;
// TODO: Send the telemetry data to the zoo api.
}
pub async fn reject_completions(&self, params: Vec<String>) {
pub async fn reject_completions(&self, params: CopilotRejectCompletionParams) {
self.client
.log_message(MessageType::INFO, format!("Rejected completions: {:?}", params))
.await;
// TODO: send telemetry data back out that we rejected the completions
// Get the original telemetry data.
let mut originals: Vec<CopilotCompletionTelemetry> = Default::default();
for uuid in params.uuids {
if let Some((_, original)) = self.telemetry.remove(&uuid) {
originals.push(original);
}
}
self.client
.log_message(MessageType::INFO, format!("Original telemetry: {:?}", originals))
.await;
// TODO: Send the telemetry data to the zoo api.
}
}

View File

@ -2,18 +2,58 @@
use ropey::Rope;
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::{Position, Range};
#[derive(Debug, Serialize, Deserialize, Clone)]
/// Position in a text document expressed as zero-based line and character offset.
/// A position is between two characters like an 'insert' cursor in a editor.
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default, Deserialize, Serialize, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
pub struct CopilotCyclingCompletion {
pub display_text: String, // partial text
pub text: String, // fulltext
pub range: Range, // start char always 0
pub position: Position,
#[ts(export)]
pub struct CopilotPosition {
/// Line position in a document (zero-based).
pub line: u32,
/// Character offset on a line in a document (zero-based). The meaning of this
/// offset is determined by the negotiated `PositionEncodingKind`.
///
/// If the character value is greater than the line length it defaults back
/// to the line length.
pub character: u32,
}
#[derive(Debug, Serialize, Deserialize)]
impl From<CopilotPosition> for tower_lsp::lsp_types::Position {
fn from(position: CopilotPosition) -> Self {
tower_lsp::lsp_types::Position {
line: position.line,
character: position.character,
}
}
}
/// A range in a text document expressed as (zero-based) start and end positions.
/// A range is comparable to a selection in an editor. Therefore the end position is exclusive.
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Deserialize, Serialize, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotRange {
/// The range's start position.
pub start: CopilotPosition,
/// The range's end position.
pub end: CopilotPosition,
}
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotCyclingCompletion {
pub uuid: uuid::Uuid, // unique id we use for tracking accepted or rejected completions
pub display_text: String, // partial text
pub text: String, // fulltext
pub range: CopilotRange, // start char always 0
pub position: CopilotPosition,
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Choices {
pub text: String,
pub index: i16,
@ -21,14 +61,16 @@ pub struct Choices {
pub logprobs: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotCompletionResponse {
pub completions: Vec<CopilotCyclingCompletion>,
pub cancellation_reason: Option<String>,
}
impl CopilotCompletionResponse {
pub fn from_str_vec(str_vec: Vec<String>, line_before: String, pos: Position) -> Self {
pub fn from_str_vec(str_vec: Vec<String>, line_before: String, pos: CopilotPosition) -> Self {
let completions = str_vec
.iter()
.map(|x| CopilotCyclingCompletion::new(x.to_string(), line_before.to_string(), pos))
@ -41,19 +83,20 @@ impl CopilotCompletionResponse {
}
impl CopilotCyclingCompletion {
pub fn new(text: String, line_before: String, position: Position) -> Self {
pub fn new(text: String, line_before: String, position: CopilotPosition) -> Self {
let display_text = text.clone();
let text = format!("{}{}", line_before, text);
let end_char = text.find('\n').unwrap_or(text.len()) as u32;
Self {
uuid: uuid::Uuid::new_v4(),
display_text, // partial text
text, // fulltext
range: Range {
start: Position {
range: CopilotRange {
start: CopilotPosition {
character: 0,
line: position.line,
},
end: Position {
end: CopilotPosition {
character: end_char,
line: position.line,
},
@ -63,17 +106,19 @@ impl CopilotCyclingCompletion {
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
struct LanguageEntry {
language_id: String,
#[ts(export)]
pub struct LanguageEntry {
pub language_id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
struct EditorConfiguration {
disabled_languages: Vec<LanguageEntry>,
enable_auto_completions: bool,
#[ts(export)]
pub struct EditorConfiguration {
pub disabled_languages: Vec<LanguageEntry>,
pub enable_auto_completions: bool,
}
impl Default for EditorConfiguration {
@ -84,47 +129,77 @@ impl Default for EditorConfiguration {
}
}
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
struct EditorInfo {
name: String,
version: String,
#[ts(export)]
pub struct EditorInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotEditorInfo {
editor_configuration: EditorConfiguration,
editor_info: EditorInfo,
editor_plugin_info: EditorInfo,
pub editor_configuration: EditorConfiguration,
pub editor_info: EditorInfo,
pub editor_plugin_info: EditorInfo,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct DocParams {
#[serde(skip)]
pub rope: Rope,
pub uri: String,
pub pos: Position,
pub pos: CopilotPosition,
pub language: String,
pub line_before: String,
pub prefix: String,
pub suffix: String,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotLspCompletionParams {
pub doc: CopilotDocParams,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotDocParams {
pub indent_size: u32,
pub insert_spaces: bool,
pub language_id: String,
pub path: String,
pub position: Position,
pub position: CopilotPosition,
pub relative_path: String,
pub source: String,
pub tab_size: u32,
pub uri: String,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotAcceptCompletionParams {
pub uuid: uuid::Uuid,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotRejectCompletionParams {
pub uuids: Vec<uuid::Uuid>,
}
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CopilotCompletionTelemetry {
pub completion: CopilotCyclingCompletion,
pub params: CopilotLspCompletionParams,
}

View File

@ -1,11 +1,12 @@
//! Functions for the `kcl` lsp server.
use std::collections::HashMap;
use std::{collections::HashMap, io::Write, str::FromStr};
use anyhow::Result;
#[cfg(feature = "cli")]
use clap::Parser;
use dashmap::DashMap;
use sha2::Digest;
use tower_lsp::{
jsonrpc::Result as RpcResult,
lsp_types::{
@ -44,6 +45,7 @@ pub struct Server {
}
/// The lsp server backend.
#[derive(Clone)]
pub struct Backend {
/// The client for the backend.
pub client: Client,
@ -285,6 +287,87 @@ impl Backend {
completions
}
pub fn create_zip(&self) -> Result<Vec<u8>> {
// Collect all the file data we know.
let mut buf = vec![];
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
for entry in self.current_code_map.iter() {
let file_name = entry.key().replace("file://", "").to_string();
let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file(file_name, options)?;
zip.write_all(entry.value())?;
}
// Apply the changes you've made.
// Dropping the `ZipWriter` will have the same effect, but may silently fail
zip.finish()?;
drop(zip);
Ok(buf)
}
pub async fn send_telemetry(&self) -> Result<()> {
// Get information about the user.
let user = self
.zoo_client
.users()
.get_self()
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
// Hash the user's id.
// Create a SHA-256 object
let mut hasher = sha2::Sha256::new();
// Write input message
hasher.update(user.id);
// Read hash digest and consume hasher
let result = hasher.finalize();
// Get the hash as a string.
let user_id_hash = format!("{:x}", result);
// Get the workspace folders.
// The key of the workspace folder is the project name.
let workspace_folders = self.workspace_folders();
let project_names: Vec<String> = workspace_folders.iter().map(|v| v.name.clone()).collect::<Vec<_>>();
// Get the first name.
let project_name = project_names
.first()
.ok_or_else(|| anyhow::anyhow!("no project names"))?
.to_string();
// Send the telemetry data.
self.zoo_client
.meta()
.create_event(
vec![kittycad::types::multipart::Attachment {
// Clean the URI part.
name: "attachment".to_string(),
filename: Some("attachment.zip".to_string()),
content_type: Some("application/x-zip".to_string()),
data: self.create_zip()?,
}],
&kittycad::types::Event {
// This gets generated server side so leave empty for now.
attachment_uri: None,
created_at: chrono::Utc::now(),
event_type: kittycad::types::ModelingAppEventType::SuccessfulCompileBeforeClose,
last_compiled_at: Some(chrono::Utc::now()),
// We do not have project descriptions yet.
project_description: None,
project_name,
// The UUID for the modeling app.
// We can unwrap here because we know it will not panic.
source_id: uuid::Uuid::from_str("70178592-dfca-47b3-bd2d-6fce2bcaee04").unwrap(),
type_: kittycad::types::Type::ModelingAppEvent,
user_id: user_id_hash,
},
)
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
Ok(())
}
}
#[tower_lsp::async_trait]
@ -402,7 +485,32 @@ impl LanguageServer for Backend {
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
self.do_did_close(params).await
self.do_did_close(params).await;
// Inject telemetry if we can train on the user's code.
// Return early if we cannot.
if !self.can_send_telemetry {
return;
}
// In wasm this needs to be spawn_local since fucking reqwests doesn't implement Send for wasm.
#[cfg(target_arch = "wasm32")]
{
let be = self.clone();
wasm_bindgen_futures::spawn_local(async move {
if let Err(err) = be.send_telemetry().await {
be.client
.log_message(MessageType::WARNING, format!("failed to send telemetry: {}", err))
.await;
}
});
}
#[cfg(not(target_arch = "wasm32"))]
if let Err(err) = self.send_telemetry().await {
self.client
.log_message(MessageType::WARNING, format!("failed to send telemetry: {}", err))
.await;
}
}
async fn hover(&self, params: HoverParams) -> RpcResult<Option<Hover>> {

View File

@ -3,4 +3,6 @@
mod backend;
pub mod copilot;
pub mod kcl;
#[cfg(test)]
mod tests;
mod util;

File diff suppressed because it is too large Load Diff

View File

@ -270,7 +270,8 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, is_dev: bool)
workspace_folders: Default::default(),
current_code_map: Default::default(),
editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())),
cache: kcl_lib::lsp::copilot::cache::CopilotCache::new(),
cache: Arc::new(kcl_lib::lsp::copilot::cache::CopilotCache::new()),
telemetry: Default::default(),
zoo_client,
})
.custom_method("setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info)
@ -278,7 +279,7 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, is_dev: bool)
"getCompletions",
kcl_lib::lsp::copilot::Backend::get_completions_cycling,
)
.custom_method("notifyAccepted", kcl_lib::lsp::copilot::Backend::accept_completions)
.custom_method("notifyAccepted", kcl_lib::lsp::copilot::Backend::accept_completion)
.custom_method("notifyRejected", kcl_lib::lsp::copilot::Backend::reject_completions)
.finish();

View File

@ -28,7 +28,7 @@ async fn execute_and_snapshot(code: &str, units: kittycad::types::UnitLength) ->
let ws = client
.modeling()
.commands_ws(None, None, None, None, Some(false))
.commands_ws(None, None, None, None, None, Some(false))
.await?;
// Create a temporary file to write the output to.

View File

@ -30,7 +30,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
let ws = client
.modeling()
.commands_ws(None, None, None, None, Some(false))
.commands_ws(None, None, None, None, None, Some(false))
.await?;
let tokens = kcl_lib::token::lexer(code);